copperhead 0.3.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 (81) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +87 -0
  3. package/dist/agent/filetools.js +118 -0
  4. package/dist/agent/filetools.js.map +1 -0
  5. package/dist/agent/ledger.js +43 -0
  6. package/dist/agent/ledger.js.map +1 -0
  7. package/dist/agent/loop.js +279 -0
  8. package/dist/agent/loop.js.map +1 -0
  9. package/dist/agent/prompts.js +47 -0
  10. package/dist/agent/prompts.js.map +1 -0
  11. package/dist/agent/providers/anthropic.js +78 -0
  12. package/dist/agent/providers/anthropic.js.map +1 -0
  13. package/dist/agent/providers/openai.js +74 -0
  14. package/dist/agent/providers/openai.js.map +1 -0
  15. package/dist/agent/tools.js +439 -0
  16. package/dist/agent/tools.js.map +1 -0
  17. package/dist/agent/transcript.js +60 -0
  18. package/dist/agent/transcript.js.map +1 -0
  19. package/dist/agent/types.js +2 -0
  20. package/dist/agent/types.js.map +1 -0
  21. package/dist/cli.js +185 -0
  22. package/dist/cli.js.map +1 -0
  23. package/dist/commands/check.js +64 -0
  24. package/dist/commands/check.js.map +1 -0
  25. package/dist/commands/create.js +91 -0
  26. package/dist/commands/create.js.map +1 -0
  27. package/dist/commands/sync.js +144 -0
  28. package/dist/commands/sync.js.map +1 -0
  29. package/dist/config.js +64 -0
  30. package/dist/config.js.map +1 -0
  31. package/dist/kicad/cli.js +91 -0
  32. package/dist/kicad/cli.js.map +1 -0
  33. package/dist/kicad/report.js +48 -0
  34. package/dist/kicad/report.js.map +1 -0
  35. package/dist/kicad/sexp.js +291 -0
  36. package/dist/kicad/sexp.js.map +1 -0
  37. package/dist/memory/constraints.js +43 -0
  38. package/dist/memory/constraints.js.map +1 -0
  39. package/dist/memory/drift.js +81 -0
  40. package/dist/memory/drift.js.map +1 -0
  41. package/dist/memory/scaffold.js +215 -0
  42. package/dist/memory/scaffold.js.map +1 -0
  43. package/dist/openspec/cli.js +31 -0
  44. package/dist/openspec/cli.js.map +1 -0
  45. package/dist/util/env.js +68 -0
  46. package/dist/util/env.js.map +1 -0
  47. package/dist/util/git.js +53 -0
  48. package/dist/util/git.js.map +1 -0
  49. package/dist/util/paths.js +25 -0
  50. package/dist/util/paths.js.map +1 -0
  51. package/dist/util/redact.js +21 -0
  52. package/dist/util/redact.js.map +1 -0
  53. package/dist/util/retry.js +27 -0
  54. package/dist/util/retry.js.map +1 -0
  55. package/package.json +73 -0
  56. package/src/agent/filetools.ts +136 -0
  57. package/src/agent/ledger.ts +71 -0
  58. package/src/agent/loop.ts +320 -0
  59. package/src/agent/prompts.ts +58 -0
  60. package/src/agent/providers/anthropic.ts +81 -0
  61. package/src/agent/providers/openai.ts +80 -0
  62. package/src/agent/tools.ts +481 -0
  63. package/src/agent/transcript.ts +78 -0
  64. package/src/agent/types.ts +32 -0
  65. package/src/cli.ts +188 -0
  66. package/src/commands/check.ts +85 -0
  67. package/src/commands/create.ts +125 -0
  68. package/src/commands/sync.ts +182 -0
  69. package/src/config.ts +77 -0
  70. package/src/kicad/cli.ts +106 -0
  71. package/src/kicad/report.ts +81 -0
  72. package/src/kicad/sexp.ts +327 -0
  73. package/src/memory/constraints.ts +73 -0
  74. package/src/memory/drift.ts +97 -0
  75. package/src/memory/scaffold.ts +241 -0
  76. package/src/openspec/cli.ts +43 -0
  77. package/src/util/env.ts +65 -0
  78. package/src/util/git.ts +63 -0
  79. package/src/util/paths.ts +25 -0
  80. package/src/util/redact.ts +20 -0
  81. package/src/util/retry.ts +33 -0
@@ -0,0 +1,215 @@
1
+ import { readFile, writeFile, mkdir, readdir, chmod } from 'node:fs/promises';
2
+ import { existsSync } from 'node:fs';
3
+ import { createHash } from 'node:crypto';
4
+ import path from 'node:path';
5
+ import { listSymbols, pinNets } from '../kicad/sexp.js';
6
+ import { configPath, loadConfig } from '../config.js';
7
+ export class InitError extends Error {
8
+ }
9
+ const sha = (s) => createHash('sha256').update(s).digest('hex');
10
+ async function findKicadFiles(searchRoot) {
11
+ let sch = null;
12
+ let pcb = null;
13
+ async function walk(dir) {
14
+ for (const entry of await readdir(dir, { withFileTypes: true })) {
15
+ if (entry.name.startsWith('.') || entry.name === 'node_modules')
16
+ continue;
17
+ const abs = path.join(dir, entry.name);
18
+ if (entry.isDirectory())
19
+ await walk(abs);
20
+ else if (entry.name.endsWith('.kicad_sch') && !sch)
21
+ sch = abs;
22
+ else if (entry.name.endsWith('.kicad_pcb') && !pcb)
23
+ pcb = abs;
24
+ }
25
+ }
26
+ await walk(searchRoot);
27
+ return { sch, pcb };
28
+ }
29
+ function bomTable(symbols) {
30
+ const rows = symbols.map((s) => `| ${s.ref} | ${s.value} | ${s.footprint} | UNVERIFIED | extracted from schematic by copperhead init |`);
31
+ return ['| Refdes | Value | Footprint | MPN | Rationale |', '|---|---|---|---|---|', ...rows].join('\n');
32
+ }
33
+ function pinoutTable(pins) {
34
+ const rows = pins.map((p) => `| ${p.ref} | ${p.pinNumber} | ${p.pinName} | ${p.net ?? ''} | |`);
35
+ return ['| Refdes | Pin | Name | Net | Notes |', '|---|---|---|---|---|', ...rows].join('\n');
36
+ }
37
+ function generateDocs(projectName, symbols, pins) {
38
+ const mcuRefs = new Set(symbols.filter((s) => s.ref.startsWith('U')).map((s) => s.ref));
39
+ const mcuPins = pins.filter((p) => mcuRefs.has(p.ref));
40
+ return {
41
+ 'SPEC.md': `# ${projectName} — Specification
42
+
43
+ What the device is, top-level constraints and budgets.
44
+
45
+ ## Budgets
46
+
47
+ <!-- Add hard budgets here; copperhead treats them as constraints, e.g. -->
48
+ <!-- - sleep_current_uA: 25 -->
49
+
50
+ ## Assumptions
51
+
52
+ <!-- Decisions the agent made without explicit direction are flagged ASSUMED here -->
53
+ `,
54
+ 'BOM.md': `# Bill of Materials
55
+
56
+ Every part: refdes, MPN, value, package, and WHY it was chosen. MPNs the agent
57
+ introduces without a datasheet check are flagged UNVERIFIED.
58
+
59
+ ${bomTable(symbols)}
60
+ `,
61
+ 'PINOUT.md': `# Pinout
62
+
63
+ Pin assignment extracted from the schematic. Check strapping/RTC notes before
64
+ reassigning pins.
65
+
66
+ ${pinoutTable(mcuPins.length ? mcuPins : pins)}
67
+ `,
68
+ 'SUBSYSTEMS.md': `# Subsystems
69
+
70
+ Per-sheet values and reasoning (regulator, charger, RF, ...).
71
+
72
+ ${[...new Set(symbols.map((s) => s.sheet))].map((sheet) => `## Sheet ${sheet}\n\n${symbols.filter((s) => s.sheet === sheet).map((s) => `- ${s.ref}: ${s.value}`).join('\n')}`).join('\n\n')}
73
+ `,
74
+ 'LAYOUT.md': `# Layout intent
75
+
76
+ Placement and routing intent: keepouts, pours, ESD placement.
77
+
78
+ ## Draft quality
79
+
80
+ <!-- copperhead writes an honest assessment here after any layout pass -->
81
+ `,
82
+ 'DECISIONS.md': `# Decision log
83
+
84
+ Append-only. Every non-trivial agent decision lands here: date, run id,
85
+ decision, rationale, and what it affects. Entries are never rewritten.
86
+ `,
87
+ 'CHANGELOG.md': `# Design changelog
88
+
89
+ Append-only, newest first. One entry per committed copperhead run.
90
+ `,
91
+ };
92
+ }
93
+ function copperheadReadme(config) {
94
+ return `# .copperhead — copperhead working memory
95
+
96
+ Generated by \`copperhead init\`; regenerated on re-runs (do not hand-edit).
97
+
98
+ ## config.json
99
+
100
+ - \`schematic\` / \`board\`: repo-relative paths to the KiCad files copperhead operates on (currently: ${config.schematic ?? 'none'} / ${config.board ?? 'none'})
101
+ - \`docs\`: design docs directory (docs-as-memory), default \`docs/\`
102
+ - \`model\`: default model (\`gpt-5\` or \`claude\`); overridden by \`--model\` and \`COPPERHEAD_MODEL\`
103
+ - \`maxTurns\`: agent loop turn budget per run (default 40)
104
+ - \`maxRepairCycles\`: ERC/DRC repair attempts before rollback (default 5)
105
+ - \`budgets\`: free-form hard constraints (e.g. \`"sleep_current_uA": 25\`); surfaced verbatim into every run's system prompt
106
+ - \`generatedHashes\`: content hashes of generated docs, used to detect hand edits on re-init
107
+
108
+ ## constraints.json
109
+
110
+ Machine-readable constraint registry. Each key maps to \`{ min/max/forbidden/value, source, affects[] }\`.
111
+ \`source\` points at the doc or spec that states the constraint; \`affects\` lists the
112
+ refdes/nets/zones to revisit when the constraint changes. Kept in sync with the
113
+ docs in the same tool turn (dual write).
114
+
115
+ ## runs/
116
+
117
+ One directory per \`do\`/\`create\`/\`sync\` run: \`transcript.jsonl\` (full audit
118
+ trail) and \`summary.md\` (human-readable: request, plan, files touched,
119
+ verification results, decisions, open obligations). Both redact secrets at
120
+ write time. This directory is gitignored.
121
+ `;
122
+ }
123
+ const HOOK_MARKER = '# installed by copperhead init';
124
+ async function installPreCommitHook(repoRoot) {
125
+ const hooksDir = path.join(repoRoot, '.git', 'hooks');
126
+ if (!existsSync(hooksDir))
127
+ return null;
128
+ const hookPath = path.join(hooksDir, 'pre-commit');
129
+ if (existsSync(hookPath)) {
130
+ const existing = await readFile(hookPath, 'utf8');
131
+ if (existing.includes(HOOK_MARKER))
132
+ return null; // idempotent
133
+ return null; // never clobber a user's own hook
134
+ }
135
+ const script = `#!/bin/sh
136
+ ${HOOK_MARKER}
137
+ # Hand edits that desync docs, constraints, or the schematic fail at commit time.
138
+ exec copperhead check
139
+ `;
140
+ await writeFile(hookPath, script, 'utf8');
141
+ await chmod(hookPath, 0o755);
142
+ return hookPath;
143
+ }
144
+ export async function runInit(opts) {
145
+ const { repoRoot } = opts;
146
+ const searchRoot = path.resolve(repoRoot, opts.searchPath ?? '.');
147
+ const { sch, pcb } = await findKicadFiles(searchRoot);
148
+ if (!sch) {
149
+ throw new InitError(`no .kicad_sch found under ${path.relative(repoRoot, searchRoot) || '.'} — point copperhead at a KiCad project (copperhead init --path <dir>)`);
150
+ }
151
+ const config = await loadConfig(repoRoot);
152
+ config.schematic = path.relative(repoRoot, sch);
153
+ config.board = pcb ? path.relative(repoRoot, pcb) : null;
154
+ const symbols = await listSymbols(sch);
155
+ const pins = await pinNets(sch);
156
+ const projectName = path.basename(sch, '.kicad_sch');
157
+ const docs = generateDocs(projectName, symbols, pins);
158
+ const docsDir = path.join(repoRoot, config.docs);
159
+ await mkdir(docsDir, { recursive: true });
160
+ const result = { created: [], skipped: [], refused: [] };
161
+ const hashes = { ...(config.generatedHashes ?? {}) };
162
+ const appendOnly = new Set(['DECISIONS.md', 'CHANGELOG.md']);
163
+ for (const [name, content] of Object.entries(docs)) {
164
+ const p = path.join(docsDir, name);
165
+ const rel = path.join(config.docs, name);
166
+ if (!existsSync(p)) {
167
+ await writeFile(p, content, 'utf8');
168
+ hashes[name] = sha(content);
169
+ result.created.push(rel);
170
+ continue;
171
+ }
172
+ if (appendOnly.has(name)) {
173
+ result.skipped.push(rel); // append-only docs are never regenerated
174
+ continue;
175
+ }
176
+ const current = await readFile(p, 'utf8');
177
+ if (hashes[name] === sha(current)) {
178
+ if (current === content) {
179
+ result.skipped.push(rel);
180
+ }
181
+ else if (opts.force) {
182
+ await writeFile(p, content, 'utf8');
183
+ hashes[name] = sha(content);
184
+ result.created.push(rel);
185
+ }
186
+ else {
187
+ // schematic changed since generation but doc untouched: safe to refresh
188
+ await writeFile(p, content, 'utf8');
189
+ hashes[name] = sha(content);
190
+ result.created.push(rel);
191
+ }
192
+ }
193
+ else if (opts.force) {
194
+ await writeFile(p, content, 'utf8');
195
+ hashes[name] = sha(content);
196
+ result.created.push(rel);
197
+ }
198
+ else {
199
+ result.refused.push(rel);
200
+ }
201
+ }
202
+ config.generatedHashes = hashes;
203
+ const cfgPath = configPath(repoRoot);
204
+ await mkdir(path.dirname(cfgPath), { recursive: true });
205
+ await writeFile(cfgPath, JSON.stringify(config, null, 2) + '\n', 'utf8');
206
+ // generated documentation, always refreshed (design D12)
207
+ await writeFile(path.join(repoRoot, '.copperhead', 'README.md'), copperheadReadme(config), 'utf8');
208
+ if (opts.installHooks !== false) {
209
+ const hook = await installPreCommitHook(repoRoot);
210
+ if (hook)
211
+ result.created.push(path.relative(repoRoot, hook));
212
+ }
213
+ return result;
214
+ }
215
+ //# sourceMappingURL=scaffold.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scaffold.js","sourceRoot":"","sources":["../../src/memory/scaffold.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AAC9E,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,WAAW,EAAE,OAAO,EAAqC,MAAM,kBAAkB,CAAC;AAC3F,OAAO,EAAE,UAAU,EAAE,UAAU,EAAyB,MAAM,cAAc,CAAC;AAE7E,MAAM,OAAO,SAAU,SAAQ,KAAK;CAAG;AAQvC,MAAM,GAAG,GAAG,CAAC,CAAS,EAAU,EAAE,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AAEhF,KAAK,UAAU,cAAc,CAAC,UAAkB;IAC9C,IAAI,GAAG,GAAkB,IAAI,CAAC;IAC9B,IAAI,GAAG,GAAkB,IAAI,CAAC;IAC9B,KAAK,UAAU,IAAI,CAAC,GAAW;QAC7B,KAAK,MAAM,KAAK,IAAI,MAAM,OAAO,CAAC,GAAG,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC;YAChE,IAAI,KAAK,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,KAAK,CAAC,IAAI,KAAK,cAAc;gBAAE,SAAS;YAC1E,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC;YACvC,IAAI,KAAK,CAAC,WAAW,EAAE;gBAAE,MAAM,IAAI,CAAC,GAAG,CAAC,CAAC;iBACpC,IAAI,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,YAAY,CAAC,IAAI,CAAC,GAAG;gBAAE,GAAG,GAAG,GAAG,CAAC;iBACzD,IAAI,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,YAAY,CAAC,IAAI,CAAC,GAAG;gBAAE,GAAG,GAAG,GAAG,CAAC;QAChE,CAAC;IACH,CAAC;IACD,MAAM,IAAI,CAAC,UAAU,CAAC,CAAC;IACvB,OAAO,EAAE,GAAG,EAAE,GAAG,EAAE,CAAC;AACtB,CAAC;AAED,SAAS,QAAQ,CAAC,OAA0B;IAC1C,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,CACtB,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,CAAC,GAAG,MAAM,CAAC,CAAC,KAAK,MAAM,CAAC,CAAC,SAAS,+DAA+D,CAC/G,CAAC;IACF,OAAO,CAAC,kDAAkD,EAAE,uBAAuB,EAAE,GAAG,IAAI,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAC3G,CAAC;AAED,SAAS,WAAW,CAAC,IAAc;IACjC,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,CAAC,GAAG,MAAM,CAAC,CAAC,SAAS,MAAM,CAAC,CAAC,OAAO,MAAM,CAAC,CAAC,GAAG,IAAI,EAAE,MAAM,CAAC,CAAC;IAChG,OAAO,CAAC,uCAAuC,EAAE,uBAAuB,EAAE,GAAG,IAAI,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAChG,CAAC;AAED,SAAS,YAAY,CACnB,WAAmB,EACnB,OAA0B,EAC1B,IAAc;IAEd,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;IACxF,MAAM,OAAO,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;IACvD,OAAO;QACL,SAAS,EAAE,KAAK,WAAW;;;;;;;;;;;;CAY9B;QACG,QAAQ,EAAE;;;;;EAKZ,QAAQ,CAAC,OAAO,CAAC;CAClB;QACG,WAAW,EAAE;;;;;EAKf,WAAW,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC;CAC7C;QACG,eAAe,EAAE;;;;EAInB,CAAC,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,YAAY,KAAK,OAAO,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,KAAK,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,CAAC,GAAG,KAAK,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC;CAC1L;QACG,WAAW,EAAE;;;;;;;CAOhB;QACG,cAAc,EAAE;;;;CAInB;QACG,cAAc,EAAE;;;CAGnB;KACE,CAAC;AACJ,CAAC;AAED,SAAS,gBAAgB,CAAC,MAAwB;IAChD,OAAO;;;;;;yGAMgG,MAAM,CAAC,SAAS,IAAI,MAAM,MAAM,MAAM,CAAC,KAAK,IAAI,MAAM;;;;;;;;;;;;;;;;;;;;;CAqB9J,CAAC;AACF,CAAC;AAED,MAAM,WAAW,GAAG,gCAAgC,CAAC;AAErD,KAAK,UAAU,oBAAoB,CAAC,QAAgB;IAClD,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC;IACtD,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC;QAAE,OAAO,IAAI,CAAC;IACvC,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,YAAY,CAAC,CAAC;IACnD,IAAI,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;QACzB,MAAM,QAAQ,GAAG,MAAM,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;QAClD,IAAI,QAAQ,CAAC,QAAQ,CAAC,WAAW,CAAC;YAAE,OAAO,IAAI,CAAC,CAAC,aAAa;QAC9D,OAAO,IAAI,CAAC,CAAC,kCAAkC;IACjD,CAAC;IACD,MAAM,MAAM,GAAG;EACf,WAAW;;;CAGZ,CAAC;IACA,MAAM,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;IAC1C,MAAM,KAAK,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC;IAC7B,OAAO,QAAQ,CAAC;AAClB,CAAC;AASD,MAAM,CAAC,KAAK,UAAU,OAAO,CAAC,IAAiB;IAC7C,MAAM,EAAE,QAAQ,EAAE,GAAG,IAAI,CAAC;IAC1B,MAAM,UAAU,GAAG,IAAI,CAAC,OAAO,CAAC,QAAQ,EAAE,IAAI,CAAC,UAAU,IAAI,GAAG,CAAC,CAAC;IAClE,MAAM,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,MAAM,cAAc,CAAC,UAAU,CAAC,CAAC;IACtD,IAAI,CAAC,GAAG,EAAE,CAAC;QACT,MAAM,IAAI,SAAS,CACjB,6BAA6B,IAAI,CAAC,QAAQ,CAAC,QAAQ,EAAE,UAAU,CAAC,IAAI,GAAG,uEAAuE,CAC/I,CAAC;IACJ,CAAC;IAED,MAAM,MAAM,GAAG,MAAM,UAAU,CAAC,QAAQ,CAAC,CAAC;IAC1C,MAAM,CAAC,SAAS,GAAG,IAAI,CAAC,QAAQ,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC;IAChD,MAAM,CAAC,KAAK,GAAG,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IAEzD,MAAM,OAAO,GAAG,MAAM,WAAW,CAAC,GAAG,CAAC,CAAC;IACvC,MAAM,IAAI,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC,CAAC;IAChC,MAAM,WAAW,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,EAAE,YAAY,CAAC,CAAC;IACrD,MAAM,IAAI,GAAG,YAAY,CAAC,WAAW,EAAE,OAAO,EAAE,IAAI,CAAC,CAAC;IAEtD,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC;IACjD,MAAM,KAAK,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAE1C,MAAM,MAAM,GAAe,EAAE,OAAO,EAAE,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC;IACrE,MAAM,MAAM,GAA2B,EAAE,GAAG,CAAC,MAAM,CAAC,eAAe,IAAI,EAAE,CAAC,EAAE,CAAC;IAC7E,MAAM,UAAU,GAAG,IAAI,GAAG,CAAC,CAAC,cAAc,EAAE,cAAc,CAAC,CAAC,CAAC;IAE7D,KAAK,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;QACnD,MAAM,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;QACnC,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;QACzC,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,CAAC;YACnB,MAAM,SAAS,CAAC,CAAC,EAAE,OAAO,EAAE,MAAM,CAAC,CAAC;YACpC,MAAM,CAAC,IAAI,CAAC,GAAG,GAAG,CAAC,OAAO,CAAC,CAAC;YAC5B,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;YACzB,SAAS;QACX,CAAC;QACD,IAAI,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;YACzB,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,yCAAyC;YACnE,SAAS;QACX,CAAC;QACD,MAAM,OAAO,GAAG,MAAM,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC;QAC1C,IAAI,MAAM,CAAC,IAAI,CAAC,KAAK,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC;YAClC,IAAI,OAAO,KAAK,OAAO,EAAE,CAAC;gBACxB,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;YAC3B,CAAC;iBAAM,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;gBACtB,MAAM,SAAS,CAAC,CAAC,EAAE,OAAO,EAAE,MAAM,CAAC,CAAC;gBACpC,MAAM,CAAC,IAAI,CAAC,GAAG,GAAG,CAAC,OAAO,CAAC,CAAC;gBAC5B,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;YAC3B,CAAC;iBAAM,CAAC;gBACN,wEAAwE;gBACxE,MAAM,SAAS,CAAC,CAAC,EAAE,OAAO,EAAE,MAAM,CAAC,CAAC;gBACpC,MAAM,CAAC,IAAI,CAAC,GAAG,GAAG,CAAC,OAAO,CAAC,CAAC;gBAC5B,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;YAC3B,CAAC;QACH,CAAC;aAAM,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;YACtB,MAAM,SAAS,CAAC,CAAC,EAAE,OAAO,EAAE,MAAM,CAAC,CAAC;YACpC,MAAM,CAAC,IAAI,CAAC,GAAG,GAAG,CAAC,OAAO,CAAC,CAAC;YAC5B,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QAC3B,CAAC;aAAM,CAAC;YACN,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QAC3B,CAAC;IACH,CAAC;IAED,MAAM,CAAC,eAAe,GAAG,MAAM,CAAC;IAChC,MAAM,OAAO,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAC;IACrC,MAAM,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACxD,MAAM,SAAS,CAAC,OAAO,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,GAAG,IAAI,EAAE,MAAM,CAAC,CAAC;IACzE,yDAAyD;IACzD,MAAM,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,aAAa,EAAE,WAAW,CAAC,EAAE,gBAAgB,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,CAAC;IAEnG,IAAI,IAAI,CAAC,YAAY,KAAK,KAAK,EAAE,CAAC;QAChC,MAAM,IAAI,GAAG,MAAM,oBAAoB,CAAC,QAAQ,CAAC,CAAC;QAClD,IAAI,IAAI;YAAE,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC,CAAC;IAC/D,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC"}
@@ -0,0 +1,31 @@
1
+ import { execa } from 'execa';
2
+ import { existsSync } from 'node:fs';
3
+ import path from 'node:path';
4
+ async function openspec(repo, args) {
5
+ try {
6
+ const { stdout, stderr } = await execa('openspec', args, { cwd: repo });
7
+ return { ok: true, output: [stdout, stderr].filter(Boolean).join('\n') };
8
+ }
9
+ catch (err) {
10
+ const e = err;
11
+ if (e.code === 'ENOENT') {
12
+ return { ok: false, output: 'openspec CLI not found on PATH (npm i -g @fission-ai/openspec)' };
13
+ }
14
+ return { ok: false, output: [e.stdout, e.stderr].filter(Boolean).join('\n') || e.message };
15
+ }
16
+ }
17
+ export function hasOpenSpec(repo) {
18
+ return existsSync(path.join(repo, 'openspec'));
19
+ }
20
+ export async function openspecInit(repo) {
21
+ if (hasOpenSpec(repo))
22
+ return { ok: true, output: 'openspec/ already present' };
23
+ return openspec(repo, ['init', '--no-interactive']);
24
+ }
25
+ export function openspecValidate(repo, changeId) {
26
+ return openspec(repo, changeId ? ['validate', changeId] : ['validate']);
27
+ }
28
+ export function openspecArchive(repo, changeId) {
29
+ return openspec(repo, ['archive', changeId, '--yes']);
30
+ }
31
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../../src/openspec/cli.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,OAAO,CAAC;AAC9B,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,IAAI,MAAM,WAAW,CAAC;AAY7B,KAAK,UAAU,QAAQ,CAAC,IAAY,EAAE,IAAc;IAClD,IAAI,CAAC;QACH,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,KAAK,CAAC,UAAU,EAAE,IAAI,EAAE,EAAE,GAAG,EAAE,IAAI,EAAE,CAAC,CAAC;QACxE,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;IAC3E,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,CAAC,GAAG,GAA2E,CAAC;QACtF,IAAI,CAAC,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;YACxB,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,gEAAgE,EAAE,CAAC;QACjG,CAAC;QACD,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,CAAC;IAC7F,CAAC;AACH,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,IAAY;IACtC,OAAO,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,UAAU,CAAC,CAAC,CAAC;AACjD,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,IAAY;IAC7C,IAAI,WAAW,CAAC,IAAI,CAAC;QAAE,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,2BAA2B,EAAE,CAAC;IAChF,OAAO,QAAQ,CAAC,IAAI,EAAE,CAAC,MAAM,EAAE,kBAAkB,CAAC,CAAC,CAAC;AACtD,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,IAAY,EAAE,QAAiB;IAC9D,OAAO,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC,UAAU,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC;AAC1E,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,IAAY,EAAE,QAAgB;IAC5D,OAAO,QAAQ,CAAC,IAAI,EAAE,CAAC,SAAS,EAAE,QAAQ,EAAE,OAAO,CAAC,CAAC,CAAC;AACxD,CAAC"}
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Minimal .env loader.
3
+ *
4
+ * Keys are env-var-only by contract (AC-4.1), and they still are: this reads a
5
+ * gitignored .env into process.env at startup and nothing else ever persists
6
+ * them. No dependency, because dotenv's extra features (interpolation, multi-
7
+ * file precedence) are all things we would have to reason about at key-handling
8
+ * time.
9
+ *
10
+ * The real environment always wins. A .env file is a convenience for local runs,
11
+ * so it must never quietly override what CI or a shell export already set.
12
+ */
13
+ import { readFileSync, existsSync } from 'node:fs';
14
+ import path from 'node:path';
15
+ /** KEY=value, optional `export ` prefix, optional matched quotes. */
16
+ const LINE = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/;
17
+ export function parseEnv(text) {
18
+ const out = {};
19
+ for (const raw of text.split('\n')) {
20
+ const line = raw.trim();
21
+ if (!line || line.startsWith('#'))
22
+ continue;
23
+ const m = LINE.exec(line);
24
+ if (!m)
25
+ continue;
26
+ const key = m[1];
27
+ let value = m[2].trim();
28
+ const quoted = (value.startsWith('"') && value.endsWith('"') && value.length > 1) ||
29
+ (value.startsWith("'") && value.endsWith("'") && value.length > 1);
30
+ if (quoted) {
31
+ value = value.slice(1, -1);
32
+ }
33
+ else {
34
+ // Only strip trailing comments from unquoted values; a `#` inside quotes
35
+ // is part of the secret.
36
+ const hash = value.indexOf(' #');
37
+ if (hash !== -1)
38
+ value = value.slice(0, hash).trim();
39
+ }
40
+ out[key] = value;
41
+ }
42
+ return out;
43
+ }
44
+ /**
45
+ * Load `<dir>/.env` into process.env without overriding existing values.
46
+ * Returns the names (never the values) of the variables it set, for logging.
47
+ */
48
+ export function loadEnvFile(dir) {
49
+ const p = path.join(dir, '.env');
50
+ if (!existsSync(p))
51
+ return [];
52
+ let parsed;
53
+ try {
54
+ parsed = parseEnv(readFileSync(p, 'utf8'));
55
+ }
56
+ catch {
57
+ return [];
58
+ }
59
+ const applied = [];
60
+ for (const [key, value] of Object.entries(parsed)) {
61
+ if (process.env[key] === undefined) {
62
+ process.env[key] = value;
63
+ applied.push(key);
64
+ }
65
+ }
66
+ return applied;
67
+ }
68
+ //# sourceMappingURL=env.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"env.js","sourceRoot":"","sources":["../../src/util/env.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACnD,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B,qEAAqE;AACrE,MAAM,IAAI,GAAG,wDAAwD,CAAC;AAEtE,MAAM,UAAU,QAAQ,CAAC,IAAY;IACnC,MAAM,GAAG,GAA2B,EAAE,CAAC;IACvC,KAAK,MAAM,GAAG,IAAI,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;QACnC,MAAM,IAAI,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC;QACxB,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC;YAAE,SAAS;QAC5C,MAAM,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC1B,IAAI,CAAC,CAAC;YAAE,SAAS;QACjB,MAAM,GAAG,GAAG,CAAC,CAAC,CAAC,CAAE,CAAC;QAClB,IAAI,KAAK,GAAG,CAAC,CAAC,CAAC,CAAE,CAAC,IAAI,EAAE,CAAC;QACzB,MAAM,MAAM,GACV,CAAC,KAAK,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC;YAClE,CAAC,KAAK,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;QACrE,IAAI,MAAM,EAAE,CAAC;YACX,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;QAC7B,CAAC;aAAM,CAAC;YACN,yEAAyE;YACzE,yBAAyB;YACzB,MAAM,IAAI,GAAG,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;YACjC,IAAI,IAAI,KAAK,CAAC,CAAC;gBAAE,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,IAAI,EAAE,CAAC;QACvD,CAAC;QACD,GAAG,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;IACnB,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,WAAW,CAAC,GAAW;IACrC,MAAM,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;IACjC,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC;QAAE,OAAO,EAAE,CAAC;IAC9B,IAAI,MAA8B,CAAC;IACnC,IAAI,CAAC;QACH,MAAM,GAAG,QAAQ,CAAC,YAAY,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC;IAC7C,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,CAAC;IACZ,CAAC;IACD,MAAM,OAAO,GAAa,EAAE,CAAC;IAC7B,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;QAClD,IAAI,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,KAAK,SAAS,EAAE,CAAC;YACnC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;YACzB,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QACpB,CAAC;IACH,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC"}
@@ -0,0 +1,53 @@
1
+ import { execa } from 'execa';
2
+ async function git(repo, args) {
3
+ const { stdout } = await execa('git', args, { cwd: repo });
4
+ return stdout.trim();
5
+ }
6
+ export async function isGitRepo(repo) {
7
+ try {
8
+ await git(repo, ['rev-parse', '--git-dir']);
9
+ return true;
10
+ }
11
+ catch {
12
+ return false;
13
+ }
14
+ }
15
+ export async function isDirty(repo) {
16
+ const status = await git(repo, ['status', '--porcelain']);
17
+ return status.length > 0;
18
+ }
19
+ /**
20
+ * Snapshot the working tree before a run. On a clean tree HEAD is enough;
21
+ * with --allow-dirty we keep a `git stash create` object so uncommitted work
22
+ * survives a rollback (SPEC §7).
23
+ */
24
+ export async function snapshot(repo) {
25
+ const head = await git(repo, ['rev-parse', 'HEAD']);
26
+ let stash = null;
27
+ if (await isDirty(repo)) {
28
+ stash = (await git(repo, ['stash', 'create'])) || null;
29
+ }
30
+ return { head, stash };
31
+ }
32
+ /**
33
+ * Hard-restore the working tree to a snapshot (AC-3.6). The run audit trail
34
+ * (.copperhead/runs/) survives rollback: it is the evidence of what failed.
35
+ */
36
+ export async function restore(repo, snap) {
37
+ await git(repo, ['reset', '--hard', snap.head]);
38
+ await git(repo, ['clean', '-fd', '-e', '.copperhead/runs']);
39
+ if (snap.stash) {
40
+ await git(repo, ['stash', 'apply', snap.stash]);
41
+ }
42
+ }
43
+ export async function commitAll(repo, message) {
44
+ await git(repo, ['add', '-A']);
45
+ await git(repo, ['commit', '-m', message]);
46
+ return git(repo, ['rev-parse', 'HEAD']);
47
+ }
48
+ export async function changedFiles(repo, sinceHead) {
49
+ const tracked = await git(repo, ['diff', '--name-only', sinceHead]);
50
+ const untracked = await git(repo, ['ls-files', '--others', '--exclude-standard']);
51
+ return [...new Set([...tracked.split('\n'), ...untracked.split('\n')])].filter(Boolean);
52
+ }
53
+ //# sourceMappingURL=git.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"git.js","sourceRoot":"","sources":["../../src/util/git.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,25 @@
1
+ import path from 'node:path';
2
+ export class SandboxError extends Error {
3
+ attempted;
4
+ constructor(attempted) {
5
+ super(`path escapes repo root: ${attempted}`);
6
+ this.attempted = attempted;
7
+ this.name = 'SandboxError';
8
+ }
9
+ }
10
+ /**
11
+ * Resolve a repo-relative path and reject anything that escapes the repo root
12
+ * (AC-4.2). All file tools must go through this.
13
+ */
14
+ export function resolveInRepo(repoRoot, p) {
15
+ const abs = path.resolve(repoRoot, p);
16
+ const root = path.resolve(repoRoot);
17
+ if (abs !== root && !abs.startsWith(root + path.sep)) {
18
+ throw new SandboxError(p);
19
+ }
20
+ return abs;
21
+ }
22
+ export function isKicadFile(p) {
23
+ return /\.(kicad_sch|kicad_pcb|kicad_pro|kicad_sym|kicad_mod)$/.test(p);
24
+ }
25
+ //# sourceMappingURL=paths.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"paths.js","sourceRoot":"","sources":["../../src/util/paths.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B,MAAM,OAAO,YAAa,SAAQ,KAAK;IACT;IAA5B,YAA4B,SAAiB;QAC3C,KAAK,CAAC,2BAA2B,SAAS,EAAE,CAAC,CAAC;QADpB,cAAS,GAAT,SAAS,CAAQ;QAE3C,IAAI,CAAC,IAAI,GAAG,cAAc,CAAC;IAC7B,CAAC;CACF;AAED;;;GAGG;AACH,MAAM,UAAU,aAAa,CAAC,QAAgB,EAAE,CAAS;IACvD,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC;IACtC,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;IACpC,IAAI,GAAG,KAAK,IAAI,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;QACrD,MAAM,IAAI,YAAY,CAAC,CAAC,CAAC,CAAC;IAC5B,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,CAAS;IACnC,OAAO,wDAAwD,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAC1E,CAAC"}
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Write-time secret redaction for transcripts and summaries (AC-4.1).
3
+ * Patterns are deliberately broad: losing a few characters of log fidelity
4
+ * beats leaking a key.
5
+ */
6
+ const PATTERNS = [
7
+ /sk-[A-Za-z0-9_-]+/g,
8
+ /Bearer\s+[A-Za-z0-9._-]{16,}/g,
9
+ // Registry and forge tokens: a transcript that quotes a publish command or a
10
+ // failing CI log can carry these just as easily as a model API key.
11
+ /npm_[A-Za-z0-9]{36,}/g,
12
+ /gh[pousr]_[A-Za-z0-9]{36,}/g,
13
+ /github_pat_[A-Za-z0-9_]{22,}/g,
14
+ ];
15
+ export function redactSecrets(text) {
16
+ let out = text;
17
+ for (const re of PATTERNS)
18
+ out = out.replace(re, '[REDACTED]');
19
+ return out;
20
+ }
21
+ //# sourceMappingURL=redact.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"redact.js","sourceRoot":"","sources":["../../src/util/redact.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,QAAQ,GAAa;IACzB,oBAAoB;IACpB,+BAA+B;IAC/B,6EAA6E;IAC7E,oEAAoE;IACpE,uBAAuB;IACvB,6BAA6B;IAC7B,+BAA+B;CAChC,CAAC;AAEF,MAAM,UAAU,aAAa,CAAC,IAAY;IACxC,IAAI,GAAG,GAAG,IAAI,CAAC;IACf,KAAK,MAAM,EAAE,IAAI,QAAQ;QAAE,GAAG,GAAG,GAAG,CAAC,OAAO,CAAC,EAAE,EAAE,YAAY,CAAC,CAAC;IAC/D,OAAO,GAAG,CAAC;AACb,CAAC"}
@@ -0,0 +1,27 @@
1
+ export function isRateLimit(err) {
2
+ const status = err?.status
3
+ ?? err?.statusCode;
4
+ return status === 429;
5
+ }
6
+ /** Exponential backoff ×N for rate limits (SPEC §4.5). */
7
+ export async function withRetry(fn, opts = {}) {
8
+ const retries = opts.retries ?? 3;
9
+ const baseMs = opts.baseMs ?? 1000;
10
+ const isRetryable = opts.isRetryable ?? isRateLimit;
11
+ const sleep = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
12
+ let lastErr;
13
+ for (let attempt = 0; attempt <= retries; attempt++) {
14
+ try {
15
+ return await fn();
16
+ }
17
+ catch (err) {
18
+ lastErr = err;
19
+ if (!isRetryable(err) || attempt === retries)
20
+ throw err;
21
+ opts.onRetry?.(attempt + 1, err);
22
+ await sleep(baseMs * 2 ** attempt);
23
+ }
24
+ }
25
+ throw lastErr;
26
+ }
27
+ //# sourceMappingURL=retry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"retry.js","sourceRoot":"","sources":["../../src/util/retry.ts"],"names":[],"mappings":"AAQA,MAAM,UAAU,WAAW,CAAC,GAAY;IACtC,MAAM,MAAM,GAAI,GAAgD,EAAE,MAAM;WAClE,GAA+B,EAAE,UAAU,CAAC;IAClD,OAAO,MAAM,KAAK,GAAG,CAAC;AACxB,CAAC;AAED,0DAA0D;AAC1D,MAAM,CAAC,KAAK,UAAU,SAAS,CAAI,EAAoB,EAAE,OAAkB,EAAE;IAC3E,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,IAAI,CAAC,CAAC;IAClC,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,IAAI,IAAI,CAAC;IACnC,MAAM,WAAW,GAAG,IAAI,CAAC,WAAW,IAAI,WAAW,CAAC;IACpD,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,IAAI,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,IAAI,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC;IAC5E,IAAI,OAAgB,CAAC;IACrB,KAAK,IAAI,OAAO,GAAG,CAAC,EAAE,OAAO,IAAI,OAAO,EAAE,OAAO,EAAE,EAAE,CAAC;QACpD,IAAI,CAAC;YACH,OAAO,MAAM,EAAE,EAAE,CAAC;QACpB,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,OAAO,GAAG,GAAG,CAAC;YACd,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,IAAI,OAAO,KAAK,OAAO;gBAAE,MAAM,GAAG,CAAC;YACxD,IAAI,CAAC,OAAO,EAAE,CAAC,OAAO,GAAG,CAAC,EAAE,GAAG,CAAC,CAAC;YACjC,MAAM,KAAK,CAAC,MAAM,GAAG,CAAC,IAAI,OAAO,CAAC,CAAC;QACrC,CAAC;IACH,CAAC;IACD,MAAM,OAAO,CAAC;AAChB,CAAC"}
package/package.json ADDED
@@ -0,0 +1,73 @@
1
+ {
2
+ "name": "copperhead",
3
+ "version": "0.3.0",
4
+ "description": "Cursor for circuit boards: an AI agent that designs, documents, and validates real PCBs on KiCad repositories",
5
+ "license": "Apache-2.0",
6
+ "author": "Animesh Chouhan <animeshchouhan@outlook.com>",
7
+ "homepage": "https://copperhead.chouhan.ai",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/chouhanindustries/copperhead.git"
11
+ },
12
+ "bugs": {
13
+ "url": "https://github.com/chouhanindustries/copperhead/issues"
14
+ },
15
+ "keywords": [
16
+ "kicad",
17
+ "pcb",
18
+ "eda",
19
+ "hardware",
20
+ "electronics",
21
+ "schematic",
22
+ "erc",
23
+ "drc",
24
+ "ai-agent",
25
+ "llm",
26
+ "cli"
27
+ ],
28
+ "type": "module",
29
+ "engines": {
30
+ "node": ">=20"
31
+ },
32
+ "bin": {
33
+ "copperhead": "dist/cli.js"
34
+ },
35
+ "files": [
36
+ "dist",
37
+ "src",
38
+ "LICENSE",
39
+ "README.md"
40
+ ],
41
+ "publishConfig": {
42
+ "access": "public",
43
+ "provenance": true
44
+ },
45
+ "scripts": {
46
+ "build": "tsc -p tsconfig.json",
47
+ "prepublishOnly": "npm run typecheck && npm run build",
48
+ "dev": "tsx src/cli.ts",
49
+ "test": "vitest run",
50
+ "test:watch": "vitest",
51
+ "typecheck": "tsc -p tsconfig.json --noEmit",
52
+ "docs:dev": "vitepress dev docs",
53
+ "docs:build": "vitepress build docs",
54
+ "docs:preview": "vitepress preview docs"
55
+ },
56
+ "dependencies": {
57
+ "@anthropic-ai/sdk": "^0.39.0",
58
+ "commander": "^12.1.0",
59
+ "execa": "^9.5.2",
60
+ "openai": "^4.87.3"
61
+ },
62
+ "devDependencies": {
63
+ "@types/node": "^22.13.10",
64
+ "tsx": "^4.19.3",
65
+ "typescript": "^5.8.2",
66
+ "vitepress": "^1.6.4",
67
+ "vitest": "^3.0.8"
68
+ },
69
+ "allowScripts": {
70
+ "esbuild@0.28.1": true,
71
+ "esbuild@0.21.5": true
72
+ }
73
+ }