55 "flag"
66 "fmt"
77 "io"
8+ "io/fs"
89 "log"
910 "os"
1011 "os/signal"
@@ -28,6 +29,111 @@ var (
2829 GitSHA = "unknown"
2930)
3031
32+ // resolveRuntimes processes a list of runtime strings (which may include "auto" or "all")
33+ // and returns the expanded list of parser.Runtime values with proper validation and detection.
34+ func resolveRuntimes (runtimeStrs []string , companyID string ) ([]parser.Runtime , error ) {
35+ var resolved []parser.Runtime
36+
37+ // Check for "auto" runtime (auto-detection)
38+ if len (runtimeStrs ) == 1 && runtimeStrs [0 ] == "auto" {
39+ var detected []parser.Runtime
40+
41+ // Detect Claude Code: check if ~/.claude/projects/ exists and has .jsonl files
42+ home , err := os .UserHomeDir ()
43+ if err == nil {
44+ claudeDir := filepath .Join (home , ".claude" , "projects" )
45+ if hasJSONLFiles (claudeDir ) {
46+ detected = append (detected , parser .RuntimeClaude )
47+ }
48+ }
49+
50+ // Detect Paperclip: only if company flag was set
51+ if companyID != "" {
52+ detected = append (detected , parser .RuntimePaperclip )
53+ }
54+
55+ // Detect Codex: check if ~/.codex/ exists and has session files
56+ if err == nil {
57+ codexDir := filepath .Join (home , ".codex" )
58+ if _ , err := os .Stat (codexDir ); err == nil {
59+ detected = append (detected , parser .RuntimeCodex )
60+ }
61+ }
62+
63+ // Fallback: if nothing detected, show Claude even if no sessions yet
64+ if len (detected ) == 0 {
65+ detected = append (detected , parser .RuntimeClaude )
66+ }
67+
68+ return detected , nil
69+ }
70+
71+ // Handle "all" runtime (enable all runtimes)
72+ if len (runtimeStrs ) == 1 && runtimeStrs [0 ] == "all" {
73+ resolved = append (resolved , parser .RuntimeClaude )
74+ resolved = append (resolved , parser .RuntimeCodex )
75+ // Paperclip requires company ID
76+ if companyID != "" {
77+ resolved = append (resolved , parser .RuntimePaperclip )
78+ } else {
79+ log .Printf ("Warning: paperclip runtime requires --company flag; skipping" )
80+ }
81+ return resolved , nil
82+ }
83+
84+ // Parse explicit list of runtimes
85+ for _ , r := range runtimeStrs {
86+ switch r {
87+ case "paperclip" :
88+ if companyID == "" {
89+ return nil , fmt .Errorf ("paperclip runtime requires --company flag" )
90+ }
91+ resolved = append (resolved , parser .RuntimePaperclip )
92+ case "claude" :
93+ resolved = append (resolved , parser .RuntimeClaude )
94+ case "codex" :
95+ resolved = append (resolved , parser .RuntimeCodex )
96+ default :
97+ return nil , fmt .Errorf ("invalid runtime value: %s (valid: auto|all|claude|codex|paperclip)" , r )
98+ }
99+ }
100+
101+ return resolved , nil
102+ }
103+
104+ // hasJSONLFiles checks if a directory exists and contains any .jsonl files.
105+ func hasJSONLFiles (dir string ) bool {
106+ entries , err := os .ReadDir (dir )
107+ if err != nil {
108+ return false
109+ }
110+
111+ for _ , entry := range entries {
112+ if entry .IsDir () {
113+ // Recursively check subdirectories
114+ subdir := filepath .Join (dir , entry .Name ())
115+ if hasJSONLFilesInDir (subdir ) {
116+ return true
117+ }
118+ }
119+ }
120+ return false
121+ }
122+
123+ // hasJSONLFilesInDir recursively searches for .jsonl files in a directory.
124+ func hasJSONLFilesInDir (dir string ) bool {
125+ err := filepath .WalkDir (dir , func (path string , d fs.DirEntry , err error ) error {
126+ if err != nil {
127+ return nil // Skip errors, keep searching
128+ }
129+ if ! d .IsDir () && strings .HasSuffix (d .Name (), ".jsonl" ) {
130+ return filepath .SkipDir // Signal that we found a file (stop walking)
131+ }
132+ return nil
133+ })
134+ return err == filepath .SkipDir
135+ }
136+
31137func main () {
32138 // Flags
33139 companyID := flag .String ("company" , "" , "Company ID (optional)" )
@@ -106,11 +212,6 @@ Examples:
106212 os .Exit (1 )
107213 }
108214
109- // When no company is specified, default to Claude-only mode
110- if * companyID == "" && * runtimesStr == "" {
111- cfg .Runtimes = []string {"claude" }
112- }
113-
114215 // Apply command-line flag overrides
115216 if * apiURL != "" {
116217 cfg .APIURL = * apiURL
@@ -179,20 +280,11 @@ Examples:
179280 agentNamer = aggregator.NullAgentNamer {}
180281 }
181282
182- // Parse config runtimes into parser.Runtime values
183- var runtimes []parser.Runtime
184- for _ , r := range cfg .Runtimes {
185- switch r {
186- case "paperclip" :
187- runtimes = append (runtimes , parser .RuntimePaperclip )
188- case "claude" :
189- runtimes = append (runtimes , parser .RuntimeClaude )
190- case "codex" :
191- runtimes = append (runtimes , parser .RuntimeCodex )
192- default :
193- fmt .Fprintf (os .Stderr , "Error: invalid runtime value in config: %s (valid: paperclip|claude|codex)\n " , r )
194- os .Exit (1 )
195- }
283+ // Resolve config runtimes (handles "auto" detection, "all" expansion, and validation)
284+ runtimes , err := resolveRuntimes (cfg .Runtimes , * companyID )
285+ if err != nil {
286+ fmt .Fprintf (os .Stderr , "Error: %v\n " , err )
287+ os .Exit (1 )
196288 }
197289
198290 // Create watcher
0 commit comments