diff --git a/internal/cmd/mail.go b/internal/cmd/mail.go index 26525e18..858934ca 100644 --- a/internal/cmd/mail.go +++ b/internal/cmd/mail.go @@ -22,6 +22,8 @@ var ( mailInboxJSON bool mailReadJSON bool mailInboxUnread bool + mailCheckInject bool + mailCheckJSON bool ) var mailCmd = &cobra.Command{ @@ -87,6 +89,25 @@ This closes the message in beads.`, RunE: runMailDelete, } +var mailCheckCmd = &cobra.Command{ + Use: "check", + Short: "Check for new mail (for hooks)", + Long: `Check for new mail - useful for Claude Code hooks. + +Exit codes (normal mode): + 0 - New mail available + 1 - No new mail + +Exit codes (--inject mode): + 0 - Always (hooks should never block) + Output: system-reminder if mail exists, silent if no mail + +Examples: + gt mail check # Simple check + gt mail check --inject # For hooks`, + RunE: runMailCheck, +} + func init() { // Send flags mailSendCmd.Flags().StringVarP(&mailSubject, "subject", "s", "", "Message subject (required)") @@ -102,11 +123,16 @@ func init() { // Read flags mailReadCmd.Flags().BoolVar(&mailReadJSON, "json", false, "Output as JSON") + // Check flags + mailCheckCmd.Flags().BoolVar(&mailCheckInject, "inject", false, "Output format for Claude Code hooks") + mailCheckCmd.Flags().BoolVar(&mailCheckJSON, "json", false, "Output as JSON") + // Add subcommands mailCmd.AddCommand(mailSendCmd) mailCmd.AddCommand(mailInboxCmd) mailCmd.AddCommand(mailReadCmd) mailCmd.AddCommand(mailDeleteCmd) + mailCmd.AddCommand(mailCheckCmd) rootCmd.AddCommand(mailCmd) } @@ -363,3 +389,81 @@ func detectSender() string { // Default to mayor return "mayor/" } + +func runMailCheck(cmd *cobra.Command, args []string) error { + // Find workspace + workDir, err := findBeadsWorkDir() + if err != nil { + if mailCheckInject { + // Inject mode: always exit 0, silent on error + return nil + } + return fmt.Errorf("not in a Gas Town workspace: %w", err) + } + + // Determine which inbox + address := detectSender() + + // Get mailbox + router := mail.NewRouter(workDir) + mailbox, err := router.GetMailbox(address) + if err != nil { + if mailCheckInject { + return nil + } + return fmt.Errorf("getting mailbox: %w", err) + } + + // Count unread + _, unread, err := mailbox.Count() + if err != nil { + if mailCheckInject { + return nil + } + return fmt.Errorf("counting messages: %w", err) + } + + // JSON output + if mailCheckJSON { + result := map[string]interface{}{ + "address": address, + "unread": unread, + "has_new": unread > 0, + } + enc := json.NewEncoder(os.Stdout) + enc.SetIndent("", " ") + return enc.Encode(result) + } + + // Inject mode: output system-reminder if mail exists + if mailCheckInject { + if unread > 0 { + // Get subjects for context + messages, _ := mailbox.ListUnread() + var subjects []string + for _, msg := range messages { + subjects = append(subjects, fmt.Sprintf("- From %s: %s", msg.From, msg.Subject)) + } + + fmt.Println("") + fmt.Printf("You have %d unread message(s) in your inbox.\n\n", unread) + for _, s := range subjects { + fmt.Println(s) + } + fmt.Println() + fmt.Println("Run 'gt mail inbox' to see your messages, or 'gt mail read ' for a specific message.") + fmt.Println("") + } + return nil + } + + // Normal mode + if unread > 0 { + fmt.Printf("%s %d unread message(s)\n", style.Bold.Render("📬"), unread) + os.Exit(0) + } else { + fmt.Println("No new mail") + os.Exit(1) + } + return nil +} diff --git a/internal/cmd/prime.go b/internal/cmd/prime.go index 86929cea..fe358931 100644 --- a/internal/cmd/prime.go +++ b/internal/cmd/prime.go @@ -156,10 +156,15 @@ func outputMayorContext(ctx RoleContext) { fmt.Println("- Monitor overall system health") fmt.Println() fmt.Println("## Key Commands") + fmt.Println("- `gt mail inbox` - Check your messages") + fmt.Println("- `gt mail read ` - Read a specific message") fmt.Println("- `gt status` - Show overall town status") fmt.Println("- `gt rigs` - List all rigs") fmt.Println("- `bd ready` - Issues ready to work") fmt.Println() + fmt.Println("## Startup") + fmt.Println("Check for handoff messages with 🤝 HANDOFF in subject - continue predecessor's work.") + fmt.Println() fmt.Printf("Town root: %s\n", style.Dim.Render(ctx.TownRoot)) }