@@ -5,16 +5,20 @@ import (
5
5
"log/slog"
6
6
"os"
7
7
"path/filepath"
8
+ "strings"
9
+ "time"
8
10
9
11
"github.com/docker/cagent/internal/config"
10
12
"github.com/docker/cagent/internal/telemetry"
11
13
"github.com/spf13/cobra"
12
14
)
13
15
14
16
var (
15
- agentName string
16
- debugMode bool
17
- enableOtel bool
17
+ agentName string
18
+ debugMode bool
19
+ enableOtel bool
20
+ logFilePath string
21
+ logFile * os.File
18
22
)
19
23
20
24
// isFirstRun checks if this is the first time cagent is being run
@@ -47,15 +51,28 @@ func NewRootCmd() *cobra.Command {
47
51
Short : "cagent - AI agent runner" ,
48
52
Long : `cagent is a command-line tool for running AI agents` ,
49
53
PersistentPreRunE : func (cmd * cobra.Command , args []string ) error {
54
+ // Initialize logging before anything else so logs don't break TUI
55
+ if err := setupLogging (cmd ); err != nil {
56
+ // If logging setup fails, fall back to stderr so we still get logs
57
+ slog .SetDefault (slog .New (slog .NewTextHandler (os .Stderr , & slog.HandlerOptions {
58
+ Level : func () slog.Level {
59
+ if debugMode {
60
+ return slog .LevelDebug
61
+ }
62
+ return slog .LevelInfo
63
+ }(),
64
+ })))
65
+ }
50
66
if cmd .DisplayName () != "exec" && os .Getenv ("CAGENT_HIDE_FEEDBACK_LINK" ) != "1" {
51
67
_ , _ = cmd .OutOrStdout ().Write ([]byte ("\n For any feedback, please visit: " + FeedbackLink + "\n \n " ))
52
68
}
53
69
54
70
telemetry .SetGlobalTelemetryDebugMode (debugMode )
55
- if debugMode {
56
- slog .SetDefault (slog .New (slog .NewTextHandler (os .Stderr , & slog.HandlerOptions {
57
- Level : slog .LevelDebug ,
58
- })))
71
+ return nil
72
+ },
73
+ PersistentPostRunE : func (cmd * cobra.Command , args []string ) error {
74
+ if logFile != nil {
75
+ _ = logFile .Close ()
59
76
}
60
77
return nil
61
78
},
@@ -68,6 +85,7 @@ func NewRootCmd() *cobra.Command {
68
85
// Add persistent debug flag available to all commands
69
86
cmd .PersistentFlags ().BoolVarP (& debugMode , "debug" , "d" , false , "Enable debug logging" )
70
87
cmd .PersistentFlags ().BoolVarP (& enableOtel , "otel" , "o" , false , "Enable OpenTelemetry tracing" )
88
+ cmd .PersistentFlags ().StringVar (& logFilePath , "log-file" , "" , "Path to log file (default: ~/.cagent/cagent.log)" )
71
89
72
90
cmd .AddCommand (NewVersionCmd ())
73
91
cmd .AddCommand (NewRunCmd ())
@@ -110,3 +128,86 @@ We collect anonymous usage data to help improve cagent. To disable:
110
128
os .Exit (1 )
111
129
}
112
130
}
131
+
132
+ // setupLogging configures slog to write to a file instead of stdout/stderr when using the TUI.
133
+ // By default, it writes to <dataDir>/logs/cagent-<timestamp>.log. Users can override with --log-file.
134
+ func setupLogging (cmd * cobra.Command ) error {
135
+ // Determine log file path
136
+ if logFilePath != "" {
137
+ path := logFilePath
138
+ if path == "" {
139
+ dataDir := config .GetDataDir ()
140
+ path = filepath .Join (dataDir , "cagent.log" )
141
+ } else {
142
+ if path == "~" || strings .HasPrefix (path , "~/" ) {
143
+ homeDir , err := os .UserHomeDir ()
144
+ if err == nil {
145
+ path = filepath .Join (homeDir , strings .TrimPrefix (path , "~/" ))
146
+ }
147
+ } else if strings .HasPrefix (path , "~\\ " ) { // Windows-style path expansion
148
+ homeDir , err := os .UserHomeDir ()
149
+ if err == nil {
150
+ path = filepath .Join (homeDir , strings .TrimPrefix (path , "~\\ " ))
151
+ }
152
+ }
153
+ }
154
+
155
+ // Ensure directory exists
156
+ if err := os .MkdirAll (filepath .Dir (path ), 0o755 ); err != nil {
157
+ return err
158
+ }
159
+
160
+ // Open file for appending
161
+ f , err := os .OpenFile (path , os .O_APPEND | os .O_CREATE | os .O_WRONLY , 0o644 )
162
+ if err != nil {
163
+ return err
164
+ }
165
+ logFile = f
166
+
167
+ // Configure slog default logger
168
+ level := slog .LevelInfo
169
+ if debugMode {
170
+ level = slog .LevelDebug
171
+ }
172
+ slog .SetDefault (slog .New (slog .NewTextHandler (f , & slog.HandlerOptions {Level : level })))
173
+ return nil
174
+ }
175
+
176
+ // Else, decide based on TUI flag: file when TUI is enabled, stderr otherwise
177
+ useTUI := false
178
+ if cmd != nil && cmd .Name () == "run" {
179
+ if f := cmd .Flags ().Lookup ("tui" ); f != nil {
180
+ if v , err := cmd .Flags ().GetBool ("tui" ); err == nil {
181
+ useTUI = v
182
+ }
183
+ }
184
+ }
185
+ if useTUI {
186
+ dataDir := config .GetDataDir ()
187
+ logsDir := filepath .Join (dataDir , "logs" )
188
+ if err := os .MkdirAll (logsDir , 0o755 ); err != nil {
189
+ return err
190
+ }
191
+ timestamp := time .Now ().Format ("20060102-150405" )
192
+ path := filepath .Join (logsDir , fmt .Sprintf ("cagent-%s.log" , timestamp ))
193
+ f , err := os .OpenFile (path , os .O_CREATE | os .O_WRONLY , 0o644 )
194
+ if err != nil {
195
+ return err
196
+ }
197
+ logFile = f
198
+ level := slog .LevelInfo
199
+ if debugMode {
200
+ level = slog .LevelDebug
201
+ }
202
+ slog .SetDefault (slog .New (slog .NewTextHandler (f , & slog.HandlerOptions {Level : level })))
203
+ return nil
204
+ }
205
+
206
+ // Default to stderr
207
+ level := slog .LevelInfo
208
+ if debugMode {
209
+ level = slog .LevelDebug
210
+ }
211
+ slog .SetDefault (slog .New (slog .NewTextHandler (os .Stderr , & slog.HandlerOptions {Level : level })))
212
+ return nil
213
+ }
0 commit comments