From f27d4f7ef3949627c9cb1077f90135a5268b7631 Mon Sep 17 00:00:00 2001 From: Peter Stone Date: Tue, 24 Feb 2026 02:01:17 +0000 Subject: Add logs CLI subcommand to tail execution output Adds `claudomator logs ` to stream or display stdout logs from a past or running execution. Includes unit tests for the command. Co-Authored-By: Claude Sonnet 4.6 --- internal/cli/logs.go | 107 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 internal/cli/logs.go (limited to 'internal/cli/logs.go') diff --git a/internal/cli/logs.go b/internal/cli/logs.go new file mode 100644 index 0000000..cabb12a --- /dev/null +++ b/internal/cli/logs.go @@ -0,0 +1,107 @@ +package cli + +import ( + "bufio" + "encoding/json" + "fmt" + "io" + "os" + + "github.com/spf13/cobra" + "github.com/thepeterstone/claudomator/internal/storage" +) + +type executionStore interface { + GetExecution(id string) (*storage.Execution, error) +} + +func newLogsCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "logs ", + Short: "Show logs for an execution", + Args: cobra.ExactArgs(1), + SilenceErrors: true, + SilenceUsage: true, + RunE: func(cmd *cobra.Command, args []string) error { + store, err := storage.Open(cfg.DBPath) + if err != nil { + return fmt.Errorf("opening db: %w", err) + } + defer store.Close() + return renderLogs(args[0], store, cmd.OutOrStdout()) + }, + } + return cmd +} + +type streamMessage struct { + Type string `json:"type"` + Message *assistantMsg `json:"message,omitempty"` +} + +type assistantMsg struct { + Content []contentBlock `json:"content"` +} + +type contentBlock struct { + Type string `json:"type"` + Text string `json:"text,omitempty"` + Name string `json:"name,omitempty"` + Input json.RawMessage `json:"input,omitempty"` +} + +func renderLogs(execID string, store executionStore, w io.Writer) error { + exec, err := store.GetExecution(execID) + if err != nil { + fmt.Fprintf(w, "execution %s not found\n", execID) + return fmt.Errorf("execution %s not found", execID) + } + + if exec.StdoutPath == "" { + fmt.Fprintln(w, "no output recorded") + return nil + } + + fi, err := os.Stat(exec.StdoutPath) + if err != nil || fi.Size() == 0 { + fmt.Fprintln(w, "no output recorded") + return nil + } + + f, err := os.Open(exec.StdoutPath) + if err != nil { + fmt.Fprintln(w, "no output recorded") + return nil + } + defer f.Close() + + scanner := bufio.NewScanner(f) + for scanner.Scan() { + line := scanner.Bytes() + if len(line) == 0 { + continue + } + var msg streamMessage + if err := json.Unmarshal(line, &msg); err != nil { + continue + } + if msg.Type != "assistant" || msg.Message == nil { + continue + } + for _, block := range msg.Message.Content { + switch block.Type { + case "text": + fmt.Fprintln(w, block.Text) + case "tool_use": + summary := string(block.Input) + if len(summary) > 80 { + summary = summary[:80] + } + fmt.Fprintf(w, " > %s (%s)\n", block.Name, summary) + } + } + } + + fmt.Fprintf(w, "Cost: $%.4f Exit: %d\n", exec.CostUSD, exec.ExitCode) + return nil +} -- cgit v1.2.3