copperhead 0.7.0 → 0.8.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 (79) hide show
  1. package/README.md +36 -4
  2. package/dist/agent/animate.js +76 -0
  3. package/dist/agent/animate.js.map +1 -0
  4. package/dist/agent/box.js +89 -0
  5. package/dist/agent/box.js.map +1 -0
  6. package/dist/agent/dock-renderer.js +173 -0
  7. package/dist/agent/dock-renderer.js.map +1 -0
  8. package/dist/agent/logo.js +21 -0
  9. package/dist/agent/logo.js.map +1 -0
  10. package/dist/agent/loop.js +15 -5
  11. package/dist/agent/loop.js.map +1 -1
  12. package/dist/agent/providers/claude-code.js +1 -212
  13. package/dist/agent/providers/claude-code.js.map +1 -1
  14. package/dist/agent/providers/cursor.js +317 -0
  15. package/dist/agent/providers/cursor.js.map +1 -0
  16. package/dist/agent/providers/tool-protocol.js +205 -0
  17. package/dist/agent/providers/tool-protocol.js.map +1 -0
  18. package/dist/agent/render.js +32 -15
  19. package/dist/agent/render.js.map +1 -1
  20. package/dist/agent/runmeta.js +4 -5
  21. package/dist/agent/runmeta.js.map +1 -1
  22. package/dist/agent/theme.js +84 -0
  23. package/dist/agent/theme.js.map +1 -0
  24. package/dist/cli.js +134 -13
  25. package/dist/cli.js.map +1 -1
  26. package/dist/commands/create.js +41 -32
  27. package/dist/commands/create.js.map +1 -1
  28. package/dist/commands/demo.js +146 -0
  29. package/dist/commands/demo.js.map +1 -0
  30. package/dist/commands/doctor.js +240 -0
  31. package/dist/commands/doctor.js.map +1 -0
  32. package/dist/commands/repl-inspect.js +342 -0
  33. package/dist/commands/repl-inspect.js.map +1 -0
  34. package/dist/commands/repl.js +618 -0
  35. package/dist/commands/repl.js.map +1 -0
  36. package/dist/config.js +5 -2
  37. package/dist/config.js.map +1 -1
  38. package/dist/kicad/cli.js +126 -6
  39. package/dist/kicad/cli.js.map +1 -1
  40. package/dist/util/cli-args.js +35 -0
  41. package/dist/util/cli-args.js.map +1 -0
  42. package/dist/util/dock.js +155 -0
  43. package/dist/util/dock.js.map +1 -0
  44. package/dist/util/git.js +129 -4
  45. package/dist/util/git.js.map +1 -1
  46. package/dist/util/live-prompt.js +542 -0
  47. package/dist/util/live-prompt.js.map +1 -0
  48. package/dist/util/paths.js +9 -0
  49. package/dist/util/paths.js.map +1 -1
  50. package/dist/util/select.js +172 -0
  51. package/dist/util/select.js.map +1 -0
  52. package/package.json +3 -2
  53. package/src/agent/animate.ts +90 -0
  54. package/src/agent/box.ts +99 -0
  55. package/src/agent/dock-renderer.ts +181 -0
  56. package/src/agent/logo.ts +23 -0
  57. package/src/agent/loop.ts +15 -5
  58. package/src/agent/providers/claude-code.ts +2 -216
  59. package/src/agent/providers/cursor.ts +364 -0
  60. package/src/agent/providers/tool-protocol.ts +212 -0
  61. package/src/agent/render.ts +33 -16
  62. package/src/agent/runmeta.ts +6 -7
  63. package/src/agent/theme.ts +91 -0
  64. package/src/cli.ts +139 -15
  65. package/src/commands/create.ts +81 -30
  66. package/src/commands/demo.ts +184 -0
  67. package/src/commands/doctor.ts +289 -0
  68. package/src/commands/repl-inspect.ts +353 -0
  69. package/src/commands/repl.ts +685 -0
  70. package/src/config.ts +6 -3
  71. package/src/kicad/cli.ts +132 -7
  72. package/src/layout/claude-ui-layout.md +72 -0
  73. package/src/layout/repl-ui-layout.md +139 -0
  74. package/src/util/cli-args.ts +42 -0
  75. package/src/util/dock.ts +161 -0
  76. package/src/util/git.ts +140 -4
  77. package/src/util/live-prompt.ts +595 -0
  78. package/src/util/paths.ts +10 -0
  79. package/src/util/select.ts +192 -0
package/README.md CHANGED
@@ -10,6 +10,10 @@
10
10
 
11
11
  **Cursor for circuit boards.** An AI agent that designs, documents, and validates real PCBs from a prompt, working directly on existing KiCad repositories.
12
12
 
13
+ <p align="center">
14
+ <img src="https://raw.githubusercontent.com/chouhanindustries/copperhead/main/assets/copperhead.gif" alt="copperhead agent shell demo" width="720">
15
+ </p>
16
+
13
17
  > **Status: early.** Phase 1 is implemented and the CLI runs. The [technical specification](openspec/specs/SPEC.md) is the source of truth; expect the surface to move before 1.0.
14
18
 
15
19
  Full documentation lives at [docs.copperhead.sh](https://docs.copperhead.sh).
@@ -29,19 +33,35 @@ It reads and edits real `.kicad_sch` / `.kicad_pcb` files (s-expression text), m
29
33
  npm install -g copperhead # or: npx copperhead check
30
34
  ```
31
35
 
36
+ ### Bootstrap script (macOS and Linux)
37
+
38
+ Prefer a one-command setup? [`install.sh`](install.sh) checks the prerequisites below, installs copperhead (globally via npm, or built from source when run inside a checkout), and verifies the result with `copperhead doctor`:
39
+
40
+ ```bash
41
+ ./install.sh # interactive: asks before installing anything
42
+ ./install.sh --yes # assume yes to the install prompts
43
+ ./install.sh --check-only # report what is missing, install nothing
44
+ ```
45
+
46
+ The script is conservative by design: it never runs `sudo` and never edits shell config; whenever it cannot act safely on its own it prints the exact command for you to run instead. Rerunning is safe: on a ready machine it installs nothing and exits 0.
47
+
32
48
  ### Requirements
33
49
 
34
50
  - Node.js ≥ 20
35
51
  - [KiCad](https://www.kicad.org/) ≥ 8 with `kicad-cli` on PATH
36
- - One model backend: a locally installed, ChatGPT-authenticated [Codex CLI](https://learn.chatgpt.com/docs/codex/cli), a logged-in Claude Code (see [Saved login](#saved-login-claude-code)), or `ANTHROPIC_API_KEY`/`OPENAI_API_KEY` in the environment. `check` never calls an LLM.
52
+ - One model backend: a locally installed, ChatGPT-authenticated [Codex CLI](https://learn.chatgpt.com/docs/codex/cli), a logged-in [Cursor Agent CLI](#saved-login-cursor-agent) (`agent login`), logged-in Claude Code (see [Saved login](#saved-login-claude-code)), or `ANTHROPIC_API_KEY`/`OPENAI_API_KEY` in the environment. `check` never calls an LLM.
37
53
 
38
54
  ## Quick start
39
55
 
40
56
  In an existing KiCad repository:
41
57
 
42
58
  ```bash
43
- export ANTHROPIC_API_KEY=... # or OPENAI_API_KEY
59
+ export ANTHROPIC_API_KEY=... # or OPENAI_API_KEY; optional: with no key, `copperhead` offers a model picker
44
60
  copperhead init # scaffold docs/ from the schematic; idempotent
61
+ copperhead # interactive agent shell (Claude Code–style REPL)
62
+ copperhead demo --tour # what the agent does (no LLM)
63
+ copperhead demo --model cursor # full USB-C breakout create pipeline
64
+ # or one-shot:
45
65
  copperhead do "add reverse-polarity protection on VIN"
46
66
  copperhead check # ERC + DRC + doc drift; no LLM, CI-safe
47
67
  ```
@@ -88,14 +108,26 @@ Spec-gated in, verification-gated out: the design can't drift from its requireme
88
108
  copperhead init [--path hardware/] # scaffold docs/ from an existing schematic; idempotent
89
109
  copperhead do "<change request>" # the core loop: propose, edit, verify, propagate, commit
90
110
  copperhead check # ERC + DRC + doc-drift + spec validation; no LLM calls (alias: verify)
111
+ copperhead doctor # env preflight: kicad-cli, git, node, provider credential; no LLM/network
91
112
  copperhead sync [--dry-run] # verify the whole design state, resolve drift
92
113
  copperhead create --brief brief.md # brief → full output package
93
114
  copperhead export bom --supplier jlcpcb # supplier-ready ordering file from docs/BOM.md
94
115
  ```
95
116
 
96
- Global flags: `--repo <path>` (default: cwd) and `--json` for machine-readable output. `do` and `create` take `--model` and `--interactive`; `do` also takes `--dry-run`, `--max-turns`, and `--allow-dirty`.
117
+ Global flags: `--repo <path>` (default: cwd) and `--json` for machine-readable output. `--model` is available on `do`, `sync`, `create`, and `doctor`; `--interactive` only on `do` and `create`; `do` also takes `--dry-run`, `--max-turns`, and `--allow-dirty`.
118
+
119
+ `--model` accepts `gpt-5` (OpenAI), `claude` / `claude-<id>` (Anthropic API), `claude-code` / `claude-code:<id>` (Claude Code, saved login), `cursor` / `cursor:<id>` (Cursor Agent CLI, saved login), and `codex` / `codex:<id>` (Codex CLI, saved login). Routing is by prefix; `claude-code` is matched before the `claude` prefix.
120
+
121
+ ### Saved login (Cursor Agent)
122
+
123
+ `--model cursor` drives the Cursor Agent CLI with your saved login from `agent login`, so you can run copperhead with **no `OPENAI_API_KEY` or `ANTHROPIC_API_KEY`**. Cursor is used purely as a reasoning backend in plan mode: the agent loop, safety gates, and every file edit stay inside copperhead.
124
+
125
+ ```bash
126
+ agent status
127
+ copperhead do "add reverse-polarity protection on VIN" --model cursor
128
+ ```
97
129
 
98
- `--model` accepts `gpt-5` (OpenAI), `claude` / `claude-<id>` (Anthropic API), `claude-code` / `claude-code:<id>` (Claude Code, saved login), and `codex` / `codex:<id>` (Codex CLI, saved login). Routing is by prefix; `claude-code` is matched before the `claude` prefix.
130
+ If `agent` is not on `PATH`, set `COPPERHEAD_CURSOR_PATH` to the CLI binary (also available as `cursor-agent` on some installs). Cursor's JSON schema does not report token usage, so run summaries show 0 tokens for `--model cursor`.
99
131
 
100
132
  ### Saved login (Claude Code)
101
133
 
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Subtle TTY motion for attended copperhead chrome. Disabled when color is
3
+ * off, stdout is not a TTY, CI=1, or COPPERHEAD_NO_ANIM=1.
4
+ */
5
+ import { copper, isColorEnabled } from './theme.js';
6
+ export function prefersAnimation() {
7
+ return (isColorEnabled() &&
8
+ Boolean(process.stdout.isTTY) &&
9
+ !process.env.CI &&
10
+ !process.env.COPPERHEAD_NO_ANIM);
11
+ }
12
+ export function sleep(ms) {
13
+ return new Promise((r) => setTimeout(r, ms));
14
+ }
15
+ const HIDE = '\x1b[?25l';
16
+ const SHOW = '\x1b[?25h';
17
+ /** Growing fiducial frames (3 rows), converging on the block mark. Exported for tests. */
18
+ export function fiducialBootFrames() {
19
+ return [
20
+ [' ', ' ██ ', ' '],
21
+ [' ▗▄▄▖ ', ' █ █ ', ' ▝▀▀▘ '],
22
+ [' ▄▟▙▄ ', ' ██ ██ ', ' ▀▜▛▀ '],
23
+ [' ▄▟▙▄ ', ' ███ ███', ' ▀▜▛▀ '],
24
+ ].map((frame) => frame.map((row) => copper(row)));
25
+ }
26
+ /**
27
+ * Pulse the fiducial mark in place over the already-rendered banner (rows
28
+ * `topRow..topRow+2`, column 1). The full screen loads first; this animates
29
+ * after, two grow cycles ending on the final mark. Frames are constant
30
+ * width so they overwrite each other without clearing the banner text
31
+ * beside them. Deliberately avoids DECSC/DECRC: the dock owns that slot for
32
+ * its caret-parking protocol; the caller repaints the dock afterwards to
33
+ * restore the caret.
34
+ */
35
+ export async function animateMarkAt(out, topRow, opts) {
36
+ if (!prefersAnimation())
37
+ return;
38
+ const frames = fiducialBootFrames();
39
+ const frameMs = opts?.slow ? 110 : 45;
40
+ const holdMs = opts?.slow ? 220 : 90;
41
+ try {
42
+ for (let cycle = 0; cycle < 2; cycle++) {
43
+ for (const frame of frames) {
44
+ let seq = HIDE;
45
+ frame.forEach((row, i) => {
46
+ seq += `\x1b[${topRow + i};1H${row}`;
47
+ });
48
+ out.write(seq);
49
+ await sleep(frameMs);
50
+ }
51
+ }
52
+ await sleep(holdMs);
53
+ }
54
+ finally {
55
+ // Never leave the terminal with a hidden cursor, even if a write or
56
+ // sleep throws mid-animation.
57
+ out.write(SHOW);
58
+ }
59
+ }
60
+ /** Copper horizontal rule (PCB-trace vibe). */
61
+ export function traceRule(width = 32) {
62
+ return copper(' ' + '─'.repeat(Math.max(8, Math.min(width, 48))));
63
+ }
64
+ /** Cascade lines (help / demo sections). Instant when animation is off. */
65
+ export async function staggerWrite(lines, write, delayMs = 12) {
66
+ if (!prefersAnimation()) {
67
+ for (const line of lines)
68
+ write(line);
69
+ return;
70
+ }
71
+ for (const line of lines) {
72
+ write(line);
73
+ await sleep(delayMs);
74
+ }
75
+ }
76
+ //# sourceMappingURL=animate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"animate.js","sourceRoot":"","sources":["../../src/agent/animate.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Box-drawing primitives for the bottom-docked REPL chrome: input box,
3
+ * status bar, and notice callouts. All width math ignores SGR sequences so
4
+ * colored segments never break padding or wrap accounting.
5
+ */
6
+ import { copper, err, isColorEnabled, ruleDim, warn } from './theme.js';
7
+ const SGR = /\x1b\[[0-9;]*m/g;
8
+ /** Visible width ignoring SGR color codes. */
9
+ export function visibleWidth(s) {
10
+ return s.replace(SGR, '').length;
11
+ }
12
+ /** Inverse video (synthetic caret / hover); no-op when color is off. */
13
+ export function inverse(s) {
14
+ if (!isColorEnabled() || s === '')
15
+ return s;
16
+ return `\x1b[7m${s}\x1b[0m`;
17
+ }
18
+ /**
19
+ * Wrap styled spans into lines no wider than `width`. Styling is applied per
20
+ * slice, so a span can be cut at a wrap boundary without leaking SGR state.
21
+ */
22
+ export function wrapSpans(spans, width) {
23
+ const w = Math.max(1, width);
24
+ const lines = [];
25
+ let cur = '';
26
+ let curLen = 0;
27
+ for (const span of spans) {
28
+ let text = span.text;
29
+ while (text.length) {
30
+ const take = text.slice(0, w - curLen);
31
+ cur += span.paint ? span.paint(take) : take;
32
+ curLen += take.length;
33
+ text = text.slice(take.length);
34
+ if (curLen >= w) {
35
+ lines.push(cur);
36
+ cur = '';
37
+ curLen = 0;
38
+ }
39
+ }
40
+ }
41
+ if (cur !== '' || !lines.length)
42
+ lines.push(cur);
43
+ return lines;
44
+ }
45
+ /** Full-width horizontal rule (the input-area separators), #888888. */
46
+ export function rule(width) {
47
+ return ruleDim('─'.repeat(Math.max(1, width)));
48
+ }
49
+ /** Truncate to a visible width, keeping SGR sequences intact and closed. */
50
+ export function truncateVisible(s, width) {
51
+ if (visibleWidth(s) <= width)
52
+ return s;
53
+ let out = '';
54
+ let vis = 0;
55
+ let i = 0;
56
+ let hadSgr = false;
57
+ while (i < s.length && vis < width) {
58
+ if (s[i] === '\x1b') {
59
+ const m = /^\x1b\[[0-9;]*m/.exec(s.slice(i));
60
+ if (m) {
61
+ out += m[0];
62
+ hadSgr = true;
63
+ i += m[0].length;
64
+ continue;
65
+ }
66
+ }
67
+ out += s[i];
68
+ vis++;
69
+ i++;
70
+ }
71
+ return out + (hadSgr ? '\x1b[0m' : '');
72
+ }
73
+ /** One status line with a left- and a right-justified half. */
74
+ export function statusBar(left, right, width) {
75
+ const w = Math.max(8, width);
76
+ const l = visibleWidth(left);
77
+ const r = visibleWidth(right);
78
+ // Too narrow for both: the hints matter more than the meta.
79
+ if (l + r + 2 > w)
80
+ return truncateVisible(left, w);
81
+ return left + ' '.repeat(w - l - r) + right;
82
+ }
83
+ /** Notice block: thin colored left bar + title + dim body (banner callouts). */
84
+ export function callout(kind, title, body) {
85
+ const paintBar = kind === 'err' ? err : kind === 'warn' ? warn : copper;
86
+ const bar = paintBar(' ▎');
87
+ return [`${bar} ${paintBar(title)}`, ...body.map((b) => `${bar} ${b}`)];
88
+ }
89
+ //# sourceMappingURL=box.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"box.js","sourceRoot":"","sources":["../../src/agent/box.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,173 @@
1
+ /**
2
+ * REPL-session renderer: the single owner of the bottom dock during agent
3
+ * turns. Durable output (tool lines, turn markers, the outcome) flows into
4
+ * the content region through `emit`, which the REPL also records as
5
+ * scrollable history; the live observability line (spinner, turn, tokens,
6
+ * elapsed, busy text) is painted inside the dock, pinned to the bottom of
7
+ * the screen no matter how much output scrolls above it.
8
+ */
9
+ import { rule, statusBar } from './box.js';
10
+ import { copper, dim, styleOutcome, toolLine, warn } from './theme.js';
11
+ import { fmtDuration, fmtTokens, turnMarker } from './render.js';
12
+ const FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
13
+ /** Claude Code-style working words, board-shop edition. One per turn. */
14
+ const WORKING = [
15
+ 'Routing',
16
+ 'Etching',
17
+ 'Reflowing',
18
+ 'Soldering',
19
+ 'Drilling',
20
+ 'Plating',
21
+ 'Probing',
22
+ 'Fluxing',
23
+ 'Tinning',
24
+ 'Laminating',
25
+ 'Silkscreening',
26
+ 'Panelizing',
27
+ ];
28
+ /** Fixed word-slot width: longest word plus the static dots. */
29
+ const WORD_SLOT = Math.max(...WORKING.map((w) => w.length)) + 3;
30
+ export class DockRenderer {
31
+ dock;
32
+ emit;
33
+ chrome;
34
+ turn = 0;
35
+ maxTurns = 0;
36
+ tokensIn = 0;
37
+ tokensOut = 0;
38
+ streamedChars = 0;
39
+ busy = null;
40
+ frame = 0;
41
+ runSeed = 0;
42
+ startMs = Date.now();
43
+ timer = null;
44
+ constructor(dock,
45
+ /** Durable line sink: content region + session history. */
46
+ emit,
47
+ /** Dock chrome around the status row (meta right, bottom hints). */
48
+ chrome) {
49
+ this.dock = dock;
50
+ this.emit = emit;
51
+ this.chrome = chrome;
52
+ }
53
+ log(line) {
54
+ this.emit(line);
55
+ }
56
+ toolResult(name, firstLine) {
57
+ this.emit(toolLine(name, firstLine));
58
+ }
59
+ turnStartMs = Date.now();
60
+ turnStart(turn, maxTurns, tokensIn, tokensOut) {
61
+ if (turn === 1) {
62
+ this.startMs = Date.now();
63
+ this.runSeed++;
64
+ }
65
+ this.turnStartMs = Date.now();
66
+ this.turn = turn;
67
+ this.maxTurns = maxTurns;
68
+ this.tokensIn = tokensIn;
69
+ this.tokensOut = tokensOut;
70
+ this.emit(dim(turnMarker(turn, maxTurns, tokensIn, tokensOut)));
71
+ this.arm();
72
+ this.paint();
73
+ }
74
+ status(text) {
75
+ this.busy = text;
76
+ if (text === null)
77
+ this.streamedChars = 0;
78
+ this.paint();
79
+ }
80
+ heartbeat(info) {
81
+ this.streamedChars = info.streamedChars;
82
+ this.paint();
83
+ }
84
+ finish(line) {
85
+ this.disarm();
86
+ this.busy = null;
87
+ this.emit(styleOutcome(line));
88
+ // The next prompt's renderDock() takes the dock back over.
89
+ }
90
+ /** Currently displayed working word; morphs letter by letter on change. */
91
+ shownWord = '';
92
+ targetWord = '';
93
+ /** -1 = settled; otherwise progress through erase-then-type transition. */
94
+ morph = -1;
95
+ arm() {
96
+ if (this.timer)
97
+ return;
98
+ this.timer = setInterval(() => {
99
+ this.frame++;
100
+ // Sweep three positions per tick: a full word crossfade in under a second.
101
+ if (this.morph >= 0)
102
+ this.morph += 3;
103
+ this.paint();
104
+ }, 120);
105
+ this.timer.unref?.();
106
+ }
107
+ disarm() {
108
+ if (this.timer)
109
+ clearInterval(this.timer);
110
+ this.timer = null;
111
+ }
112
+ /** Pinned observability row painted inside the dock (same layout as the prompt). */
113
+ paint() {
114
+ const w = Math.max(10, this.dock.cols() - 1);
115
+ const spinner = copper(FRAMES[this.frame % FRAMES.length]);
116
+ // Claude Code-style working word, board-shop themed: rotates every ~6s
117
+ // while a turn runs, with a shimmering highlight sweeping the letters.
118
+ // A word change morphs letter by letter: the old word is erased into
119
+ // `_` slots left to right, then the new word types over them.
120
+ const wordIdx = (this.runSeed + this.turn + Math.floor((Date.now() - this.turnStartMs) / 6000)) %
121
+ WORKING.length;
122
+ const target = WORKING[wordIdx];
123
+ if (this.shownWord === '')
124
+ this.shownWord = target;
125
+ if (target !== this.targetWord) {
126
+ this.targetWord = target;
127
+ if (target !== this.shownWord)
128
+ this.morph = 0;
129
+ }
130
+ let text = `${this.shownWord}...`;
131
+ if (this.morph >= 0) {
132
+ // Single left-to-right sweep over the whole dotted string (dots
133
+ // included): each position flips old char -> `_` -> new char, so the
134
+ // words cross-fade character by character.
135
+ const oldS = `${this.shownWord}...`;
136
+ const newS = `${this.targetWord}...`;
137
+ const width = Math.max(oldS.length, newS.length);
138
+ const k = this.morph;
139
+ if (k >= width) {
140
+ this.shownWord = this.targetWord;
141
+ this.morph = -1;
142
+ text = `${this.shownWord}...`;
143
+ }
144
+ else {
145
+ let out = '';
146
+ for (let i = 0; i < width; i++) {
147
+ out += i < k ? (newS[i] ?? ' ') : i === k ? '_' : (oldS[i] ?? ' ');
148
+ }
149
+ text = out.trimEnd();
150
+ }
151
+ }
152
+ // Fixed-width slot (longest word + dots) so the stats after it never
153
+ // shift; dots and morph slots share the word's copper.
154
+ const word = copper(text.padEnd(WORD_SLOT));
155
+ const parts = [
156
+ dim(`turn ${this.turn}/${this.maxTurns}`),
157
+ dim(`${fmtTokens(this.tokensIn)} in / ${fmtTokens(this.tokensOut)} out`),
158
+ dim(fmtDuration(Date.now() - this.startMs)),
159
+ ];
160
+ if (this.busy) {
161
+ parts.push(warn(this.streamedChars ? `${this.busy} ~${fmtTokens(this.streamedChars)} ch` : this.busy));
162
+ }
163
+ const { meta, hints } = this.chrome();
164
+ this.dock.set([
165
+ ...(meta ? [statusBar('', `${meta} `, w)] : []),
166
+ rule(w),
167
+ statusBar(`${spinner} ${word}`, `${parts.join(dim(' · '))} `, w),
168
+ rule(w),
169
+ ...(hints ? [statusBar(` ${hints}`, '', w)] : []),
170
+ ]);
171
+ }
172
+ }
173
+ //# sourceMappingURL=dock-renderer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dock-renderer.js","sourceRoot":"","sources":["../../src/agent/dock-renderer.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Terminal block-art mark derived from the website logo
3
+ * (docs/public/favicon.svg and docs.copperhead.sh): a via with a square
4
+ * drilled hole and long copper tracks routed out of both sides, copper
5
+ * #b87333 on dark. (`scripts/gen-logo.mjs` renders the exact favicon
6
+ * geometry at any size for reference.)
7
+ */
8
+ import { copper } from './theme.js';
9
+ /** 3-row quadrant-block via with long tracks; rows are equal width. */
10
+ export function fiducialMark() {
11
+ return [
12
+ ' ▄▟▙▄ ',
13
+ ' ███ ███',
14
+ ' ▀▜▛▀ ',
15
+ ];
16
+ }
17
+ /** The mark painted in brand copper. */
18
+ export function fiducialLines() {
19
+ return fiducialMark().map((line) => copper(line));
20
+ }
21
+ //# sourceMappingURL=logo.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logo.js","sourceRoot":"","sources":["../../src/agent/logo.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AAEpC,uEAAuE;AACvE,MAAM,UAAU,YAAY;IAC1B,OAAO;QACL,WAAW;QACX,WAAW;QACX,WAAW;KACZ,CAAC;AACJ,CAAC;AAED,wCAAwC;AACxC,MAAM,UAAU,aAAa;IAC3B,OAAO,YAAY,EAAE,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC;AACpD,CAAC"}
@@ -10,6 +10,7 @@ import { loadConfig, CONFIG_DIR } from '../config.js';
10
10
  import { Transcript } from './transcript.js';
11
11
  import { collectRunMeta, renderCliHeader } from './runmeta.js';
12
12
  import { plainRenderer, fmtDuration, fmtTokens } from './render.js';
13
+ import { styleHeaderLines } from './theme.js';
13
14
  import { ObligationsLedger } from './ledger.js';
14
15
  import { gitPreflight, isDirty, snapshot, restore, commitAll, changedFiles, preserveFailedRun } from '../util/git.js';
15
16
  import { withRetry, isRateLimit, sessionLimit } from '../util/retry.js';
@@ -19,6 +20,7 @@ import { OpenAIProvider } from './providers/openai.js';
19
20
  import { AnthropicProvider } from './providers/anthropic.js';
20
21
  import { CodexProvider } from './providers/codex.js';
21
22
  import { ClaudeCodeProvider } from './providers/claude-code.js';
23
+ import { CursorProvider } from './providers/cursor.js';
22
24
  import { openSynapMemory } from '../memory/synap.js';
23
25
  export async function makeProvider(model, sessionResume = false) {
24
26
  if (model === 'codex' || model.startsWith('codex:')) {
@@ -46,6 +48,14 @@ export async function makeProvider(model, sessionResume = false) {
46
48
  }
47
49
  return new ClaudeCodeProvider(claudeCodeModel, undefined, undefined, sessionResume);
48
50
  }
51
+ // Saved-login Cursor Agent CLI (`agent login`). Matched as its own namespace.
52
+ if (model === 'cursor' || model.startsWith('cursor:')) {
53
+ const cursorModel = model.startsWith('cursor:') ? model.slice('cursor:'.length) : undefined;
54
+ if (cursorModel === '') {
55
+ throw new Error('cursor model override cannot be empty; use "cursor" or "cursor:<model-id>"');
56
+ }
57
+ return new CursorProvider(cursorModel, undefined, sessionResume);
58
+ }
49
59
  if (model === 'claude' || model.startsWith('claude')) {
50
60
  return new AnthropicProvider(model === 'claude' ? undefined : model);
51
61
  }
@@ -53,7 +63,7 @@ export async function makeProvider(model, sessionResume = false) {
53
63
  }
54
64
  function otherProvider(current) {
55
65
  // Only the two keyed providers fail over to each other. A rate-limited
56
- // 'claude-code' run returns null here (no silent fallback to a paid API).
66
+ // 'claude-code' or 'cursor' run returns null here (no silent fallback to a paid API).
57
67
  if (current.name === 'openai' && process.env.ANTHROPIC_API_KEY)
58
68
  return new AnthropicProvider();
59
69
  if (current.name === 'anthropic' && process.env.OPENAI_API_KEY)
@@ -141,9 +151,9 @@ async function runWithMemory(opts, memory, providers) {
141
151
  repairCycles: 0,
142
152
  finishRequest: null,
143
153
  };
144
- // Session resume for claude-code (1.1) is only correct when the response cache
145
- // is off: the cache replays turns a resumed session never saw. So enable it
146
- // only when the env flag is set AND config.llmCache is disabled — the same
154
+ // Session resume for claude-code / cursor is only correct when the response
155
+ // cache is off: the cache replays turns a resumed session never saw. So enable
156
+ // it only when the env flag is set AND config.llmCache is disabled — the same
147
157
  // condition under which we skip the CachingProvider wrap below.
148
158
  const sessionResume = process.env.COPPERHEAD_CC_SESSION_RESUME === '1' && !config.llmCache;
149
159
  let provider = opts.provider ?? (await makeProvider(opts.model, sessionResume));
@@ -173,7 +183,7 @@ async function runWithMemory(opts, memory, providers) {
173
183
  interactive: opts.interactive ?? false,
174
184
  input: opts.meta,
175
185
  });
176
- for (const line of renderCliHeader(meta))
186
+ for (const line of styleHeaderLines(renderCliHeader(meta)))
177
187
  log(line);
178
188
  // Revisit obligations deferred while their artifact didn't exist re-open now
179
189
  // if it does (must run before loadConstraints so the prompt sees the updated