copperhead 0.8.0 → 0.9.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.
- package/README.md +9 -1
- package/dist/agent/loop.js +38 -4
- package/dist/agent/loop.js.map +1 -1
- package/dist/agent/providers/openai.js +28 -6
- package/dist/agent/providers/openai.js.map +1 -1
- package/dist/agent/response-cache.js +18 -2
- package/dist/agent/response-cache.js.map +1 -1
- package/dist/cli.js +2 -2
- package/dist/cli.js.map +1 -1
- package/dist/commands/create.js +113 -9
- package/dist/commands/create.js.map +1 -1
- package/dist/commands/doctor.js +164 -11
- package/dist/commands/doctor.js.map +1 -1
- package/dist/config.js +60 -4
- package/dist/config.js.map +1 -1
- package/dist/kicad/cli.js +7 -26
- package/dist/kicad/cli.js.map +1 -1
- package/dist/kicad/sexp.js +23 -4
- package/dist/kicad/sexp.js.map +1 -1
- package/dist/memory/bom-table.js +48 -10
- package/dist/memory/bom-table.js.map +1 -1
- package/dist/openspec/cli.js +2 -1
- package/dist/openspec/cli.js.map +1 -1
- package/dist/util/preflight.js +17 -0
- package/dist/util/preflight.js.map +1 -1
- package/dist/util/redact.js +6 -2
- package/dist/util/redact.js.map +1 -1
- package/package.json +2 -5
- package/src/agent/loop.ts +60 -4
- package/src/agent/providers/openai.ts +38 -4
- package/src/agent/response-cache.ts +17 -1
- package/src/cli.ts +2 -2
- package/src/commands/create.ts +107 -10
- package/src/commands/doctor.ts +171 -12
- package/src/config.ts +83 -2
- package/src/kicad/cli.ts +6 -19
- package/src/kicad/sexp.ts +24 -4
- package/src/memory/bom-table.ts +51 -10
- package/src/openspec/cli.ts +3 -2
- package/src/util/preflight.ts +18 -0
- package/src/util/redact.ts +6 -2
package/src/kicad/sexp.ts
CHANGED
|
@@ -224,15 +224,33 @@ function symbolsOf(sheet: ParsedSheet): { node: SexpNode[]; sym: SchematicSymbol
|
|
|
224
224
|
return out;
|
|
225
225
|
}
|
|
226
226
|
|
|
227
|
-
|
|
227
|
+
function collectPowerSymbols(sheets: ParsedSheet[]): Set<string> {
|
|
228
|
+
const set = new Set<string>();
|
|
229
|
+
for (const sheet of sheets) {
|
|
230
|
+
const libs = child(sheet.root, 'lib_symbols');
|
|
231
|
+
if (!libs) continue;
|
|
232
|
+
for (const sym of children(libs, 'symbol')) {
|
|
233
|
+
const name = atomAt(sym, 1);
|
|
234
|
+
const p = child(sym, 'power');
|
|
235
|
+
if (name && p !== undefined && atomAt(p, 1) !== 'no') {
|
|
236
|
+
set.add(name);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return set;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const isPowerSymbol = (libId: string, powerSyms: Set<string>): boolean =>
|
|
244
|
+
libId.startsWith('power:') || powerSyms.has(libId);
|
|
228
245
|
|
|
229
246
|
/** One row per real component (power symbols excluded), across all sheets. */
|
|
230
247
|
export async function listSymbols(rootSch: string): Promise<SchematicSymbol[]> {
|
|
231
248
|
const sheets = await loadSheets(rootSch);
|
|
249
|
+
const powerSyms = collectPowerSymbols(sheets);
|
|
232
250
|
const out: SchematicSymbol[] = [];
|
|
233
251
|
for (const sheet of sheets) {
|
|
234
252
|
for (const { sym } of symbolsOf(sheet)) {
|
|
235
|
-
if (!isPowerSymbol(sym.libId)) out.push(sym);
|
|
253
|
+
if (!isPowerSymbol(sym.libId, powerSyms)) out.push(sym);
|
|
236
254
|
}
|
|
237
255
|
}
|
|
238
256
|
return out.sort((a, b) => a.ref.localeCompare(b.ref, undefined, { numeric: true }));
|
|
@@ -241,6 +259,7 @@ export async function listSymbols(rootSch: string): Promise<SchematicSymbol[]> {
|
|
|
241
259
|
/** All net names visible via labels and power symbols, across all sheets. */
|
|
242
260
|
export async function listNets(rootSch: string): Promise<string[]> {
|
|
243
261
|
const sheets = await loadSheets(rootSch);
|
|
262
|
+
const powerSyms = collectPowerSymbols(sheets);
|
|
244
263
|
const names = new Set<string>();
|
|
245
264
|
for (const sheet of sheets) {
|
|
246
265
|
for (const kind of ['label', 'global_label', 'hierarchical_label']) {
|
|
@@ -250,7 +269,7 @@ export async function listNets(rootSch: string): Promise<string[]> {
|
|
|
250
269
|
}
|
|
251
270
|
}
|
|
252
271
|
for (const { sym } of symbolsOf(sheet)) {
|
|
253
|
-
if (isPowerSymbol(sym.libId)) names.add(sym.value);
|
|
272
|
+
if (isPowerSymbol(sym.libId, powerSyms)) names.add(sym.value);
|
|
254
273
|
}
|
|
255
274
|
}
|
|
256
275
|
return [...names].sort();
|
|
@@ -271,6 +290,7 @@ export interface PinNet {
|
|
|
271
290
|
*/
|
|
272
291
|
export async function pinNets(rootSch: string): Promise<PinNet[]> {
|
|
273
292
|
const sheets = await loadSheets(rootSch);
|
|
293
|
+
const powerSyms = collectPowerSymbols(sheets);
|
|
274
294
|
const out: PinNet[] = [];
|
|
275
295
|
for (const sheet of sheets) {
|
|
276
296
|
const pinDefs = libPinDefs(sheet.root);
|
|
@@ -304,7 +324,7 @@ export async function pinNets(rootSch: string): Promise<PinNet[]> {
|
|
|
304
324
|
const abs = pinAbsolute(sym.at, mirror, pin);
|
|
305
325
|
const k = key(abs.x, abs.y);
|
|
306
326
|
uf.find(k);
|
|
307
|
-
if (isPowerSymbol(sym.libId)) {
|
|
327
|
+
if (isPowerSymbol(sym.libId, powerSyms)) {
|
|
308
328
|
netNameAt.set(k, sym.value);
|
|
309
329
|
} else {
|
|
310
330
|
symPins.push({ sym, pin, k });
|
package/src/memory/bom-table.ts
CHANGED
|
@@ -9,6 +9,25 @@ export interface TableRow {
|
|
|
9
9
|
cells: string[];
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
/**
|
|
13
|
+
* Split one row into trimmed cells. The outer pipes are optional, because they
|
|
14
|
+
* are optional in GitHub-flavored markdown: `Refdes | Pin | Net` renders as a
|
|
15
|
+
* table exactly like `| Refdes | Pin | Net |` does, and a hand- or LLM-authored
|
|
16
|
+
* doc may legitimately be written either way.
|
|
17
|
+
*/
|
|
18
|
+
function splitRow(line: string): string[] {
|
|
19
|
+
return line
|
|
20
|
+
.replace(/^\|/, '')
|
|
21
|
+
.replace(/\|$/, '')
|
|
22
|
+
.split('|')
|
|
23
|
+
.map((c) => c.trim());
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** `|---|:--:|` and friends: the row separating a header from its data. */
|
|
27
|
+
function isSeparatorRow(cells: string[]): boolean {
|
|
28
|
+
return cells.length > 0 && cells.every((c) => /^:?-+:?$/.test(c));
|
|
29
|
+
}
|
|
30
|
+
|
|
12
31
|
/**
|
|
13
32
|
* Parses every markdown pipe-table row out of a document, across however
|
|
14
33
|
* many tables the file contains, skipping separator rows (e.g. `|---|---|`).
|
|
@@ -67,28 +86,50 @@ export interface TableRow {
|
|
|
67
86
|
* it loops on finish forever. Resolving by header name fixes that.
|
|
68
87
|
*/
|
|
69
88
|
export function parseCanonicalTables(md: string): Array<{ header: TableRow; rows: TableRow[] }> {
|
|
70
|
-
|
|
71
|
-
|
|
89
|
+
type Line = { cells: string[]; separator: boolean };
|
|
90
|
+
const groups: Line[][] = [];
|
|
91
|
+
let current: Line[] | null = null;
|
|
92
|
+
let width = 0; // column count of the open group, set by its first line
|
|
72
93
|
for (const line of md.split('\n')) {
|
|
73
94
|
const t = line.trim();
|
|
74
|
-
if (!t.
|
|
95
|
+
if (!t.includes('|')) {
|
|
75
96
|
current = null; // a blank or prose line terminates the current table
|
|
76
97
|
continue;
|
|
77
98
|
}
|
|
78
|
-
const cells = t
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
99
|
+
const cells = splitRow(t);
|
|
100
|
+
// Outer pipes make a line unambiguously a table row. Without them, a
|
|
101
|
+
// pipe-bearing prose line (`Legend: A | B`) is indistinguishable from a
|
|
102
|
+
// row by shape alone, so the column count decides: matching the open
|
|
103
|
+
// table's width keeps it as a row, a mismatch ends the table there rather
|
|
104
|
+
// than reading the prose as a part/pin. The line still opens a new group,
|
|
105
|
+
// in case it is itself the header of an un-piped table.
|
|
106
|
+
if (current && !t.startsWith('|') && cells.length !== width) current = null;
|
|
83
107
|
if (!current) {
|
|
84
108
|
current = [];
|
|
109
|
+
width = cells.length;
|
|
85
110
|
groups.push(current);
|
|
86
111
|
}
|
|
87
|
-
|
|
112
|
+
// The separator row stays in the group so the header can be located
|
|
113
|
+
// relative to it; it is dropped from the rows returned below.
|
|
114
|
+
current.push({ cells, separator: isSeparatorRow(cells) });
|
|
88
115
|
}
|
|
89
116
|
const tables: Array<{ header: TableRow; rows: TableRow[] }> = [];
|
|
90
117
|
for (const g of groups) {
|
|
91
|
-
|
|
118
|
+
// The header is the row directly above the separator. Falling back to the
|
|
119
|
+
// first row keeps a table that omits the separator working, and anchoring
|
|
120
|
+
// on the separator means a stray pipe-bearing prose line immediately above
|
|
121
|
+
// a table no longer hides it.
|
|
122
|
+
const sep = g.findIndex((l) => l.separator);
|
|
123
|
+
const headerIdx = sep > 0 ? sep - 1 : 0;
|
|
124
|
+
const header = g[headerIdx];
|
|
125
|
+
if (!header || header.separator || !isHeader({ cells: header.cells })) continue;
|
|
126
|
+
tables.push({
|
|
127
|
+
header: { cells: header.cells },
|
|
128
|
+
rows: g
|
|
129
|
+
.slice(headerIdx + 1)
|
|
130
|
+
.filter((l) => !l.separator)
|
|
131
|
+
.map((l) => ({ cells: l.cells })),
|
|
132
|
+
});
|
|
92
133
|
}
|
|
93
134
|
return tables;
|
|
94
135
|
}
|
package/src/openspec/cli.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { execa } from 'execa';
|
|
2
2
|
import { existsSync } from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
+
import { isNotFoundError } from '../util/preflight.js';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* OpenSpec is driven as a subprocess, same pattern as kicad-cli (SPEC §2.6).
|
|
@@ -17,8 +18,8 @@ async function openspec(repo: string, args: string[]): Promise<OpenSpecResult> {
|
|
|
17
18
|
const { stdout, stderr } = await execa('openspec', args, { cwd: repo });
|
|
18
19
|
return { ok: true, output: [stdout, stderr].filter(Boolean).join('\n') };
|
|
19
20
|
} catch (err) {
|
|
20
|
-
const e = err as { stdout?: string; stderr?: string; code?: string; message: string };
|
|
21
|
-
if (e
|
|
21
|
+
const e = err as { stdout?: string; stderr?: string; code?: string; message: string; exitCode?: number };
|
|
22
|
+
if (isNotFoundError(e)) {
|
|
22
23
|
return { ok: false, output: 'openspec CLI not found on PATH (npm i -g @fission-ai/openspec)' };
|
|
23
24
|
}
|
|
24
25
|
return { ok: false, output: [e.stdout, e.stderr].filter(Boolean).join('\n') || e.message };
|
package/src/util/preflight.ts
CHANGED
|
@@ -23,6 +23,24 @@ export function formatPreflightFailure(reason: string, why: string, remedy: stri
|
|
|
23
23
|
return [reason, '', `why it failed: ${why}`, 'to fix:', ...steps].join('\n');
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
export function isNotFoundError(err: any): boolean {
|
|
27
|
+
if (!err) return false;
|
|
28
|
+
if (err.code === 'ENOENT') return true;
|
|
29
|
+
if (process.platform === 'win32') {
|
|
30
|
+
const msg = String(err.stderr || err.message || '');
|
|
31
|
+
if (err.exitCode === 9009) {
|
|
32
|
+
return (
|
|
33
|
+
msg.includes('is not recognized') ||
|
|
34
|
+
msg.includes('cannot find the path specified')
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
if (err.exitCode === 1) {
|
|
38
|
+
return msg.includes('is not recognized');
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
|
|
26
44
|
/** Default minimum free space to start a run: 2 GiB. A create run emits gerbers,
|
|
27
45
|
* STEP, SVG renders and KiCad local history; 2 GiB is comfortably above a
|
|
28
46
|
* single board's output while still catching a nearly-full disk. */
|
package/src/util/redact.ts
CHANGED
|
@@ -5,10 +5,14 @@
|
|
|
5
5
|
*/
|
|
6
6
|
const PATTERNS: RegExp[] = [
|
|
7
7
|
/sk-[A-Za-z0-9_-]+/g,
|
|
8
|
-
|
|
8
|
+
// Auth schemes are case-insensitive (RFC 7235 §2.1), and the token charset has
|
|
9
|
+
// to include base64's +, / and = or the match stops at the first one and the
|
|
10
|
+
// tail of the secret gets written out verbatim.
|
|
11
|
+
/Bearer\s+[A-Za-z0-9._+/=-]{16,}/gi,
|
|
9
12
|
// Registry and forge tokens: a transcript that quotes a publish command or a
|
|
10
13
|
// failing CI log can carry these just as easily as a model API key.
|
|
11
|
-
|
|
14
|
+
// npm also issues UUID-shaped tokens, so the charset allows dashes.
|
|
15
|
+
/npm_[A-Za-z0-9-]{36,}/g,
|
|
12
16
|
/gh[pousr]_[A-Za-z0-9]{36,}/g,
|
|
13
17
|
/github_pat_[A-Za-z0-9_]{22,}/g,
|
|
14
18
|
];
|