copperhead 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.md +51 -1
  2. package/dist/agent/loop.js +44 -4
  3. package/dist/agent/loop.js.map +1 -1
  4. package/dist/agent/providers/claude-code.js +286 -0
  5. package/dist/agent/providers/claude-code.js.map +1 -0
  6. package/dist/agent/providers/codex.js +292 -0
  7. package/dist/agent/providers/codex.js.map +1 -0
  8. package/dist/agent/providers/openai.js +30 -10
  9. package/dist/agent/providers/openai.js.map +1 -1
  10. package/dist/cli.js +47 -2
  11. package/dist/cli.js.map +1 -1
  12. package/dist/commands/create.js +16 -0
  13. package/dist/commands/create.js.map +1 -1
  14. package/dist/commands/export.js +90 -0
  15. package/dist/commands/export.js.map +1 -0
  16. package/dist/config.js +17 -7
  17. package/dist/config.js.map +1 -1
  18. package/dist/kicad/bom-export.js +240 -0
  19. package/dist/kicad/bom-export.js.map +1 -0
  20. package/dist/kicad/fab.js +94 -0
  21. package/dist/kicad/fab.js.map +1 -0
  22. package/dist/memory/bom-table.js +61 -0
  23. package/dist/memory/bom-table.js.map +1 -0
  24. package/dist/memory/drift.js +1 -17
  25. package/dist/memory/drift.js.map +1 -1
  26. package/dist/memory/scaffold.js +2 -1
  27. package/dist/memory/scaffold.js.map +1 -1
  28. package/package.json +17 -2
  29. package/src/agent/loop.ts +49 -4
  30. package/src/agent/providers/claude-code.ts +367 -0
  31. package/src/agent/providers/codex.ts +339 -0
  32. package/src/agent/providers/openai.ts +33 -16
  33. package/src/agent/types.ts +2 -0
  34. package/src/cli.ts +52 -2
  35. package/src/commands/create.ts +15 -0
  36. package/src/commands/export.ts +117 -0
  37. package/src/config.ts +23 -7
  38. package/src/kicad/bom-export.ts +321 -0
  39. package/src/kicad/fab.ts +121 -0
  40. package/src/memory/bom-table.ts +78 -0
  41. package/src/memory/drift.ts +1 -22
  42. package/src/memory/scaffold.ts +2 -1
@@ -99,7 +99,8 @@ Generated by \`copperhead init\`; regenerated on re-runs (do not hand-edit).
99
99
 
100
100
  - \`schematic\` / \`board\`: repo-relative paths to the KiCad files copperhead operates on (currently: ${config.schematic ?? 'none'} / ${config.board ?? 'none'})
101
101
  - \`docs\`: design docs directory (docs-as-memory), default \`docs/\`
102
- - \`model\`: default model (\`gpt-5\` or \`claude\`); overridden by \`--model\` and \`COPPERHEAD_MODEL\`
102
+ - \`model\`: default provider/model (\`codex\`, \`gpt-5\`, or \`claude\`); overridden by \`--model\` and \`COPPERHEAD_MODEL\`
103
+ - local Codex uses the saved \`codex login\`; set \`COPPERHEAD_CODEX_PATH\` only when the executable is not on \`PATH\`
103
104
  - \`maxTurns\`: agent loop turn budget per run (default 40)
104
105
  - \`maxRepairCycles\`: ERC/DRC repair attempts before rollback (default 5)
105
106
  - \`budgets\`: free-form hard constraints (e.g. \`"sleep_current_uA": 25\`); surfaced verbatim into every run's system prompt
@@ -1 +1 @@
1
- {"version":3,"file":"scaffold.js","sourceRoot":"","sources":["../../src/memory/scaffold.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"scaffold.js","sourceRoot":"","sources":["../../src/memory/scaffold.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "copperhead",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "description": "Cursor for circuit boards: an AI agent that designs, documents, and validates real PCBs on KiCad repositories",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Animesh Chouhan <animeshchouhan@outlook.com>",
@@ -48,6 +48,7 @@
48
48
  "prepublishOnly": "npm run typecheck && npm run build",
49
49
  "dev": "tsx src/cli.ts",
50
50
  "demo:simple": "bash scripts/demo-simple.sh",
51
+ "lint:md": "markdownlint-cli2 \"**/*.md\" \"#node_modules/**\" \"#**/node_modules/**\" \"#dist/**\" \"#**/dist/**\" \"#docs/.astro/**\" \"#demo-runs/**\" \"#.copperhead/runs/**\" \"#_site/**\"",
51
52
  "test": "vitest run",
52
53
  "test:watch": "vitest",
53
54
  "typecheck": "tsc -p tsconfig.json --noEmit",
@@ -56,20 +57,34 @@
56
57
  "docs:preview": "npm --prefix docs run preview"
57
58
  },
58
59
  "dependencies": {
59
- "@anthropic-ai/sdk": "^0.39.0",
60
+ "@anthropic-ai/sdk": "^0.113.0",
60
61
  "commander": "^12.1.0",
61
62
  "execa": "^9.5.2",
62
63
  "openai": "^4.87.3"
63
64
  },
64
65
  "optionalDependencies": {
66
+ "@anthropic-ai/claude-agent-sdk": "^0.3.217",
65
67
  "@maximem/synap-js-sdk": "^0.3.0"
66
68
  },
69
+ "overrides": {
70
+ "zod": "^4.0.0"
71
+ },
67
72
  "devDependencies": {
73
+ "@openai/codex-sdk": "^0.144.6",
68
74
  "@types/node": "^22.13.10",
75
+ "markdownlint-cli2": "0.22.0",
69
76
  "tsx": "^4.19.3",
70
77
  "typescript": "^5.8.2",
71
78
  "vitest": "^3.0.8"
72
79
  },
80
+ "peerDependencies": {
81
+ "@openai/codex-sdk": "^0.144.6"
82
+ },
83
+ "peerDependenciesMeta": {
84
+ "@openai/codex-sdk": {
85
+ "optional": true
86
+ }
87
+ },
73
88
  "allowScripts": {
74
89
  "esbuild@0.28.1": true,
75
90
  "esbuild@0.21.5": true
package/src/agent/loop.ts CHANGED
@@ -16,6 +16,8 @@ import { openspecArchive } from '../openspec/cli.js';
16
16
  import { existsSync } from 'node:fs';
17
17
  import { OpenAIProvider } from './providers/openai.js';
18
18
  import { AnthropicProvider } from './providers/anthropic.js';
19
+ import { CodexProvider } from './providers/codex.js';
20
+ import { ClaudeCodeProvider } from './providers/claude-code.js';
19
21
  import { openSynapMemory, type RunRecord, type SynapMemory } from '../memory/synap.js';
20
22
 
21
23
  /** What the user sees at the moment they decide whether to keep going. */
@@ -63,7 +65,34 @@ export interface RunResult {
63
65
  commit: string | null;
64
66
  }
65
67
 
66
- export function makeProvider(model: string): Provider {
68
+ export async function makeProvider(model: string): Promise<Provider> {
69
+ if (model === 'codex' || model.startsWith('codex:')) {
70
+ const codexModel = model.startsWith('codex:') ? model.slice('codex:'.length) : undefined;
71
+ if (codexModel === '') throw new Error('codex model override cannot be empty; use "codex" or "codex:<model-id>"');
72
+ const { Codex } = await import('@openai/codex-sdk').catch((err: unknown) => {
73
+ throw new Error(
74
+ 'Codex provider requires the optional @openai/codex-sdk package; install it alongside Copperhead before using --model codex',
75
+ { cause: err },
76
+ );
77
+ });
78
+ return new CodexProvider({
79
+ ...(codexModel ? { model: codexModel } : {}),
80
+ client: new Codex({
81
+ // Use the user's installed CLI and its saved login rather than a model API key.
82
+ codexPathOverride: process.env.COPPERHEAD_CODEX_PATH || 'codex',
83
+ }),
84
+ });
85
+ }
86
+ // Match claude-code before the `claude*` prefix: both `claude-code` and
87
+ // `claude-code:<id>` start with `claude` and would otherwise route to the
88
+ // Anthropic API provider.
89
+ if (model === 'claude-code' || model.startsWith('claude-code:')) {
90
+ const claudeCodeModel = model.startsWith('claude-code:') ? model.slice('claude-code:'.length) : undefined;
91
+ if (claudeCodeModel === '') {
92
+ throw new Error('claude-code model override cannot be empty; use "claude-code" or "claude-code:<model-id>"');
93
+ }
94
+ return new ClaudeCodeProvider(claudeCodeModel);
95
+ }
67
96
  if (model === 'claude' || model.startsWith('claude')) {
68
97
  return new AnthropicProvider(model === 'claude' ? undefined : model);
69
98
  }
@@ -71,6 +100,8 @@ export function makeProvider(model: string): Provider {
71
100
  }
72
101
 
73
102
  function otherProvider(current: Provider): Provider | null {
103
+ // Only the two keyed providers fail over to each other. A rate-limited
104
+ // 'claude-code' run returns null here (no silent fallback to a paid API).
74
105
  if (current.name === 'openai' && process.env.ANTHROPIC_API_KEY) return new AnthropicProvider();
75
106
  if (current.name === 'anthropic' && process.env.OPENAI_API_KEY) return new OpenAIProvider();
76
107
  return null;
@@ -117,14 +148,26 @@ async function appendChangelog(
117
148
  */
118
149
  export async function runAgentLoop(opts: RunOptions): Promise<RunResult> {
119
150
  const memory = await openSynapMemory({ repoRoot: opts.repoRoot, log: opts.log });
151
+ const providers = new Set<Provider>();
120
152
  try {
121
- return await runWithMemory(opts, memory);
153
+ return await runWithMemory(opts, memory, providers);
122
154
  } finally {
155
+ for (const provider of providers) {
156
+ try {
157
+ await provider.close?.();
158
+ } catch (err) {
159
+ opts.log?.(`warning: ${provider.name} provider cleanup failed (${(err as Error).message})`);
160
+ }
161
+ }
123
162
  await memory?.close();
124
163
  }
125
164
  }
126
165
 
127
- async function runWithMemory(opts: RunOptions, memory: SynapMemory | null): Promise<RunResult> {
166
+ async function runWithMemory(
167
+ opts: RunOptions,
168
+ memory: SynapMemory | null,
169
+ providers: Set<Provider>,
170
+ ): Promise<RunResult> {
128
171
  const r = opts.renderer ?? plainRenderer(opts.log ?? ((l: string) => console.log(l)));
129
172
  const log = (l: string): void => r.log(l);
130
173
  const repoRoot = opts.repoRoot;
@@ -155,7 +198,8 @@ async function runWithMemory(opts: RunOptions, memory: SynapMemory | null): Prom
155
198
  finishRequest: null,
156
199
  };
157
200
 
158
- let provider = opts.provider ?? makeProvider(opts.model);
201
+ let provider = opts.provider ?? (await makeProvider(opts.model));
202
+ providers.add(provider);
159
203
 
160
204
  // Deterministic, LLM-free metadata block: collected once, rendered onto all
161
205
  // three surfaces (run-start event, summary ## Environment, CLI header) so
@@ -359,6 +403,7 @@ async function runWithMemory(opts: RunOptions, memory: SynapMemory | null): Prom
359
403
  log(`failing over ${provider.name} → ${fallback.name}`);
360
404
  await transcript.event('provider-failover', { from: provider.name, to: fallback.name });
361
405
  provider = fallback;
406
+ providers.add(provider);
362
407
  turn--;
363
408
  continue;
364
409
  }
@@ -0,0 +1,367 @@
1
+ import { mkdtemp, rm } from 'node:fs/promises';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import type { ChatOpts, Msg, Provider, ToolCall, ToolSchema, Turn } from '../types.js';
5
+
6
+ /**
7
+ * Saved-login provider: drives Claude Code through the Claude Agent SDK
8
+ * (`@anthropic-ai/claude-agent-sdk`) and reuses its saved login (the
9
+ * `CLAUDE_CODE_OAUTH_TOKEN` from `claude setup-token`, or a logged-in CLI), so a
10
+ * Claude subscription user runs copperhead with no ANTHROPIC_API_KEY. What is
11
+ * reused is the login, not necessarily a separately-installed `claude` binary:
12
+ * the SDK ships its own. See the `add-claude-code-provider` change (D1–D6).
13
+ *
14
+ * It is a REASONING-ONLY backend. The Agent SDK is built to run its own
15
+ * autonomous multi-turn tool loop, which conflicts with copperhead's contract
16
+ * that `loop.ts` is the single driver and every mutation flows through the
17
+ * capability-filtered tools, obligations ledger, ERC/DRC gates, snapshot, and
18
+ * commit gate. So each `chat()` issues exactly ONE `query()` with no SDK tools
19
+ * registered and built-ins disabled, in an isolated cwd — the SDK executes
20
+ * nothing. copperhead's tools are advertised to the model as a text protocol;
21
+ * the model replies with a JSON tool-call block that we parse back into
22
+ * `Turn.toolCalls`. The spec-gated-in invariant stays structural: we advertise
23
+ * exactly the tools `availableTools(ctx)` returned for the turn.
24
+ *
25
+ * Auth stays external (D2): the constructor performs no API-key check and the
26
+ * provider never reads, copies, or logs the credential — the SDK resolves
27
+ * CLAUDE_CODE_OAUTH_TOKEN / the logged-in CLI itself.
28
+ */
29
+
30
+ /** Structural subset of the Agent SDK's `query` surface we depend on. Declared
31
+ * locally rather than `import type`d from the SDK on purpose: the SDK is an
32
+ * undeclared optional dependency (design D3), so importing its types would force
33
+ * it to be installed for `tsc` to pass — the exact coupling `codex.ts` now has.
34
+ * Naming the options here still compile-checks our option keys (e.g. `tools`),
35
+ * which is the safety the review asked for without the packaging cost. */
36
+ export type DenyResult = { behavior: 'deny'; message: string; interrupt?: boolean };
37
+ export type AllowResult = { behavior: 'allow'; updatedInput?: Record<string, unknown> };
38
+ export type CanUseToolLike = (
39
+ toolName: string,
40
+ input: Record<string, unknown>,
41
+ ) => Promise<DenyResult | AllowResult>;
42
+
43
+ export interface QueryOptions {
44
+ systemPrompt?: string;
45
+ model?: string;
46
+ /** `[]` disables all built-in tools (Agent SDK 0.3.x). */
47
+ tools?: string[];
48
+ disallowedTools?: string[];
49
+ /** Called before any tool executes; we deny everything (reasoning-only). */
50
+ canUseTool?: CanUseToolLike;
51
+ cwd?: string;
52
+ env?: Record<string, string | undefined>;
53
+ maxTurns?: number;
54
+ }
55
+ export interface QueryArgs {
56
+ prompt: string;
57
+ options?: QueryOptions;
58
+ }
59
+ export interface QueryMessage {
60
+ type: string;
61
+ subtype?: string;
62
+ message?: { content?: Array<{ type: string; text?: string }> };
63
+ usage?: { input_tokens?: number; output_tokens?: number };
64
+ }
65
+ export type QueryLike = (args: QueryArgs) => AsyncIterable<QueryMessage>;
66
+
67
+ /** Injectable dynamic import of the optional SDK. Exists so the
68
+ * missing-dependency path stays testable now that the SDK is a declared
69
+ * optional dependency (installed in a normal/CI install). The default keeps the
70
+ * specifier non-literal so tsc never resolves the optional dep at build time. */
71
+ export type ImportLike = (specifier: string) => Promise<unknown>;
72
+
73
+ /** Explicit deny list, belt-and-suspenders on top of the empty `tools` allowlist.
74
+ * `'*'` is the documented wildcard; the named entries stay for clarity and in
75
+ * case a given SDK build does not honor the wildcard. */
76
+ const DISALLOWED_BUILTINS = [
77
+ '*',
78
+ 'Bash',
79
+ 'Edit',
80
+ 'MultiEdit',
81
+ 'Write',
82
+ 'Read',
83
+ 'Glob',
84
+ 'Grep',
85
+ 'NotebookEdit',
86
+ 'WebFetch',
87
+ 'WebSearch',
88
+ 'Task',
89
+ 'TodoWrite',
90
+ ];
91
+
92
+ export class ClaudeCodeProvider implements Provider {
93
+ readonly name = 'claude-code';
94
+ private callSeq = 0;
95
+ private cwdPromise?: Promise<string>;
96
+
97
+ constructor(
98
+ private readonly model?: string,
99
+ private readonly injectedQuery?: QueryLike,
100
+ private readonly importSdk: ImportLike = (specifier) => import(specifier),
101
+ ) {}
102
+
103
+ // `_opts.maxTokens` is intentionally ignored: the Agent SDK drives the Claude
104
+ // Code subprocess and exposes no per-call max-tokens knob. loop.ts calls
105
+ // chat() without opts today; noted so a future opts pass is not a surprise.
106
+ async chat(messages: Msg[], tools: ToolSchema[], _opts: ChatOpts = {}): Promise<Turn> {
107
+ const query = await this.resolveQuery();
108
+
109
+ const system = messages
110
+ .filter((m) => m.role === 'system')
111
+ .map((m) => m.content)
112
+ .join('\n\n');
113
+ const systemPrompt = [system, renderToolProtocol(tools)].filter(Boolean).join('\n\n');
114
+ const prompt = renderConversation(messages);
115
+ const catalog = new Set(tools.map((t) => t.name));
116
+ const cwd = await this.ensureCwd();
117
+
118
+ let text: string | null = null;
119
+ let inputTokens = 0;
120
+ let outputTokens = 0;
121
+ try {
122
+ for await (const msg of query({
123
+ prompt,
124
+ options: {
125
+ systemPrompt,
126
+ ...(this.model ? { model: this.model } : {}),
127
+ // Layered "the SDK executes nothing" defense (D1/D5):
128
+ // 1. `tools: []` disables ALL built-in tools (Agent SDK 0.3.x docs:
129
+ // "[] (empty array) - Disable all built-in tools").
130
+ // 2. `disallowedTools` denies by name, with a wildcard, as a backstop.
131
+ // 3. `canUseTool` denies every tool BEFORE it runs — the permission
132
+ // analog to Codex's read-only sandbox — so even an unrecognized
133
+ // future tool cannot execute.
134
+ // 4. The tool_use tripwire below fails the run loudly if one is
135
+ // emitted anyway. Any single layer failing is caught by the next.
136
+ tools: [],
137
+ disallowedTools: DISALLOWED_BUILTINS,
138
+ canUseTool: async (toolName) => ({
139
+ behavior: 'deny',
140
+ message: `copperhead claude-code is reasoning-only; the SDK must not execute tools (blocked ${toolName}).`,
141
+ interrupt: true,
142
+ }),
143
+ cwd,
144
+ // The SDK's `env` REPLACES the subprocess environment entirely, so
145
+ // inherit process.env and strip the billed API keys: a claude-code run
146
+ // must use the saved login and never silently a paid ANTHROPIC_API_KEY
147
+ // / OPENAI_API_KEY, even when one is also set (D2).
148
+ env: { ...process.env, ANTHROPIC_API_KEY: undefined, OPENAI_API_KEY: undefined },
149
+ maxTurns: 1,
150
+ },
151
+ })) {
152
+ if (msg.type === 'assistant') {
153
+ for (const block of msg.message?.content ?? []) {
154
+ if (block.type === 'text' && block.text) {
155
+ text = (text ?? '') + block.text;
156
+ } else if (block.type === 'tool_use') {
157
+ // Load-bearing invariant (D1): the SDK must execute nothing, so it
158
+ // must never emit a tool_use block. If it does, `tools: []` was not
159
+ // honored — fail loudly rather than let an edit bypass copperhead's
160
+ // snapshot / verify / commit gates.
161
+ throw new Error(
162
+ 'claude-code: the Agent SDK emitted a tool_use block, but its tools are ' +
163
+ 'disabled — the reasoning-only invariant was violated (SDK option drift?). ' +
164
+ 'Refusing to continue.',
165
+ );
166
+ }
167
+ }
168
+ } else if (msg.type === 'result') {
169
+ if (typeof msg.usage?.input_tokens === 'number') inputTokens = msg.usage.input_tokens;
170
+ if (typeof msg.usage?.output_tokens === 'number') outputTokens = msg.usage.output_tokens;
171
+ }
172
+ }
173
+ } catch (err) {
174
+ // Auth failures get an actionable message (non-retryable); everything else
175
+ // — crucially a 429 — is re-thrown untouched so its status survives for
176
+ // withRetry/isRateLimit (D4). We never fall back to a keyed provider: the
177
+ // distinct `name` makes otherProvider() return null for us.
178
+ if (isAuthError(err)) throw new Error(authHint((err as Error).message));
179
+ throw err;
180
+ }
181
+
182
+ const parsed = parseToolCalls(text, () => `cc-${++this.callSeq}`, catalog);
183
+ return { text: parsed.text, toolCalls: parsed.toolCalls, usage: { inputTokens, outputTokens } };
184
+ }
185
+
186
+ /** Remove the scratch cwd. loop.ts calls this on every provider in a finally,
187
+ * so the one temp dir this instance created does not leak past the run. */
188
+ async close(): Promise<void> {
189
+ const pending = this.cwdPromise;
190
+ this.cwdPromise = undefined;
191
+ if (!pending) return;
192
+ try {
193
+ await rm(await pending, { recursive: true, force: true });
194
+ } catch {
195
+ // best effort: a leftover empty dir in the OS tmpdir is harmless
196
+ }
197
+ }
198
+
199
+ /** One isolated scratch cwd per provider instance, created once and reused
200
+ * across turns so a long run does not leak a temp dir per turn. Even with
201
+ * tools disabled this guarantees the SDK has no path into the repo (D5). */
202
+ private ensureCwd(): Promise<string> {
203
+ if (!this.cwdPromise) this.cwdPromise = mkdtemp(path.join(os.tmpdir(), 'copperhead-cc-'));
204
+ return this.cwdPromise;
205
+ }
206
+
207
+ private async resolveQuery(): Promise<QueryLike> {
208
+ if (this.injectedQuery) return this.injectedQuery;
209
+ let mod: { query?: QueryLike; default?: { query?: QueryLike } };
210
+ try {
211
+ // importSdk defaults to a non-literal `import()`, so tsc never resolves the
212
+ // optional dependency at build time and it may legitimately be absent (D3).
213
+ mod = (await this.importSdk('@anthropic-ai/claude-agent-sdk')) as {
214
+ query?: QueryLike;
215
+ default?: { query?: QueryLike };
216
+ };
217
+ } catch (err) {
218
+ // Only a genuinely-absent module gets the "install it" message; a present
219
+ // but broken install surfaces its real error rather than being mislabeled.
220
+ const code = (err as { code?: string }).code;
221
+ if (code === 'ERR_MODULE_NOT_FOUND' || code === 'MODULE_NOT_FOUND') {
222
+ throw new Error(
223
+ 'the claude-code provider needs the optional dependency @anthropic-ai/claude-agent-sdk; ' +
224
+ 'install it with `npm i @anthropic-ai/claude-agent-sdk`',
225
+ );
226
+ }
227
+ throw err;
228
+ }
229
+ const query = mod.query ?? mod.default?.query;
230
+ if (!query) {
231
+ throw new Error(
232
+ '@anthropic-ai/claude-agent-sdk did not export `query`; the installed version may be incompatible',
233
+ );
234
+ }
235
+ return query;
236
+ }
237
+ }
238
+
239
+ function renderToolProtocol(tools: ToolSchema[]): string {
240
+ if (!tools.length) return '';
241
+ const lines = [
242
+ '# Tool protocol',
243
+ '',
244
+ 'You are the reasoning half of a tool-driven workflow; you cannot run anything yourself.',
245
+ 'To take an action, reply with EXACTLY ONE JSON object and nothing else, wrapped in a',
246
+ '```json fenced code block:',
247
+ '',
248
+ '```json',
249
+ '{"tool": "<tool_name>", "args": { ... }}',
250
+ '```',
251
+ '',
252
+ 'Use only the tools listed below, with `args` matching the tool\'s JSON Schema. If you have',
253
+ 'no tool to call and only want to say something, reply with plain prose and no JSON block.',
254
+ '',
255
+ '## Available tools',
256
+ ];
257
+ for (const t of tools) {
258
+ lines.push(
259
+ '',
260
+ `### ${t.name}`,
261
+ t.description,
262
+ `Parameters (JSON Schema): ${JSON.stringify(t.parameters)}`,
263
+ );
264
+ }
265
+ return lines.join('\n');
266
+ }
267
+
268
+ function renderConversation(messages: Msg[]): string {
269
+ const idToName = new Map<string, string>();
270
+ const parts: string[] = [];
271
+ for (const m of messages) {
272
+ if (m.role === 'system') continue;
273
+ if (m.role === 'user') {
274
+ parts.push(`[user]\n${m.content}`);
275
+ } else if (m.role === 'assistant') {
276
+ if (m.content) parts.push(`[assistant]\n${m.content}`);
277
+ for (const call of m.toolCalls ?? []) {
278
+ idToName.set(call.id, call.name);
279
+ parts.push(
280
+ `[assistant tool call]\n\`\`\`json\n${JSON.stringify({ tool: call.name, args: call.args })}\n\`\`\``,
281
+ );
282
+ }
283
+ } else {
284
+ const name = idToName.get(m.toolCallId) ?? m.toolCallId;
285
+ parts.push(`[result of ${name}]\n${m.content}`);
286
+ }
287
+ }
288
+ return parts.join('\n\n');
289
+ }
290
+
291
+ interface Parsed {
292
+ text: string | null;
293
+ toolCalls: ToolCall[];
294
+ }
295
+
296
+ /**
297
+ * Extract tool-call JSON from the model's reply. Tolerant by design (D1):
298
+ * unparseable output is returned as plain text with no tool calls rather than
299
+ * throwing, so a non-conforming turn degrades to the loop's stall/nudge path.
300
+ * A parsed block only counts as a tool call when its name is in the current
301
+ * turn's catalog (`availableTools(ctx)`): a hallucinated or locked tool name is
302
+ * left as prose so the loop nudges, rather than dispatching a bogus call.
303
+ */
304
+ function parseToolCalls(text: string | null, nextId: () => string, catalog: Set<string>): Parsed {
305
+ if (!text) return { text: null, toolCalls: [] };
306
+ const toolCalls: ToolCall[] = [];
307
+ let prose = text;
308
+
309
+ const fence = /```(?:json)?\s*([\s\S]*?)```/gi;
310
+ let match: RegExpExecArray | null;
311
+ const consumed: string[] = [];
312
+ while ((match = fence.exec(text)) !== null) {
313
+ const call = toToolCall(match[1], nextId, catalog);
314
+ if (call) {
315
+ toolCalls.push(call);
316
+ consumed.push(match[0]);
317
+ }
318
+ }
319
+ for (const block of consumed) prose = prose.replace(block, '');
320
+
321
+ // No fenced tool block: maybe the whole reply is a bare JSON object.
322
+ if (!toolCalls.length) {
323
+ const call = toToolCall(text, nextId, catalog);
324
+ if (call) return { text: null, toolCalls: [call] };
325
+ }
326
+
327
+ const trimmed = prose.trim();
328
+ return { text: trimmed.length ? trimmed : null, toolCalls };
329
+ }
330
+
331
+ function toToolCall(raw: string | undefined, nextId: () => string, catalog: Set<string>): ToolCall | null {
332
+ if (!raw) return null;
333
+ let obj: unknown;
334
+ try {
335
+ obj = JSON.parse(raw.trim());
336
+ } catch {
337
+ return null;
338
+ }
339
+ if (!obj || typeof obj !== 'object') return null;
340
+ const rec = obj as Record<string, unknown>;
341
+ if (typeof rec.tool !== 'string') return null;
342
+ // Only accept names the turn actually advertised. An empty catalog means the
343
+ // turn offered no tools, so nothing parses as a call.
344
+ if (!catalog.has(rec.tool)) return null;
345
+ const args = rec.args && typeof rec.args === 'object' ? (rec.args as Record<string, unknown>) : {};
346
+ return { id: nextId(), name: rec.tool, args };
347
+ }
348
+
349
+ function isAuthError(err: unknown): boolean {
350
+ const status = (err as { status?: number; statusCode?: number })?.status
351
+ ?? (err as { statusCode?: number })?.statusCode;
352
+ // A present status is authoritative: only 401/403 are auth failures, so a 429
353
+ // (or anything else) is NOT treated as auth and is re-thrown untouched — its
354
+ // status must survive for withRetry/isRateLimit even if the message mentions
355
+ // "oauth token". Only when there is no status do we fall back to a narrow
356
+ // message heuristic.
357
+ if (typeof status === 'number') return status === 401 || status === 403;
358
+ const m = ((err as Error)?.message ?? '').toLowerCase();
359
+ return /unauthenticat|unauthoriz|not logged in|please log in|invalid api key|oauth token|setup-token/.test(m);
360
+ }
361
+
362
+ function authHint(detail: string): string {
363
+ return (
364
+ 'claude-code is not authenticated: log in to Claude Code, or run `claude setup-token` and ' +
365
+ `set CLAUDE_CODE_OAUTH_TOKEN. copperhead never reads your credential itself (original error: ${detail})`
366
+ );
367
+ }