// Assist - Acme interface for AnviLLM package main import ( "anvillm/pkg/logging" "encoding/json" "flag" "fmt" "os" "os/exec" "path/filepath" "regexp" "sort" "strings" "sync" "time" "9fans.net/go/acme" "9fans.net/go/plan9" "9fans.net/go/plan9/client" "go.uber.org/zap" ) const windowName = "/AnviLLM/" // Terminal to use for tmux attach (configurable via environment or flag) var terminalCommand = getTerminalCommand() // shellEscape escapes a string for use inside single quotes in shell commands. func shellEscape(s string) string { return strings.ReplaceAll(s, "'", "'\\''") } func getTerminalCommand() string { if term := os.Getenv("ANVILLM_TERMINAL"); term != "" { return term } return "foot" // default } type SessionInfo struct { ID string Backend string Role string State string Alias string Cwd string Pid int WinID int } var ( fs *client.Fsys // Track window IDs for prompt windows (client-side state) promptWindows = make(map[string]int) // session ID -> window ID promptWindowsMu sync.RWMutex // protects promptWindows map // Compile regex patterns once at startup sessionIDRegex = regexp.MustCompile(`^[a-f0-9]{8}$`) aliasRegex = regexp.MustCompile(`^[A-Za-z0-9_-]+$`) ) func main() { if err := logging.Init(); err != nil { fmt.Fprintf(os.Stderr, "Failed to initialize logging: %v\n", err) os.Exit(1) } defer logging.Logger().Sync() flag.Parse() // Connect to anvillm via 9P, auto-starting if needed var err error fs, err = connectToServer() if err != nil { // Try to start anvillm automatically logging.Logger().Info("anvillm not running, attempting to start") startCmd := exec.Command("anvillm", "start") if err := startCmd.Run(); err != nil { logging.Logger().Error("failed to start anvillm", zap.Error(err)) logging.Logger().Info("continuing without daemon connection") } else { // Wait a moment for daemon to initialize for i := 0; i < 20; i++ { fs, err = connectToServer() if err == nil { break } time.Sleep(100 * time.Millisecond) } if err != nil { logging.Logger().Error("failed to connect to anvillm after starting", zap.Error(err)) logging.Logger().Info("continuing without daemon connection") } } } if fs != nil { defer fs.Close() } w, err := acme.New() if err != nil { logging.Logger().Fatal("failed to create acme window", zap.Error(err)) } defer w.CloseFiles() w.Name(windowName) w.Write("tag", []byte("Get Put Attach Stop Restart Kill Alias Context Daemon Recover Inbox Archive ")) refreshList(w) w.Ctl("clean") // Event loop for e := range w.EventChan() { switch e.C2 { case 'x', 'X': cmd := string(e.Text) arg := strings.TrimSpace(string(e.Arg)) // Handle Put: apply inline session edits from body if cmd == "Put" { body, err := w.ReadAll("body") if err != nil { fmt.Fprintf(os.Stderr, "Error reading body: %v\n", err) continue } edits := parseSessionEdits(string(body)) applySessionEdits(w, edits) continue } // Handle session ID clicks if sessionIDRegex.MatchString(cmd) { if len(e.Arg) > 0 { // Fire-and-forget: B2 on session ID with selected text sends prompt go func(id, prompt string) { if err := sendPrompt(id, prompt); err != nil { fmt.Fprintf(os.Stderr, "Failed to send prompt: %v\n", err) } }(cmd, string(e.Arg)) } else { // Middle-click on session ID without selection attaches to tmux if err := attachSession(cmd); err != nil { fmt.Fprintf(os.Stderr, "Failed to attach: %v\n", err) } } continue } // Parse commands with arguments (e.g., "Kiro /path" -> cmd="Kiro", arg="/path") cmd, arg = parseCommand(cmd, arg) switch cmd { case "Kiro": if arg == "" { fmt.Fprintf(os.Stderr, "Error: Kiro requires a path argument\n") continue } if err := createSession("kiro-cli", arg); err != nil { fmt.Fprintf(os.Stderr, "Error: %v\n", err) continue } refreshList(w) case "Claude": if arg == "" { fmt.Fprintf(os.Stderr, "Error: Claude requires a path argument\n") continue } if err := createSession("claude", arg); err != nil { fmt.Fprintf(os.Stderr, "Error: %v\n", err) continue } refreshList(w) case "Ollie": if err := createSession("ollie", arg); err != nil { fmt.Fprintf(os.Stderr, "Error: %v\n", err) continue } refreshList(w) case "GoQ": if arg == "" { fmt.Fprintf(os.Stderr, "Error: GoQ requires a path argument\n") continue } if err := createSession("goq", arg); err != nil { fmt.Fprintf(os.Stderr, "Error: %v\n", err) continue } refreshList(w) case "Stop": if arg == "" { fmt.Fprintf(os.Stderr, "Usage: Stop \n") continue } if err := controlSession(arg, "stop"); err != nil { fmt.Fprintf(os.Stderr, "Failed to stop session: %v\n", err) continue } refreshList(w) case "Restart": if arg == "" { fmt.Fprintf(os.Stderr, "Usage: Restart \n") continue } if err := controlSession(arg, "restart"); err != nil { fmt.Fprintf(os.Stderr, "Failed to restart session: %v\n", err) continue } refreshList(w) case "Kill": if arg == "" { fmt.Fprintf(os.Stderr, "Usage: Kill \n") continue } if err := controlSession(arg, "kill"); err != nil { fmt.Fprintf(os.Stderr, "Failed to kill session: %v\n", err) continue } refreshList(w) case "Attach": if arg == "" { fmt.Fprintf(os.Stderr, "Usage: Attach \n") continue } if err := attachSession(arg); err != nil { fmt.Fprintf(os.Stderr, "Failed to attach: %v\n", err) } case "Get": refreshList(w) case "Alias": parts := strings.Fields(arg) if len(parts) < 2 { fmt.Fprintf(os.Stderr, "Usage: Alias \n") continue } id := parts[0] alias := parts[1] if !aliasRegex.MatchString(alias) { fmt.Fprintf(os.Stderr, "Invalid alias: must match [A-Za-z0-9_-]+\n") continue } if err := setAlias(id, alias); err != nil { fmt.Fprintf(os.Stderr, "Failed to set alias: %v\n", err) continue } // Rename prompt window if it exists promptWindowsMu.RLock() winID, ok := promptWindows[id] promptWindowsMu.RUnlock() if ok { if aw, err := acme.Open(winID, nil); err == nil { sess, _ := getSession(id) displayName := alias if displayName == "" { displayName = id } aw.Name(filepath.Join(sess.Cwd, fmt.Sprintf("+Prompt.%s", displayName))) aw.CloseFiles() } } refreshList(w) case "Context": if arg == "" { fmt.Fprintf(os.Stderr, "Usage: Context \n") continue } sess, err := getSession(arg) if err != nil { fmt.Fprintf(os.Stderr, "Session not found: %s\n", arg) continue } if err := openContextWindow(sess); err != nil { fmt.Fprintf(os.Stderr, "Error opening context window: %v\n", err) } case "Daemon": if err := openDaemonWindow(); err != nil { fmt.Fprintf(os.Stderr, "Error opening daemon window: %v\n", err) } case "Recover": if err := recoverSessions(w); err != nil { fmt.Fprintf(os.Stderr, "Error recovering sessions: %v\n", err) } case "Inbox": owner := "user" if arg != "" { owner = arg } if err := openInboxWindow(owner); err != nil { fmt.Fprintf(os.Stderr, "Error opening inbox window: %v\n", err) } case "Archive": date := time.Now().Format("20060102") if len(arg) == 8 && isDigits(arg) { date = arg } if err := openArchiveWindow("user", date); err != nil { fmt.Fprintf(os.Stderr, "Error opening archive window: %v\n", err) } default: w.WriteEvent(e) } case 'l', 'L': text := strings.TrimSpace(string(e.Text)) if sessionIDRegex.MatchString(text) { // Try to open/focus prompt window promptWindowsMu.RLock() winID, ok := promptWindows[text] promptWindowsMu.RUnlock() if ok { if aw, err := acme.Open(winID, nil); err == nil { aw.Ctl("show") aw.CloseFiles() } else { // Window died, open new one sess, _ := getSession(text) if sess != nil { openPromptWindow(sess) } } } else { // Open new prompt window sess, _ := getSession(text) if sess != nil { openPromptWindow(sess) } } } else { w.WriteEvent(e) } default: w.WriteEvent(e) } } } // parseCommand extracts command and argument from input text // Handles cases like "Kiro /path" -> ("Kiro", "/path") func parseCommand(cmd, arg string) (string, string) { commandsWithArgs := []string{"Kiro", "Claude", "Ollie", "GoQ", "Stop", "Restart", "Kill", "Alias", "Context"} for _, cmdName := range commandsWithArgs { prefix := cmdName + " " if strings.HasPrefix(cmd, prefix) { return cmdName, strings.TrimPrefix(cmd, prefix) } } return cmd, arg } func connectToServer() (*client.Fsys, error) { ns := client.Namespace() if ns == "" { return nil, fmt.Errorf("no namespace") } // MountService expects just the service name, it adds the namespace automatically return client.MountService("anvillm") } func isConnected() bool { return fs != nil } func createSession(backend, cwd string) error { if !isConnected() { return fmt.Errorf("not connected to anvillm (use Daemon command to start server)") } // Validate and clean the path cleanPath := filepath.Clean(cwd) // Ensure it's an absolute path if !filepath.IsAbs(cleanPath) { var err error cleanPath, err = filepath.Abs(cleanPath) if err != nil { return fmt.Errorf("invalid path: %v", err) } } // Verify the directory exists if info, err := os.Stat(cleanPath); err != nil { return fmt.Errorf("path does not exist: %v", err) } else if !info.IsDir() { return fmt.Errorf("path is not a directory: %s", cleanPath) } fid, err := fs.Open("ctl", plan9.OWRITE) if err != nil { return err } defer fid.Close() cmd := fmt.Sprintf("new %s %s", backend, cleanPath) _, err = fid.Write([]byte(cmd)) return err } func controlSession(id, cmd string) error { if !isConnected() { return fmt.Errorf("not connected to anvillm") } path := filepath.Join(id, "ctl") fid, err := fs.Open(path, plan9.OWRITE) if err != nil { return err } defer fid.Close() _, err = fid.Write([]byte(cmd)) return err } func sendPrompt(id, prompt string) error { if !isConnected() { return fmt.Errorf("not connected to anvillm") } // Create message JSON msg := map[string]interface{}{ "to": id, "type": "PROMPT_REQUEST", "subject": "User prompt", "body": prompt, } msgJSON, err := json.Marshal(msg) if err != nil { return fmt.Errorf("failed to marshal message: %w", err) } // Write to user mail path := "user/mail" fid, err := fs.Open(path, plan9.OWRITE) if err != nil { return fmt.Errorf("failed to open mail file: %w", err) } defer fid.Close() _, err = fid.Write(msgJSON) if err != nil { return fmt.Errorf("failed to write message: %w", err) } return nil } func setAlias(id, alias string) error { if !isConnected() { return fmt.Errorf("not connected to anvillm") } path := filepath.Join(id, "alias") fid, err := fs.Open(path, plan9.OWRITE) if err != nil { return err } defer fid.Close() _, err = fid.Write([]byte(alias)) return err } func getSession(id string) (*SessionInfo, error) { // Read session metadata from 9P files sess := &SessionInfo{ID: id} // Read backend if data, err := readFile(filepath.Join(id, "backend")); err == nil { sess.Backend = strings.TrimSpace(string(data)) } // Read state if data, err := readFile(filepath.Join(id, "state")); err == nil { sess.State = strings.TrimSpace(string(data)) } // Read alias if data, err := readFile(filepath.Join(id, "alias")); err == nil { sess.Alias = strings.TrimSpace(string(data)) } // Read cwd if data, err := readFile(filepath.Join(id, "cwd")); err == nil { sess.Cwd = strings.TrimSpace(string(data)) } // Read pid if data, err := readFile(filepath.Join(id, "pid")); err == nil { fmt.Sscanf(string(data), "%d", &sess.Pid) } return sess, nil } func listSessions() ([]*SessionInfo, error) { if !isConnected() { return nil, fmt.Errorf("not connected to anvillm") } data, err := readFile("list") if err != nil { return nil, err } lines := strings.Split(string(data), "\n") var sessions []*SessionInfo for _, line := range lines { line = strings.TrimSpace(line) if line == "" { continue } // Parse: id backend state alias model cwd fields := strings.Fields(line) if len(fields) < 6 { continue } sess := &SessionInfo{ ID: fields[0], Backend: fields[1], State: fields[2], Alias: fields[3], Role: fields[4], Cwd: strings.Join(fields[5:], " "), } if sess.Alias == "-" { sess.Alias = "" } if sess.Role == "-" { sess.Role = "" } sessions = append(sessions, sess) } return sessions, nil } func readFile(path string) ([]byte, error) { if !isConnected() { return nil, fmt.Errorf("not connected to anvillm") } fid, err := fs.Open(path, plan9.OREAD) if err != nil { return nil, err } defer fid.Close() var buf []byte tmp := make([]byte, 8192) for { n, err := fid.Read(tmp) if n > 0 { buf = append(buf, tmp[:n]...) } if err != nil { break } } return buf, nil } func writeFile(path string, data []byte) error { if !isConnected() { return fmt.Errorf("not connected to anvillm") } fid, err := fs.Open(path, plan9.OWRITE) if err != nil { return err } defer fid.Close() _, err = fid.Write(data) return err } func refreshList(w *acme.Win) { var buf strings.Builder buf.WriteString("Backends: [Kiro] [Claude] [Ollie] [GoQ]\n\n") if !isConnected() { buf.WriteString("Not connected to anvillm daemon.\n") buf.WriteString("Use 'Daemon' command to start the server.\n") w.Addr(",") w.Write("data", []byte(buf.String())) w.Ctl("clean") w.Addr("0") w.Ctl("dot=addr") w.Ctl("show") return } sessions, err := listSessions() if err != nil { fmt.Fprintf(os.Stderr, "Failed to list sessions: %v\n", err) buf.WriteString("Error listing sessions: " + err.Error() + "\n") w.Addr(",") w.Write("data", []byte(buf.String())) w.Ctl("clean") w.Addr("0") w.Ctl("dot=addr") w.Ctl("show") return } buf.WriteString(fmt.Sprintf("%-8s %-10s %-16s %-9s %-16s %s\n", "ID", "Backend", "Role", "State", "Alias", "Cwd")) buf.WriteString(fmt.Sprintf("%-8s %-10s %-16s %-9s %-16s %s\n", "--------", "----------", "----------------", "---------", "----------------", strings.Repeat("-", 40))) for _, sess := range sessions { alias := sess.Alias if alias == "" { alias = "-" } role := sess.Role if role == "" { role = "-" } buf.WriteString(fmt.Sprintf("%-8s %-10s %-16s %-9s %-16s %s\n", sess.ID, sess.Backend, role, sess.State, alias, sess.Cwd)) } w.Addr(",") w.Write("data", []byte(buf.String())) w.Ctl("clean") w.Addr("0") w.Ctl("dot=addr") w.Ctl("show") } func recoverSessions(w *acme.Win) error { if !isConnected() { return fmt.Errorf("not connected to anvillm") } if err := writeFile("ctl", []byte("recover")); err != nil { return err } refreshList(w) return nil } // sessionEdit represents a single inline action parsed from the sessions window body. type sessionEdit struct { action string // "kill", "stop", "alias", "start" id string // session ID (for kill/stop/alias) backend string // backend name (for start) path string // working directory (for start) alias string // alias name (for alias/start) } // parseSessionEdits parses inline action annotations from the sessions window body. // Recognized prefixes (borrowed from the parseEdits pattern in permissions.go): // // - Kill the session with that ID // ~ Stop the session (graceful) // @ Set alias on the session // + [alias] Start a new session (backends: claude, kiro-cli, ollie, goq) func parseSessionEdits(content string) []sessionEdit { var edits []sessionEdit validBackends := map[string]bool{ "claude": true, "kiro-cli": true, "ollie": true, "goq": true, } for _, line := range strings.Split(content, "\n") { line = strings.TrimSpace(line) if len(line) == 0 || strings.HasPrefix(line, "#") { continue } switch { case strings.HasPrefix(line, "- "): parts := strings.Fields(line[2:]) if len(parts) >= 1 && sessionIDRegex.MatchString(parts[0]) { edits = append(edits, sessionEdit{action: "kill", id: parts[0]}) } case strings.HasPrefix(line, "~ "): parts := strings.Fields(line[2:]) if len(parts) >= 1 && sessionIDRegex.MatchString(parts[0]) { edits = append(edits, sessionEdit{action: "stop", id: parts[0]}) } case strings.HasPrefix(line, "@ "): parts := strings.Fields(line[2:]) if len(parts) >= 2 { id := parts[0] alias := parts[1] if sessionIDRegex.MatchString(id) && aliasRegex.MatchString(alias) { edits = append(edits, sessionEdit{action: "alias", id: id, alias: alias}) } } case strings.HasPrefix(line, "+ "): parts := strings.Fields(line[2:]) if len(parts) >= 2 { backend := parts[0] path := parts[1] if validBackends[backend] { alias := "" if len(parts) >= 3 && aliasRegex.MatchString(parts[2]) { alias = parts[2] } edits = append(edits, sessionEdit{action: "start", backend: backend, path: path, alias: alias}) } } } } return edits } // applySessionEdits applies a batch of session edits atomically, then refreshes the list. // For newly started sessions that need an alias, it retries asynchronously until the // session appears, then sets the alias. func applySessionEdits(w *acme.Win, edits []sessionEdit) { if len(edits) == 0 { refreshList(w) return } // Snapshot existing session IDs before any starts so we can identify new ones. existingSessions, _ := listSessions() existingIDs := make(map[string]bool) for _, s := range existingSessions { existingIDs[s.ID] = true } // pendingAliases holds an alias string per start-action that requested one. // "" means no alias wanted for that slot. var pendingAliases []string var errs []string for _, edit := range edits { switch edit.action { case "kill": if err := controlSession(edit.id, "kill"); err != nil { errs = append(errs, fmt.Sprintf("kill %s: %v", edit.id, err)) } case "stop": if err := controlSession(edit.id, "stop"); err != nil { errs = append(errs, fmt.Sprintf("stop %s: %v", edit.id, err)) } case "alias": if err := setAlias(edit.id, edit.alias); err != nil { errs = append(errs, fmt.Sprintf("alias %s %s: %v", edit.id, edit.alias, err)) } case "start": if err := createSession(edit.backend, edit.path); err != nil { errs = append(errs, fmt.Sprintf("start %s: %v", edit.backend, err)) } else { pendingAliases = append(pendingAliases, edit.alias) } } } if len(errs) > 0 { fmt.Fprintf(os.Stderr, "Session edit errors:\n%s\n", strings.Join(errs, "\n")) } // If any start-actions requested aliases, resolve them asynchronously: poll // until the new sessions appear (up to ~10 s), assign aliases in FIFO order, // then do a final list refresh. if len(pendingAliases) > 0 { go func() { assigned := make([]bool, len(pendingAliases)) for attempt := 0; attempt < 20; attempt++ { time.Sleep(500 * time.Millisecond) sessions, err := listSessions() if err != nil { continue } // Collect new session IDs in appearance order. var newSessions []*SessionInfo for _, s := range sessions { if !existingIDs[s.ID] { newSessions = append(newSessions, s) } } // Match pending aliases to new sessions in FIFO order. newIdx := 0 for i, alias := range pendingAliases { if assigned[i] { newIdx++ // skip slots already resolved continue } if alias == "" { assigned[i] = true continue } if newIdx < len(newSessions) { if setAlias(newSessions[newIdx].ID, alias) == nil { assigned[i] = true } newIdx++ } } // Check if all slots are resolved. allDone := true for _, done := range assigned { if !done { allDone = false break } } if allDone { break } } refreshList(w) }() } refreshList(w) } func openPromptWindow(sess *SessionInfo) (*acme.Win, error) { displayName := sess.Alias if displayName == "" { displayName = sess.ID } name := filepath.Join(sess.Cwd, fmt.Sprintf("+Prompt.%s", displayName)) w, err := acme.New() if err != nil { return nil, err } w.Name(name) w.Write("tag", []byte("Send Compact Clear Resume ")) w.Ctl("clean") // Track window ID client-side promptWindowsMu.Lock() promptWindows[sess.ID] = w.ID() promptWindowsMu.Unlock() go handlePromptWindow(w, sess) return w, nil } func handlePromptWindow(w *acme.Win, sess *SessionInfo) { defer w.CloseFiles() defer func() { promptWindowsMu.Lock() delete(promptWindows, sess.ID) promptWindowsMu.Unlock() }() for e := range w.EventChan() { cmd := string(e.Text) if e.C2 == 'x' || e.C2 == 'X' { switch cmd { case "Send": body, err := w.ReadAll("body") if err != nil { continue } prompt := strings.TrimSpace(string(body)) if prompt != "" { if err := sendPrompt(sess.ID, prompt); err != nil { fmt.Fprintf(os.Stderr, "Failed to send: %v\n", err) continue } w.Ctl("delete") return } case "Compact": if err := controlSession(sess.ID, "compact"); err != nil { fmt.Fprintf(os.Stderr, "Failed to compact: %v\n", err) } case "Clear": if err := controlSession(sess.ID, "clear"); err != nil { fmt.Fprintf(os.Stderr, "Failed to clear: %v\n", err) } case "Resume": if err := controlSession(sess.ID, "resume"); err != nil { fmt.Fprintf(os.Stderr, "Failed to resume: %v\n", err) } default: w.WriteEvent(e) } } else { w.WriteEvent(e) } } } func openContextWindow(sess *SessionInfo) error { w, err := acme.New() if err != nil { return err } w.Name(fmt.Sprintf("/AnviLLM/%s/context", sess.ID)) w.Write("tag", []byte("Put ")) // Load existing context if data, err := readFile(filepath.Join(sess.ID, "context")); err == nil { w.Write("body", data) } w.Ctl("clean") go handleContextWindow(w, sess) return nil } func handleContextWindow(w *acme.Win, sess *SessionInfo) { defer w.CloseFiles() for e := range w.EventChan() { if e.C2 == 'x' || e.C2 == 'X' { if string(e.Text) == "Put" { body, err := w.ReadAll("body") if err != nil { fmt.Fprintf(os.Stderr, "Error reading body: %v\n", err) continue } path := filepath.Join(sess.ID, "context") if err := writeFile(path, body); err != nil { fmt.Fprintf(os.Stderr, "Error writing context: %v\n", err) } else { w.Ctl("clean") logging.Logger().Info("context updated", zap.String("session", sess.ID)) fmt.Printf("Context updated for session %s\n", sess.ID) } continue } } w.WriteEvent(e) } } func attachSession(id string) error { // Read tmux session/window from the tmux file tmuxPath := filepath.Join(id, "tmux") data, err := readFile(tmuxPath) if err != nil { return fmt.Errorf("failed to read tmux target: %w", err) } target := strings.TrimSpace(string(data)) if target == "" { return fmt.Errorf("session does not support attach") } // Check if there's already a tmux client running clientsCmd := exec.Command("tmux", "list-clients") clientsOutput, err := clientsCmd.Output() if err == nil && len(clientsOutput) > 0 { // There's an existing tmux client, switch to the target session/window go func() { cmd := exec.Command("tmux", "switch-client", "-t", target) if err := cmd.Run(); err != nil { fmt.Fprintf(os.Stderr, "Failed to switch tmux client: %v\n", err) } }() } else { // No existing client, launch new terminal go func() { cmd := exec.Command(terminalCommand, "-e", "tmux", "attach", "-t", target) if err := cmd.Start(); err != nil { fmt.Fprintf(os.Stderr, "Failed to launch terminal: %v\n", err) } }() } return nil } // openDaemonWindow opens the daemon management window func openDaemonWindow() error { w, err := acme.New() if err != nil { return err } w.Name("/AnviLLM/Daemon") w.Write("tag", []byte("Get Stop Start Restart ")) go handleDaemonWindow(w) return nil } func handleDaemonWindow(w *acme.Win) { defer w.CloseFiles() // Load and display current status refreshDaemonWindow(w) for e := range w.EventChan() { switch e.C2 { case 'x', 'X': cmd := string(e.Text) switch cmd { case "Get": refreshDaemonWindow(w) case "Stop": stopDaemon(w) time.Sleep(500 * time.Millisecond) refreshDaemonWindow(w) case "Start": startDaemon(w) time.Sleep(1 * time.Second) refreshDaemonWindow(w) case "Restart": stopDaemon(w) time.Sleep(500 * time.Millisecond) startDaemon(w) time.Sleep(1 * time.Second) refreshDaemonWindow(w) default: w.WriteEvent(e) } default: w.WriteEvent(e) } } } func refreshDaemonWindow(w *acme.Win) { var buf strings.Builder buf.WriteString("AnviLLM Daemon Status\n") buf.WriteString(strings.Repeat("=", 60) + "\n\n") // Check daemon status statusCmd := exec.Command("anvillm", "status") output, err := statusCmd.CombinedOutput() if err != nil { // Not running or error buf.WriteString("Status: NOT RUNNING\n") if len(output) > 0 { buf.WriteString(string(output)) } buf.WriteString("\n") } else { // Running buf.WriteString("Status: RUNNING\n") buf.WriteString(string(output)) buf.WriteString("\n") } // Check socket ns := client.Namespace() if ns != "" { sockPath := filepath.Join(ns, "anvillm") if _, err := os.Stat(sockPath); err == nil { buf.WriteString("9P Socket: " + sockPath + " (exists)\n") } else { buf.WriteString("9P Socket: " + sockPath + " (missing)\n") } } // Check connection if fs != nil { buf.WriteString("Connection: CONNECTED\n") } else { buf.WriteString("Connection: DISCONNECTED\n") } buf.WriteString("\n") buf.WriteString(strings.Repeat("-", 60) + "\n") buf.WriteString("Commands:\n") buf.WriteString(" Start - Start the daemon (anvillm start)\n") buf.WriteString(" Stop - Stop the daemon (anvillm stop)\n") buf.WriteString(" Restart - Restart the daemon\n") buf.WriteString(" Get - Refresh this window\n\n") buf.WriteString("Note: Use 'anvillm fgstart' in terminal for debug logs.\n") w.Addr(",") w.Write("data", []byte(buf.String())) w.Ctl("clean") w.Addr("0") w.Ctl("dot=addr") w.Ctl("show") } func startDaemon(w *acme.Win) { w.Addr("$") w.Write("data", []byte("\nStarting daemon...\n")) cmd := exec.Command("anvillm", "start") output, err := cmd.CombinedOutput() if err != nil { w.Write("data", []byte(fmt.Sprintf("Error: %v\n%s\n", err, output))) } else { w.Write("data", []byte("Daemon started successfully\n")) if len(output) > 0 { w.Write("data", []byte(string(output)+"\n")) } // Try to reconnect time.Sleep(500 * time.Millisecond) if newFs, err := connectToServer(); err == nil { if fs != nil { fs.Close() } fs = newFs w.Write("data", []byte("Reconnected to daemon\n")) } } } func stopDaemon(w *acme.Win) { w.Addr("$") w.Write("data", []byte("\nStopping daemon...\n")) cmd := exec.Command("anvillm", "stop") output, err := cmd.CombinedOutput() if err != nil { w.Write("data", []byte(fmt.Sprintf("Error: %v\n%s\n", err, output))) } else { w.Write("data", []byte("Daemon stopped\n")) if len(output) > 0 { w.Write("data", []byte(string(output)+"\n")) } // Disconnect if fs != nil { fs.Close() fs = nil } } } type Message struct { ID string `json:"id"` From string `json:"from"` To string `json:"to"` Type string `json:"type"` Subject string `json:"subject"` Body string `json:"body"` Timestamp int64 `json:"timestamp"` } func openInboxWindow(owner string) error { w, err := acme.New() if err != nil { return err } if owner == "user" { w.Name("/AnviLLM/inbox") } else { w.Name(fmt.Sprintf("/AnviLLM/%s/inbox", owner)) } w.Write("tag", []byte("Get Put ")) go handleInboxWindow(w, owner) return nil } func handleInboxWindow(w *acme.Win, owner string) { defer w.CloseFiles() folder := owner + "/inbox" refreshMailboxWindow(w, folder, "Inbox") for e := range w.EventChan() { switch e.C2 { case 'x', 'X': switch string(e.Text) { case "Get": refreshMailboxWindow(w, folder, "Inbox") case "Put": messages, _, err := listMessages(folder) if err != nil { fmt.Fprintf(os.Stderr, "Error listing messages: %v\n", err) continue } body, err := w.ReadAll("body") if err != nil { fmt.Fprintf(os.Stderr, "Error reading body: %v\n", err) continue } edits := parseMailEdits(string(body), messages) var errs []string for _, edit := range edits { if err := deleteInboxMessage(edit.msgID); err != nil { errs = append(errs, fmt.Sprintf("delete %s: %v", edit.msgID, err)) } } if len(errs) > 0 { fmt.Fprintf(os.Stderr, "Put errors: %s\n", strings.Join(errs, "; ")) } refreshMailboxWindow(w, folder, "Inbox") default: w.WriteEvent(e) } case 'l', 'L': text := strings.TrimSpace(string(e.Text)) if isHexString(text) { openMessageWindowByPrefix(text, folder) } else { w.WriteEvent(e) } default: w.WriteEvent(e) } } } func refreshMailboxWindow(w *acme.Win, folder, title string) { messages, _, err := listMessages(folder) if err != nil { w.Addr(",") w.Write("data", []byte(fmt.Sprintf("Error reading %s: %v\n", title, err))) w.Ctl("clean") return } // Sort by timestamp, newest first sort.Slice(messages, func(i, j int) bool { return messages[i].Timestamp > messages[j].Timestamp }) var buf strings.Builder buf.WriteString(fmt.Sprintf("%s %s\n", folder, title)) buf.WriteString(strings.Repeat("=", 120) + "\n\n") buf.WriteString(fmt.Sprintf("%-10s %-20s %-12s %-1s %-18s %s\n", "ID", "Date", "From", "!", "Type", "Subject")) buf.WriteString(fmt.Sprintf("%-10s %-20s %-12s %-1s %-18s %s\n", "----------", "--------------------", "------------", "-", "------------------", strings.Repeat("-", 40))) for _, msg := range messages { from := msg.From if from == "" { from = "-" } shortID := shortUUID(msg.ID) dateStr := formatTimestamp(msg.Timestamp) // Mark messages that require user approval or review action flag := " " if msg.Type == "APPROVAL_REQUEST" || msg.Type == "REVIEW_REQUEST" { flag = "!" } buf.WriteString(fmt.Sprintf("%-10s %-20s %-12s %-1s %-18s %s\n", shortID, dateStr, from, flag, msg.Type, msg.Subject)) } w.Addr(",") w.Write("data", []byte(buf.String())) w.Ctl("clean") w.Addr("0") w.Ctl("dot=addr") w.Ctl("show") } func shortUUID(id string) string { if idx := strings.Index(id, "-"); idx > 0 { return id[:idx] } return id } func expandUUID(shortID string, messages []Message) string { for _, msg := range messages { if strings.HasPrefix(msg.ID, shortID) { return msg.ID } } return shortID } func isDigits(s string) bool { for _, c := range s { if c < '0' || c > '9' { return false } } return len(s) > 0 } func extractArchiveDate(tag string) string { // tag format: "/AnviLLM/archive/20260318 Del Snarf ... | user tagline" name := strings.Fields(tag)[0] if strings.HasPrefix(name, "/AnviLLM/archive/") { d := strings.TrimPrefix(name, "/AnviLLM/archive/") if isDigits(d) { return d } } return "" } func openArchiveWindow(owner, date string) error { w, err := acme.New() if err != nil { return err } if date != "" { w.Name(fmt.Sprintf("/AnviLLM/archive/%s", date)) } else if owner == "user" { w.Name("/AnviLLM/archive") } else { w.Name(fmt.Sprintf("/AnviLLM/%s/archive", owner)) } w.Write("tag", []byte("Get Search ")) go handleArchiveWindow(w, owner, date) return nil } func handleArchiveWindow(w *acme.Win, owner, date string) { defer w.CloseFiles() var messages []Message var loadErr error if date != "" { messages, loadErr = loadMailHistory(owner, date) } else { messages, _, loadErr = listMessages(owner + "/completed") } refreshArchiveWindowWithMessages(w, messages, owner, date, loadErr) for e := range w.EventChan() { switch e.C2 { case 'x', 'X': cmd := string(e.Text) arg := strings.TrimSpace(string(e.Arg)) switch cmd { case "Get": // Re-read date from window name tag, _ := w.ReadAll("tag") date = extractArchiveDate(string(tag)) messages = nil if date != "" { messages, loadErr = loadMailHistory(owner, date) } else { messages, _, loadErr = listMessages(owner + "/completed") } refreshArchiveWindowWithMessages(w, messages, owner, date, loadErr) case "Search": if arg == "" { w.WriteEvent(e) continue } out, err := searchMail("user", arg, "") if err != nil { w.Addr(",") w.Write("data", []byte(fmt.Sprintf("Search error: %v\n", err))) w.Ctl("clean") continue } var searchResults []Message for _, line := range strings.Split(string(out), "\n") { line = strings.TrimSpace(line) if line == "" { continue } var entry struct { Ts int64 `json:"ts"` Data struct { ID string `json:"id"` From string `json:"from"` To string `json:"to"` Type string `json:"type"` Subject string `json:"subject"` Body string `json:"body"` } `json:"data"` } if err := json.Unmarshal([]byte(line), &entry); err != nil { continue } searchResults = append(searchResults, Message{ ID: entry.Data.ID, From: entry.Data.From, To: entry.Data.To, Type: entry.Data.Type, Subject: entry.Data.Subject, Body: entry.Data.Body, Timestamp: entry.Ts, }) } messages = searchResults refreshArchiveWindowWithMessages(w, messages, owner, "", nil) default: w.WriteEvent(e) } case 'l', 'L': text := strings.TrimSpace(string(e.Text)) if isHexString(text) { openArchiveMessageByPrefix(text, messages) } else { w.WriteEvent(e) } default: w.WriteEvent(e) } } } func loadMailHistory(owner, date string) ([]Message, error) { agentID := owner if agentID == "" { agentID = "user" } mailDir := filepath.Join(os.Getenv("HOME"), ".local/share/anvillm/mail", agentID) sentFile := filepath.Join(mailDir, date+"-sent.jsonl") recvFile := filepath.Join(mailDir, date+"-recv.jsonl") var messages []Message for _, f := range []string{sentFile, recvFile} { data, err := os.ReadFile(f) if err != nil { continue } for _, line := range strings.Split(string(data), "\n") { line = strings.TrimSpace(line) if line == "" { continue } var entry struct { Ts int64 `json:"ts"` Data struct { ID string `json:"id"` From string `json:"from"` To string `json:"to"` Type string `json:"type"` Subject string `json:"subject"` Body string `json:"body"` } `json:"data"` } if err := json.Unmarshal([]byte(line), &entry); err != nil { continue } messages = append(messages, Message{ ID: entry.Data.ID, From: entry.Data.From, To: entry.Data.To, Type: entry.Data.Type, Subject: entry.Data.Subject, Body: entry.Data.Body, Timestamp: entry.Ts, }) } } return messages, nil } func refreshArchiveWindowWithMessages(w *acme.Win, messages []Message, owner, date string, loadErr error) { if loadErr != nil { w.Addr(",") w.Write("data", []byte(fmt.Sprintf("Error reading archive: %v\n", loadErr))) w.Ctl("clean") return } sort.Slice(messages, func(i, j int) bool { return messages[i].Timestamp > messages[j].Timestamp }) var buf strings.Builder if date != "" { buf.WriteString(fmt.Sprintf("Archive %s\n", date)) } else { buf.WriteString(fmt.Sprintf("%s/completed Archive\n", owner)) } buf.WriteString(strings.Repeat("=", 120) + "\n\n") buf.WriteString(fmt.Sprintf("%-10s %-20s %-12s %-12s %-18s %s\n", "ID", "Date", "From", "To", "Type", "Subject")) buf.WriteString(fmt.Sprintf("%-10s %-20s %-12s %-12s %-18s %s\n", "----------", "--------------------", "------------", "------------", "------------------", strings.Repeat("-", 40))) for _, msg := range messages { from := msg.From if from == "" { from = "-" } to := msg.To if to == "" { to = "-" } shortID := shortUUID(msg.ID) dateStr := formatTimestamp(msg.Timestamp) buf.WriteString(fmt.Sprintf("%-10s %-20s %-12s %-12s %-18s %s\n", shortID, dateStr, from, to, msg.Type, msg.Subject)) } w.Addr(",") w.Write("data", []byte(buf.String())) w.Ctl("clean") w.Addr("0") w.Ctl("dot=addr") w.Ctl("show") } func openArchiveMessageByPrefix(prefix string, messages []Message) error { for _, m := range messages { if strings.HasPrefix(m.ID, prefix) { return openArchiveMessageWindow(&m) } } return fmt.Errorf("message not found: %s", prefix) } func openArchiveMessageWindow(msg *Message) error { w, err := acme.New() if err != nil { return err } w.Name(fmt.Sprintf("/AnviLLM/archive/%s", shortUUID(msg.ID))) var buf strings.Builder buf.WriteString(fmt.Sprintf("From: %s\n", msg.From)) buf.WriteString(fmt.Sprintf("To: %s\n", msg.To)) buf.WriteString(fmt.Sprintf("Type: %s\n", msg.Type)) buf.WriteString(fmt.Sprintf("Subject: %s\n", msg.Subject)) buf.WriteString(fmt.Sprintf("Date: %s\n", formatTimestamp(msg.Timestamp))) buf.WriteString("\n") buf.WriteString(msg.Body) w.Write("body", []byte(buf.String())) w.Ctl("clean") return nil } func listInboxMessages() ([]Message, []string, error) { return listMessages("user/inbox") } func listArchiveMessages() ([]Message, []string, error) { return listMessages("user/completed") } func listMessages(folder string) ([]Message, []string, error) { if !isConnected() { return nil, nil, fmt.Errorf("not connected to anvillm") } fid, err := fs.Open(folder, plan9.OREAD) if err != nil { return nil, nil, err } defer fid.Close() var filenames []string for { dirs, err := fid.Dirread() if err != nil || len(dirs) == 0 { break } for _, d := range dirs { if strings.HasSuffix(d.Name, ".json") { filenames = append(filenames, d.Name) } } } var messages []Message for _, filename := range filenames { data, err := readFile(filepath.Join(folder, filename)) if err != nil { continue } var msg Message if err := json.Unmarshal(data, &msg); err != nil { continue } messages = append(messages, msg) } return messages, filenames, nil } func extractTimestamp(filename string) int64 { var ts int64 parts := strings.Split(strings.TrimSuffix(filename, ".json"), "-") if len(parts) == 2 { fmt.Sscanf(parts[1], "%d", &ts) } return ts } func formatTimestamp(ts int64) string { t := time.Unix(ts, 0) return t.Format("02-Jan-2006 15:04:05") } func parseIndex(s string) int { var idx int fmt.Sscanf(s, "%d", &idx) return idx } func isUUID(s string) bool { // Simple UUID check: 36 chars with hyphens at positions 8, 13, 18, 23 if len(s) != 36 { return false } for i, c := range s { if i == 8 || i == 13 || i == 18 || i == 23 { if c != '-' { return false } } else if !((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')) { return false } } return true } func isHexString(s string) bool { if len(s) < 4 || len(s) > 36 { return false } for _, c := range s { if !((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')) { return false } } return true } func openMessageWindowByPrefix(prefix, folder string) error { messages, _, err := listMessages(folder) if err != nil { return err } for _, m := range messages { if strings.HasPrefix(m.ID, prefix) { return openMessageWindowByID(m.ID, folder) } } return fmt.Errorf("message not found: %s", prefix) } func openMessageWindowByID(msgID, folder string) error { messages, filenames, err := listMessages(folder) if err != nil { return err } var msg *Message var filename string for i, m := range messages { if m.ID == msgID { msg = &messages[i] filename = filenames[i] break } } if msg == nil { return fmt.Errorf("message not found: %s", msgID) } w, err := acme.New() if err != nil { return err } windowPath := "inbox" if folder == "user/completed" { windowPath = "archive" } w.Name(fmt.Sprintf("/AnviLLM/%s/%s", windowPath, filename)) // Add Approve/Reject buttons for messages requiring user action tagStr := "Reply Archive " if msg.Type == "APPROVAL_REQUEST" || msg.Type == "REVIEW_REQUEST" { tagStr = "Approve Reject Reply Archive " } w.Write("tag", []byte(tagStr)) var buf strings.Builder buf.WriteString(fmt.Sprintf("From: %s\n", msg.From)) buf.WriteString(fmt.Sprintf("To: %s\n", msg.To)) buf.WriteString(fmt.Sprintf("Type: %s\n", msg.Type)) buf.WriteString(fmt.Sprintf("Subject: %s\n", msg.Subject)) buf.WriteString(fmt.Sprintf("Date: %s\n", formatTimestamp(extractTimestamp(filename)))) buf.WriteString("\n") buf.WriteString(msg.Body) w.Write("body", []byte(buf.String())) w.Ctl("clean") go handleMessageWindow(w, msg, filename) return nil } func openMessageWindow(idx int) error { messages, filenames, err := listInboxMessages() if err != nil { return err } if idx < 1 || idx > len(messages) { return fmt.Errorf("invalid index: %d", idx) } filename := filenames[idx-1] msg := messages[idx-1] w, err := acme.New() if err != nil { return err } w.Name(fmt.Sprintf("/AnviLLM/inbox/%s", filename)) w.Write("tag", []byte("Reply Archive ")) var buf strings.Builder buf.WriteString(fmt.Sprintf("From: %s\n", msg.From)) buf.WriteString(fmt.Sprintf("To: %s\n", msg.To)) buf.WriteString(fmt.Sprintf("Type: %s\n", msg.Type)) buf.WriteString(fmt.Sprintf("Subject: %s\n", msg.Subject)) buf.WriteString(fmt.Sprintf("Date: %s\n", formatTimestamp(extractTimestamp(filename)))) buf.WriteString("\n") buf.WriteString(msg.Body) w.Write("body", []byte(buf.String())) w.Ctl("clean") go handleMessageWindow(w, &msg, filename) return nil } func handleMessageWindow(w *acme.Win, msg *Message, filename string) { defer w.CloseFiles() for e := range w.EventChan() { cmd := string(e.Text) if e.C2 == 'x' || e.C2 == 'X' { switch cmd { case "Reply": openReplyWindow(msg) case "Approve": openApproveWindow(msg) case "Reject": openRejectWindow(msg) case "Archive": if err := archiveMessage(msg.ID); err != nil { w.Fprintf("errors", "Archive failed: %v\n", err) } else { refreshInboxWindowByName() w.Ctl("delete") } default: w.WriteEvent(e) } } else { w.WriteEvent(e) } } } func openReplyWindow(originalMsg *Message) error { w, err := acme.New() if err != nil { return err } replyType := getReplyType(originalMsg.Type) replySubject := fmt.Sprintf("Re: %s", originalMsg.Subject) w.Name(fmt.Sprintf("/AnviLLM/reply/%s", originalMsg.From)) w.Write("tag", []byte("Send ")) w.Ctl("clean") // Pass "" for originalMsgID — plain replies do not auto-archive go handleReplyWindow(w, originalMsg.From, replyType, replySubject, "") return nil } // openApproveWindow opens a reply window pre-filled with "Approved." for one-click approval. // On Send the original message is automatically archived. func openApproveWindow(originalMsg *Message) error { w, err := acme.New() if err != nil { return err } replyType := getReplyType(originalMsg.Type) replySubject := fmt.Sprintf("Re: %s", originalMsg.Subject) w.Name(fmt.Sprintf("/AnviLLM/reply/%s", originalMsg.From)) w.Write("tag", []byte("Send ")) w.Write("body", []byte("Approved.")) w.Ctl("clean") go handleReplyWindow(w, originalMsg.From, replyType, replySubject, originalMsg.ID) return nil } // openRejectWindow opens a reply window pre-filled with a rejection template, // prompting the user to supply a reason before sending. // On Send the original message is automatically archived. func openRejectWindow(originalMsg *Message) error { w, err := acme.New() if err != nil { return err } replyType := getReplyType(originalMsg.Type) replySubject := fmt.Sprintf("Re: %s", originalMsg.Subject) w.Name(fmt.Sprintf("/AnviLLM/reply/%s", originalMsg.From)) w.Write("tag", []byte("Send ")) w.Write("body", []byte("Rejected.\n\nReason: ")) w.Ctl("clean") go handleReplyWindow(w, originalMsg.From, replyType, replySubject, originalMsg.ID) return nil } func refreshInboxWindowByName() { wins, err := acme.Windows() if err != nil { return } for _, info := range wins { if info.Name == "/AnviLLM/inbox" { w, err := acme.Open(info.ID, nil) if err != nil { continue } refreshMailboxWindow(w, "user/inbox", "Inbox") w.CloseFiles() return } } } func archiveMessage(msgID string) error { if !isConnected() { return fmt.Errorf("not connected to anvillm") } fid, err := fs.Open("user/ctl", plan9.OWRITE) if err != nil { return err } defer fid.Close() ctlMsg := fmt.Sprintf("complete %s", msgID) _, err = fid.Write([]byte(ctlMsg)) return err } // deleteArchivedMessage permanently removes a message from the completed/archive folder. func deleteArchivedMessage(msgID string) error { if !isConnected() { return fmt.Errorf("not connected to anvillm") } fid, err := fs.Open("user/ctl", plan9.OWRITE) if err != nil { return err } defer fid.Close() ctlMsg := fmt.Sprintf("delete %s", msgID) _, err = fid.Write([]byte(ctlMsg)) return err } // deleteInboxMessage removes a message from the inbox without archiving. func deleteInboxMessage(msgID string) error { if !isConnected() { return fmt.Errorf("not connected to anvillm") } fid, err := fs.Open("user/ctl", plan9.OWRITE) if err != nil { return err } defer fid.Close() ctlMsg := fmt.Sprintf("delete %s", msgID) _, err = fid.Write([]byte(ctlMsg)) return err } // mailEdit represents a single inline archive/delete action from a mailbox window body. type mailEdit struct { msgID string // full message ID (expanded from short-ID prefix) } // parseMailEdits parses inline action annotations from a mailbox window body. // Lines starting with "- " mark messages for bulk action (archive or delete). func parseMailEdits(content string, messages []Message) []mailEdit { var edits []mailEdit for _, line := range strings.Split(content, "\n") { line = strings.TrimSpace(line) if !strings.HasPrefix(line, "- ") { continue } parts := strings.Fields(line[2:]) if len(parts) >= 1 && isHexString(parts[0]) { fullID := expandUUID(parts[0], messages) edits = append(edits, mailEdit{msgID: fullID}) } } return edits } func getReplyType(msgType string) string { switch msgType { case "QUERY_REQUEST": return "QUERY_RESPONSE" case "REVIEW_REQUEST": return "REVIEW_RESPONSE" case "APPROVAL_REQUEST": return "APPROVAL_RESPONSE" default: return "PROMPT_REQUEST" } } func handleReplyWindow(w *acme.Win, to, msgType, subject, originalMsgID string) { defer w.CloseFiles() for e := range w.EventChan() { if (e.C2 == 'x' || e.C2 == 'X') && string(e.Text) == "Send" { body, err := w.ReadAll("body") if err != nil { continue } prompt := strings.TrimSpace(string(body)) if prompt != "" { if err := sendReply(to, msgType, subject, prompt); err != nil { fmt.Fprintf(os.Stderr, "Failed to send reply: %v\n", err) continue } // Auto-archive the original message when replying from approve/reject if originalMsgID != "" { if err := archiveMessage(originalMsgID); err != nil { fmt.Fprintf(os.Stderr, "Failed to archive original message: %v\n", err) } else { refreshInboxWindowByName() } } w.Ctl("delete") return } } else { w.WriteEvent(e) } } } func sendReply(to, msgType, subject, body string) error { if !isConnected() { return fmt.Errorf("not connected to anvillm") } msg := map[string]interface{}{ "to": to, "type": msgType, "subject": subject, "body": body, } msgJSON, err := json.Marshal(msg) if err != nil { return fmt.Errorf("failed to marshal message: %w", err) } path := "user/mail" fid, err := fs.Open(path, plan9.OWRITE) if err != nil { return fmt.Errorf("failed to open mail file: %w", err) } defer fid.Close() if _, err := fid.Write(msgJSON); err != nil { return fmt.Errorf("failed to write message: %w", err) } return nil } func searchMail(agentID, pattern, date string) ([]byte, error) { homeDir, _ := os.UserHomeDir() mailDir := filepath.Join(homeDir, ".local/share/anvillm/mail", agentID) var files []string if date != "" { sent := filepath.Join(mailDir, date+"-sent.jsonl") recv := filepath.Join(mailDir, date+"-recv.jsonl") if _, err := os.Stat(sent); err == nil { files = append(files, sent) } if _, err := os.Stat(recv); err == nil { files = append(files, recv) } } else { entries, _ := os.ReadDir(mailDir) for _, e := range entries { if strings.HasSuffix(e.Name(), "-sent.jsonl") || strings.HasSuffix(e.Name(), "-recv.jsonl") { files = append(files, filepath.Join(mailDir, e.Name())) } } } re, err := regexp.Compile(pattern) if err != nil { return nil, err } var results []byte for _, f := range files { data, err := os.ReadFile(f) if err != nil { continue } for _, line := range strings.Split(string(data), "\n") { if re.MatchString(line) { results = append(results, line...) results = append(results, '\n') } } } return results, nil }