summaryrefslogtreecommitdiff
path: root/internal/cli/logs.go
diff options
context:
space:
mode:
authorPeter Stone <thepeterstone@gmail.com>2026-02-24 02:01:17 +0000
committerPeter Stone <thepeterstone@gmail.com>2026-02-24 02:01:17 +0000
commitf27d4f7ef3949627c9cb1077f90135a5268b7631 (patch)
tree3f6c2dcca7ba33d0a1d377b50e43eb08834bf412 /internal/cli/logs.go
parent0377c06310cf92cfa477917f35f5e0755c09f063 (diff)
Add logs CLI subcommand to tail execution output
Adds `claudomator logs <execution-id>` 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 <noreply@anthropic.com>
Diffstat (limited to 'internal/cli/logs.go')
-rw-r--r--internal/cli/logs.go107
1 files changed, 107 insertions, 0 deletions
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 <exec-id>",
+ 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
+}