diff --git a/acme-q b/acme-q index 1aeb30a..40f7b6a 100755 Binary files a/acme-q and b/acme-q differ diff --git a/anvillm b/anvillm new file mode 100755 index 0000000..b91a6fd Binary files /dev/null and b/anvillm differ diff --git a/go.mod b/go.mod index 2e474ef..8258dc8 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module acme-q +module anvillm go 1.21 diff --git a/internal/backend/pty/pty.go b/internal/backend/pty/pty.go index 6e4fed5..3262582 100644 --- a/internal/backend/pty/pty.go +++ b/internal/backend/pty/pty.go @@ -2,7 +2,8 @@ package pty import ( - "acme-q/internal/backend" + "anvillm/internal/debug" + "anvillm/internal/backend" "bytes" "context" "fmt" @@ -133,7 +134,7 @@ func (b *Backend) CreateSession(ctx context.Context, cwd string) (backend.Sessio sess.state = "idle" sess.output.Reset() - log.Printf("[session %s] ready (pid=%d)", sess.ID(), sess.pid) + debug.Log("[session %s] ready (pid=%d)", sess.ID(), sess.pid) return sess, nil } @@ -223,7 +224,7 @@ func (s *Session) reader() { n, err := s.ptmx.Read(buf) if err != nil { if err != io.EOF { - log.Printf("[session %s] reader error: %v", s.id, err) + debug.Log("[session %s] reader error: %v", s.id, err) } close(s.dataCh) // Reap the process @@ -313,7 +314,7 @@ func (s *Session) Send(ctx context.Context, prompt string) (string, error) { s.state = "running" s.output.Reset() - log.Printf("[session %s] sending: %q", s.id, prompt) + debug.Log("[session %s] sending: %q", s.id, prompt) if _, err := s.ptmx.Write([]byte(prompt + "\r")); err != nil { s.state = "idle" return "", fmt.Errorf("write failed: %w", err) @@ -329,7 +330,7 @@ func (s *Session) Send(ctx context.Context, prompt string) (string, error) { // Clean output result := s.cleaner.Clean(prompt, s.output.String()) - log.Printf("[session %s] response: %d bytes", s.id, len(result)) + debug.Log("[session %s] response: %d bytes", s.id, len(result)) return result, nil } @@ -346,7 +347,6 @@ func (s *Session) waitForComplete(ctx context.Context, prompt string) error { return fmt.Errorf("session closed") } s.output.Write(data) - log.Printf("[session %s] +%d bytes, total %d", s.id, len(data), s.output.Len()) if s.detector.IsComplete(prompt, s.output.String()) { // Drain any remaining data @@ -357,7 +357,7 @@ func (s *Session) waitForComplete(ctx context.Context, prompt string) error { s.output.Write(extra) } case <-time.After(10 * time.Millisecond): - log.Printf("[session %s] response complete, %d bytes", s.id, s.output.Len()) + debug.Log("[session %s] response complete, %d bytes", s.id, s.output.Len()) return nil } } diff --git a/internal/backend/tmux/session.go b/internal/backend/tmux/session.go index 7d3c03f..fdff430 100644 --- a/internal/backend/tmux/session.go +++ b/internal/backend/tmux/session.go @@ -1,12 +1,12 @@ package tmux import ( - "acme-q/internal/backend" + "anvillm/internal/backend" + "anvillm/internal/debug" "bytes" "context" "fmt" "io" - "log" "os" "strings" "sync" @@ -110,7 +110,7 @@ func (s *Session) reader() { if err != nil { // Only log unexpected errors (not EOF or closed file during shutdown) if err != io.EOF && !strings.Contains(err.Error(), "file already closed") { - log.Printf("[session %s] reader error: %v", s.id, err) + debug.Log("[session %s] reader error: %v", s.id, err) } close(s.dataCh) s.mu.Lock() @@ -141,19 +141,17 @@ func (s *Session) waitForReady(ctx context.Context, timeout time.Duration) error } s.output.Write(data) - log.Printf("[session %s] startup: %d bytes accumulated", s.id, s.output.Len()) - // If we have a startup handler and it hasn't completed, call it if !startupDone && s.startupHandler != nil { keys, done := s.startupHandler.HandleStartup(s.output.String()) if keys != "" { - log.Printf("[session %s] startup handler sending keys: %q", s.id, keys) + debug.Log("[session %s] startup handler sending keys: %q", s.id, keys) if err := sendKeys(s.target(), keys); err != nil { return fmt.Errorf("failed to send startup response: %w", err) } } if done { - log.Printf("[session %s] startup handler complete", s.id) + debug.Log("[session %s] startup handler complete", s.id) startupDone = true s.output.Reset() // Reset output after startup handling } @@ -161,12 +159,12 @@ func (s *Session) waitForReady(ctx context.Context, timeout time.Duration) error // Check if ready (only after startup is done) if startupDone && s.detector.IsReady(s.output.String()) { - log.Printf("[session %s] ready detected", s.id) + debug.Log("[session %s] ready detected", s.id) return nil } case <-time.After(time.Until(deadline)): if time.Now().After(deadline) { - log.Printf("[session %s] timeout, current output: %s", s.id, s.output.String()) + debug.Log("[session %s] timeout, current output: %s", s.id, s.output.String()) return fmt.Errorf("timeout waiting for ready") } } @@ -214,7 +212,7 @@ func (s *Session) Send(ctx context.Context, prompt string) (string, error) { // Release lock during long-running operations so state can be read s.mu.Unlock() - log.Printf("[session %s] sending: %q", s.id, prompt) + debug.Log("[session %s] sending: %q", s.id, prompt) // Send prompt using literal mode to avoid special character interpretation if err := sendLiteral(s.target(), prompt); err != nil { @@ -250,9 +248,9 @@ func (s *Session) Send(ctx context.Context, prompt string) (string, error) { rawLen := s.output.Len() rawOutput := s.output.String() result := s.cleaner.Clean(prompt, rawOutput) - log.Printf("[session %s] raw: %d bytes, cleaned: %d bytes", s.id, rawLen, len(result)) + debug.Log("[session %s] raw: %d bytes, cleaned: %d bytes", s.id, rawLen, len(result)) if len(result) == 0 && rawLen > 0 { - log.Printf("[session %s] WARNING: cleaner returned 0 bytes", s.id) + debug.Log("[session %s] WARNING: cleaner returned 0 bytes", s.id) // Debug: show first and last 500 chars of raw output debugFirst := rawOutput if len(debugFirst) > 500 { @@ -262,13 +260,13 @@ func (s *Session) Send(ctx context.Context, prompt string) (string, error) { if len(debugLast) > 500 { debugLast = debugLast[len(debugLast)-500:] } - log.Printf("[session %s] DEBUG raw output (first 500 chars): %q", s.id, debugFirst) - log.Printf("[session %s] DEBUG raw output (last 500 chars): %q", s.id, debugLast) + debug.Log("[session %s] DEBUG raw output (first 500 chars): %q", s.id, debugFirst) + debug.Log("[session %s] DEBUG raw output (last 500 chars): %q", s.id, debugLast) // Check for key markers (simple check, no ANSI stripping) hasBullet := strings.Contains(rawOutput, "●") hasThinking := strings.Contains(rawOutput, "(esc to interrupt)") || strings.Contains(rawOutput, "∴") - log.Printf("[session %s] DEBUG markers: hasBullet=%v, hasThinking=%v", s.id, hasBullet, hasThinking) + debug.Log("[session %s] DEBUG markers: hasBullet=%v, hasThinking=%v", s.id, hasBullet, hasThinking) // Find where the bullet is if hasBullet { @@ -281,7 +279,7 @@ func (s *Session) Send(ctx context.Context, prompt string) (string, error) { if end > len(rawOutput) { end = len(rawOutput) } - log.Printf("[session %s] DEBUG bullet context: %q", s.id, rawOutput[start:end]) + debug.Log("[session %s] DEBUG bullet context: %q", s.id, rawOutput[start:end]) } } @@ -300,7 +298,6 @@ func (s *Session) waitForComplete(ctx context.Context, prompt string) error { return fmt.Errorf("session closed") } s.output.Write(data) - log.Printf("[session %s] +%d bytes, total %d", s.id, len(data), s.output.Len()) if s.detector.IsComplete(prompt, s.output.String()) { // Drain any remaining data @@ -311,7 +308,7 @@ func (s *Session) waitForComplete(ctx context.Context, prompt string) error { s.output.Write(extra) } case <-time.After(10 * time.Millisecond): - log.Printf("[session %s] response complete, %d bytes", s.id, s.output.Len()) + debug.Log("[session %s] response complete, %d bytes", s.id, s.output.Len()) return nil } } @@ -352,14 +349,14 @@ func (s *Session) Close() error { s.mu.Lock() defer s.mu.Unlock() - log.Printf("[session %s] Close() called, killing window %s:%s", s.id, s.tmuxSession, s.windowName) + debug.Log("[session %s] Close() called, killing window %s:%s", s.id, s.tmuxSession, s.windowName) // 1. Kill tmux window first (closes pipe-pane writer) if s.tmuxSession != "" && s.windowName != "" { if err := killWindow(s.tmuxSession, s.windowName); err != nil { - log.Printf("[session %s] killWindow failed: %v", s.id, err) + debug.Log("[session %s] killWindow failed: %v", s.id, err) } else { - log.Printf("[session %s] Window %s:%s killed", s.id, s.tmuxSession, s.windowName) + debug.Log("[session %s] Window %s:%s killed", s.id, s.tmuxSession, s.windowName) } } diff --git a/internal/backend/tmux/tmux.go b/internal/backend/tmux/tmux.go index a5f3bd2..ce169e0 100644 --- a/internal/backend/tmux/tmux.go +++ b/internal/backend/tmux/tmux.go @@ -2,10 +2,10 @@ package tmux import ( - "acme-q/internal/backend" + "anvillm/internal/backend" + "anvillm/internal/debug" "context" "fmt" - "log" "os" "sync/atomic" "syscall" @@ -84,7 +84,7 @@ func (b *Backend) ensureTmuxSession() error { return nil } - log.Printf("[backend %s] creating persistent tmux session: %s", b.cfg.Name, b.tmuxSession) + debug.Log("[backend %s] creating persistent tmux session: %s", b.cfg.Name, b.tmuxSession) // Keep window 0 alive to hold the session open // It will be killed when the program exits return createSession(b.tmuxSession, b.cfg.TmuxSize.Rows, b.cfg.TmuxSize.Cols) @@ -93,7 +93,7 @@ func (b *Backend) ensureTmuxSession() error { // Cleanup kills the persistent tmux session (call on program exit) func (b *Backend) Cleanup() error { if sessionExists(b.tmuxSession) { - log.Printf("[backend %s] cleaning up tmux session: %s", b.cfg.Name, b.tmuxSession) + debug.Log("[backend %s] cleaning up tmux session: %s", b.cfg.Name, b.tmuxSession) return killSession(b.tmuxSession) } return nil @@ -107,7 +107,7 @@ func (b *Backend) CreateSession(ctx context.Context, cwd string) (backend.Sessio id := fmt.Sprintf("%d", atomic.AddUint64(&b.counter, 1)) windowName := id // Use session ID as window name - log.Printf("[session %s] creating window in tmux session %s", id, b.tmuxSession) + debug.Log("[session %s] creating window in tmux session %s", id, b.tmuxSession) // 1. Ensure persistent tmux session exists if err := b.ensureTmuxSession(); err != nil { @@ -204,7 +204,7 @@ func (b *Backend) CreateSession(ctx context.Context, cwd string) (backend.Sessio // 9. Get PID pid, err := getPanePID(target) if err != nil { - log.Printf("[session %s] warning: failed to get PID: %v", id, err) + debug.Log("[session %s] warning: failed to get PID: %v", id, err) pid = 0 } @@ -237,7 +237,7 @@ func (b *Backend) CreateSession(ctx context.Context, cwd string) (backend.Sessio sess.state = "idle" sess.output.Reset() - log.Printf("[session %s] ready (tmux=%s:%s, pid=%d)", sess.ID(), b.tmuxSession, windowName, sess.pid) + debug.Log("[session %s] ready (tmux=%s:%s, pid=%d)", sess.ID(), b.tmuxSession, windowName, sess.pid) return sess, nil } diff --git a/internal/backends/claude.go b/internal/backends/claude.go index 44cc880..7083986 100644 --- a/internal/backends/claude.go +++ b/internal/backends/claude.go @@ -2,8 +2,8 @@ package backends import ( - "acme-q/internal/backend" - "acme-q/internal/backend/tmux" + "anvillm/internal/backend" + "anvillm/internal/backend/tmux" "bufio" "context" "fmt" diff --git a/internal/backends/kirocli.go b/internal/backends/kirocli.go index 24189ca..54b5115 100644 --- a/internal/backends/kirocli.go +++ b/internal/backends/kirocli.go @@ -2,11 +2,11 @@ package backends import ( - "acme-q/internal/backend" - "acme-q/internal/backend/tmux" + "anvillm/internal/backend" + "anvillm/internal/backend/tmux" + "anvillm/internal/debug" "context" "fmt" - "log" "os" "path/filepath" "regexp" @@ -413,7 +413,7 @@ func SaveWithSmartFilename(sess backend.Session, id string, bodyText string) (st smartPath, err := GenerateSmartFilename(tempPath, bodyText) if err != nil { - log.Printf("[session %s] Smart filename failed: %v, keeping %s", id, err, tempPath) + debug.Log("[session %s] Smart filename failed: %v, keeping %s", id, err, tempPath) return tempPath, nil } @@ -421,7 +421,7 @@ func SaveWithSmartFilename(sess backend.Session, id string, bodyText string) (st if err := os.Rename(tempPath, smartPath); err == nil { return smartPath, nil } else { - log.Printf("[session %s] Rename failed: %v", id, err) + debug.Log("[session %s] Rename failed: %v", id, err) return tempPath, nil } } diff --git a/internal/debug/debug.go b/internal/debug/debug.go new file mode 100644 index 0000000..159e701 --- /dev/null +++ b/internal/debug/debug.go @@ -0,0 +1,13 @@ +package debug + +import "log" + +// Enabled controls whether debug logs are printed +var Enabled = false + +// Log prints a log message only when debug mode is enabled +func Log(format string, args ...interface{}) { + if Enabled { + log.Printf(format, args...) + } +} diff --git a/internal/p9/server.go b/internal/p9/server.go index f91cde3..ee73809 100644 --- a/internal/p9/server.go +++ b/internal/p9/server.go @@ -2,10 +2,10 @@ package p9 import ( - "acme-q/internal/backend" - "acme-q/internal/backend/tmux" - "acme-q/internal/backends" - "acme-q/internal/session" + "anvillm/internal/backend" + "anvillm/internal/backend/tmux" + "anvillm/internal/backends" + "anvillm/internal/session" "context" "errors" "fmt" diff --git a/internal/session/manager.go b/internal/session/manager.go index 0ef758b..e6ab3a1 100644 --- a/internal/session/manager.go +++ b/internal/session/manager.go @@ -2,7 +2,7 @@ package session import ( - "acme-q/internal/backend" + "anvillm/internal/backend" "context" "sync" ) diff --git a/main.go b/main.go index c612196..9bcfcfd 100644 --- a/main.go +++ b/main.go @@ -2,11 +2,12 @@ package main import ( - "acme-q/internal/backend" - "acme-q/internal/backend/tmux" - "acme-q/internal/backends" - "acme-q/internal/p9" - "acme-q/internal/session" + "anvillm/internal/backend" + "anvillm/internal/backend/tmux" + "anvillm/internal/backends" + "anvillm/internal/debug" + "anvillm/internal/p9" + "anvillm/internal/session" "context" "flag" "fmt" @@ -23,7 +24,7 @@ import ( "9fans.net/go/acme" ) -const windowName = "/Q/" +const windowName = "/AnviLLM/" // Terminal to use for tmux attach (configurable) // Common options: "foot", "kitty", "xterm", "st", "alacritty" @@ -46,10 +47,10 @@ func main() { switch backendName { case "claude": backend = backends.NewClaude() - log.Printf("Using claude backend") + debug.Log("Using claude backend") case "kiro-cli": backend = backends.NewKiroCLI() - log.Printf("Using kiro-cli backend") + debug.Log("Using kiro-cli backend") default: log.Fatalf("Unknown backend: %s (available: kiro-cli, claude)", backendName) } @@ -127,47 +128,47 @@ func main() { switch cmd { case "New": - // Use current directory if no path specified - cwd := arg - if cwd == "" { - cwd, _ = os.Getwd() + // Require path argument + if arg == "" { + fmt.Fprintf(os.Stderr, "Error: New requires a path argument\n") + continue } - sess, err := mgr.New(cwd) + sess, err := mgr.New(arg) if err != nil { - w.Fprintf("body", "Error: %v\n", err) + fmt.Fprintf(os.Stderr, "Error: %v\n", err) continue } _, err = openChatWindow(sess) if err != nil { - w.Fprintf("body", "Error opening chat: %v\n", err) + fmt.Fprintf(os.Stderr, "Error opening chat: %v\n", err) continue } // WinID is set inside openChatWindow refreshList(w, mgr) case "Open": if arg == "" { - w.Fprintf("body", "Usage: Open \n") + fmt.Fprintf(os.Stderr, "Usage: Open \n") continue } sess := mgr.Get(arg) if sess == nil { - w.Fprintf("body", "Session not found: %s\n", arg) + fmt.Fprintf(os.Stderr, "Session not found: %s\n", arg) continue } _, err := openChatWindow(sess) if err != nil { - w.Fprintf("body", "Error opening chat: %v\n", err) + fmt.Fprintf(os.Stderr, "Error opening chat: %v\n", err) continue } // WinID is set inside openChatWindow case "Kill": if arg == "" { - w.Fprintf("body", "Usage: Kill \n") + fmt.Fprintf(os.Stderr, "Usage: Kill \n") continue } pid, err := strconv.Atoi(arg) if err != nil { - w.Fprintf("body", "Invalid pid: %s\n", arg) + fmt.Fprintf(os.Stderr, "Invalid pid: %s\n", arg) continue } // Find session by pid and kill it @@ -178,9 +179,9 @@ func main() { if meta.Pid == pid { // First kill the process if err := syscall.Kill(pid, syscall.SIGTERM); err != nil { - log.Printf("[session %s] Failed to kill PID %d: %v", id, pid, err) + debug.Log("[session %s] Failed to kill PID %d: %v", id, pid, err) } else { - log.Printf("[session %s] Killed PID %d", id, pid) + debug.Log("[session %s] Killed PID %d", id, pid) } // Then close the tmux window sess.Close() @@ -192,22 +193,23 @@ func main() { refreshList(w, mgr) case "Get": refreshList(w, mgr) + case "Debug": + debug.Enabled = !debug.Enabled + state := "disabled" + if debug.Enabled { + state = "enabled" + } + fmt.Printf("Debug mode %s\n", state) case "Login": cmd := exec.Command("firefox") cmd.Start() cmd = exec.Command("foot", "-e", "q", "login") - log.Printf("starting terminal, pid will follow") - if err := cmd.Start(); err != nil { - log.Printf("terminal start error: %v", err) - } else { - log.Printf("terminal started, pid=%d", cmd.Process.Pid) - } + cmd.Start() default: w.WriteEvent(e) } case 'l', 'L': text := strings.TrimSpace(string(e.Text)) - log.Printf("[/Q/] Look: text=%q", text) if sess := mgr.Get(text); sess != nil { // Try to focus existing window, or open new one meta := sess.Metadata() @@ -315,9 +317,9 @@ func handleChatWindow(w *acme.Win, sess backend.Session) { case "Stop": ctx := context.Background() if err := sess.Stop(ctx); err != nil { - log.Printf("[session %s] Stop error: %v", sess.ID(), err) + debug.Log("[session %s] Stop error: %v", sess.ID(), err) } else { - log.Printf("[session %s] Sent CTRL+C", sess.ID()) + debug.Log("[session %s] Sent CTRL+C", sess.ID()) } case "Attach": // Attach to tmux window (only works for tmux-based backends) @@ -330,23 +332,23 @@ func handleChatWindow(w *acme.Win, sess backend.Session) { target := fmt.Sprintf("%s:%s", tmuxSession, tmuxWindow) cmd := exec.Command(terminalCommand, "-e", "tmux", "attach", "-t", target) if err := cmd.Start(); err != nil { - log.Printf("[session %s] Failed to launch terminal: %v", sess.ID(), err) + debug.Log("[session %s] Failed to launch terminal: %v", sess.ID(), err) } else { - log.Printf("[session %s] Launched terminal attached to window: %s", sess.ID(), target) + debug.Log("[session %s] Launched terminal attached to window: %s", sess.ID(), target) } }() } else { - log.Printf("[session %s] Attach not supported: not a tmux-based backend", sess.ID()) + debug.Log("[session %s] Attach not supported: not a tmux-based backend", sess.ID()) } case "Alias": if arg == "" { - log.Printf("[session %s] Usage: Alias ", sess.ID()) + debug.Log("[session %s] Usage: Alias ", sess.ID()) continue } // Validate alias matched, _ := regexp.MatchString(`^[A-Za-z0-9_-]+$`, arg) if !matched { - log.Printf("[session %s] Invalid alias: must match [A-Za-z0-9_-]+", sess.ID()) + debug.Log("[session %s] Invalid alias: must match [A-Za-z0-9_-]+", sess.ID()) continue } sess.SetAlias(arg) @@ -354,7 +356,7 @@ func handleChatWindow(w *acme.Win, sess backend.Session) { meta := sess.Metadata() newName := filepath.Join(meta.Cwd, fmt.Sprintf("+Chat.%s", arg)) w.Name(newName) - log.Printf("[session %s] alias set to %s", sess.ID(), arg) + debug.Log("[session %s] alias set to %s", sess.ID(), arg) case "Save": // Grab first part of window content for filename hints var bodyText string @@ -376,10 +378,10 @@ func handleChatWindow(w *acme.Win, sess backend.Session) { savePath, err = backends.SaveWithSmartFilename(sess, sess.ID(), bodyText) } if err != nil { - log.Printf("[session %s] Error saving: %v", sess.ID(), err) + debug.Log("[session %s] Error saving: %v", sess.ID(), err) return } - log.Printf("[session %s] Saved to %s", sess.ID(), savePath) + debug.Log("[session %s] Saved to %s", sess.ID(), savePath) }() case "Sessions": openSessionsWindow(w, sess) @@ -395,14 +397,14 @@ func handleChatWindow(w *acme.Win, sess backend.Session) { func sendPrompt(w *acme.Win, sess backend.Session) { body, err := w.ReadAll("body") if err != nil { - log.Printf("[session %s] Error reading body: %v", sess.ID(), err) + debug.Log("[session %s] Error reading body: %v", sess.ID(), err) return } content := string(body) idx := strings.LastIndex(content, "USER:") if idx < 0 { - log.Printf("[session %s] No USER: section found", sess.ID()) + debug.Log("[session %s] No USER: section found", sess.ID()) return } @@ -416,7 +418,7 @@ func sendPrompt(w *acme.Win, sess backend.Session) { ctx := context.Background() response, err := sess.Send(ctx, prompt) if err != nil { - log.Printf("[session %s] Error: %v", sess.ID(), err) + debug.Log("[session %s] Error: %v", sess.ID(), err) } else { w.Fprintf("body", "%s\n", response) }