package cmd import ( "fmt" "os" "regexp" "strings" "github.com/spf13/cobra" "github.com/steveyegge/gastown/internal/beads" "github.com/steveyegge/gastown/internal/mail" "github.com/steveyegge/gastown/internal/style" "github.com/steveyegge/gastown/internal/workspace" ) // runMoleculeAttachFromMail handles the "gt mol attach-from-mail " command. // It reads a mail message, extracts the molecule ID from the body, and attaches // it to the current agent's hook (pinned bead). func runMoleculeAttachFromMail(cmd *cobra.Command, args []string) error { mailID := args[0] // Get current working directory and town root cwd, err := os.Getwd() if err != nil { return fmt.Errorf("getting current directory: %w", err) } townRoot, err := workspace.FindFromCwd() if err != nil || townRoot == "" { return fmt.Errorf("not in a Gas Town workspace") } // Detect agent role and identity using env-aware detection roleInfo, err := GetRoleWithContext(cwd, townRoot) if err != nil { return fmt.Errorf("detecting role: %w", err) } roleCtx := RoleContext{ Role: roleInfo.Role, Rig: roleInfo.Rig, Polecat: roleInfo.Polecat, TownRoot: townRoot, WorkDir: cwd, } agentIdentity := buildAgentIdentity(roleCtx) if agentIdentity == "" { return fmt.Errorf("cannot determine agent identity (role: %s)", roleCtx.Role) } // Get the agent's mailbox mailWorkDir, err := findMailWorkDir() if err != nil { return fmt.Errorf("finding mail workspace: %w", err) } router := mail.NewRouter(mailWorkDir) mailbox, err := router.GetMailbox(agentIdentity) if err != nil { return fmt.Errorf("getting mailbox: %w", err) } // Read the mail message msg, err := mailbox.Get(mailID) if err != nil { return fmt.Errorf("reading mail message: %w", err) } // Extract molecule ID from mail body moleculeID := extractMoleculeIDFromMail(msg.Body) if moleculeID == "" { return fmt.Errorf("no attached_molecule field found in mail body") } // Find local beads directory workDir, err := findLocalBeadsDir() if err != nil { return fmt.Errorf("not in a beads workspace: %w", err) } b := beads.New(workDir) // Find the agent's pinned bead (hook) pinnedBeads, err := b.List(beads.ListOptions{ Status: beads.StatusPinned, Assignee: agentIdentity, Priority: -1, }) if err != nil { return fmt.Errorf("listing pinned beads: %w", err) } if len(pinnedBeads) == 0 { return fmt.Errorf("no pinned bead found for agent %s - create one first", agentIdentity) } // Use the first pinned bead as the hook hookBead := pinnedBeads[0] // Check if molecule exists _, err = b.Show(moleculeID) if err != nil { return fmt.Errorf("molecule %s not found: %w", moleculeID, err) } // Attach the molecule to the hook issue, err := b.AttachMolecule(hookBead.ID, moleculeID) if err != nil { return fmt.Errorf("attaching molecule: %w", err) } // Mark mail as read if err := mailbox.MarkRead(mailID); err != nil { // Non-fatal: log warning but don't fail style.PrintWarning("could not mark mail as read: %v", err) } // Output success attachment := beads.ParseAttachmentFields(issue) fmt.Printf("%s Attached molecule from mail\n", style.Bold.Render("✓")) fmt.Printf(" Mail: %s\n", mailID) fmt.Printf(" Hook: %s\n", hookBead.ID) fmt.Printf(" Molecule: %s\n", moleculeID) if attachment != nil && attachment.AttachedAt != "" { fmt.Printf(" Attached at: %s\n", attachment.AttachedAt) } fmt.Printf("\n%s Run 'gt hook' to see progress\n", style.Dim.Render("Hint:")) return nil } // extractMoleculeIDFromMail extracts a molecule ID from a mail message body. // It looks for patterns like: // - attached_molecule: // - molecule_id: // - molecule: // // The ID is expected to be on the same line after the colon. func extractMoleculeIDFromMail(body string) string { // Try various patterns for molecule ID in mail body (case-insensitive) patterns := []string{ `(?i)attached_molecule:\s*(\S+)`, `(?i)molecule_id:\s*(\S+)`, `(?i)molecule:\s*(\S+)`, `(?i)mol:\s*(\S+)`, } for _, pattern := range patterns { re := regexp.MustCompile(pattern) matches := re.FindStringSubmatch(body) if len(matches) >= 2 { return strings.TrimSpace(matches[1]) } } return "" }