diff --git a/internal/gateway/claude_warmup.go b/internal/gateway/claude_warmup.go index 8d86beae..e8143c6c 100644 --- a/internal/gateway/claude_warmup.go +++ b/internal/gateway/claude_warmup.go @@ -53,11 +53,7 @@ func askClaude(ctx context.Context, configDir, model string) error { var out, stderr bytes.Buffer cmd.Stdout, cmd.Stderr = &out, &stderr runErr := cmd.Run() - var res struct { - IsError bool `json:"is_error"` - Result string `json:"result"` - } - if json.Unmarshal(out.Bytes(), &res) == nil && res.IsError { + if res, ok := claudeResult(out.Bytes()); ok && res.IsError { return errors.New("Claude Code: " + clip(res.Result)) } if runErr != nil { @@ -98,11 +94,7 @@ func claudeUsage(ctx context.Context) (string, error) { var out, stderr bytes.Buffer cmd.Stdout, cmd.Stderr = &out, &stderr runErr := cmd.Run() - var res struct { - IsError bool `json:"is_error"` - Result string `json:"result"` - } - if err := json.Unmarshal(out.Bytes(), &res); err == nil { + if res, ok := claudeResult(out.Bytes()); ok { if res.IsError { return "", errors.New("Claude Code: " + clip(res.Result)) } @@ -118,6 +110,35 @@ func claudeUsage(ctx context.Context) (string, error) { return "", errors.New("Claude Code: " + clip(msg)) } +// claudeRun is the result Claude Code prints for a -p run with +// --output-format json. +type claudeRun struct { + Type string `json:"type"` + IsError bool `json:"is_error"` + Result string `json:"result"` +} + +// claudeResult reads the result from what claude -p --output-format json +// printed: one object, or, when the user turned on Claude Code's verbose +// output (its own config, which --setting-sources leaves on), an array of +// every message, the result last. +func claudeResult(b []byte) (claudeRun, bool) { + var res claudeRun + if json.Unmarshal(b, &res) == nil { + return res, true + } + var all []claudeRun + if json.Unmarshal(b, &all) != nil { + return claudeRun{}, false + } + for i := len(all) - 1; i >= 0; i-- { + if all[i].Type == "result" { + return all[i], true + } + } + return claudeRun{}, false +} + func claudeUsageArgs() []string { return []string{"-p", "/usage", "--output-format", "json", "--tools", "", "--strict-mcp-config", "--setting-sources", "", "--no-session-persistence"} diff --git a/internal/gateway/claude_warmup_test.go b/internal/gateway/claude_warmup_test.go index 6517e6c8..9e7539ba 100644 --- a/internal/gateway/claude_warmup_test.go +++ b/internal/gateway/claude_warmup_test.go @@ -115,3 +115,28 @@ func TestClaudeUsageRunsClaudeCode(t *testing.T) { } } } + +// With Claude Code's verbose output on (its /config, kept in .claude.json), +// claude -p --output-format json prints every message as an array, the +// system init first and the result last (KevinXC on Discord: usage +// couldn't be read; the tooltip began [{"type":"system","subtype":"init") +func TestClaudeVerboseOutput(t *testing.T) { + text := "Current session: 13% used" + b, _ := json.Marshal([]map[string]any{ + {"type": "system", "subtype": "init", "tools": []string{}}, + {"type": "result", "subtype": "success", "is_error": false, "result": text}, + }) + fakeWarmClaude(t, string(b), 0) + got, err := claudeUsage(context.Background()) + if err != nil || got != text { + t.Fatalf("usage %q %v", got, err) + } + b, _ = json.Marshal([]map[string]any{ + {"type": "system", "subtype": "init"}, + {"type": "result", "is_error": true, "result": "Invalid API key ยท Please run /login"}, + }) + fakeWarmClaude(t, string(b), 1) + if err := warmClaude(context.Background(), ""); err == nil || !strings.Contains(err.Error(), "Please run /login") || strings.Contains(err.Error(), "init") { + t.Fatalf("got %v", err) + } +}