copperhead 0.8.1 → 0.10.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/NOTICE +1 -1
- package/README.md +13 -5
- package/dist/agent/filetools.js +24 -1
- package/dist/agent/filetools.js.map +1 -1
- package/dist/agent/ledger.js +24 -0
- package/dist/agent/ledger.js.map +1 -1
- package/dist/agent/loop.js +67 -62
- package/dist/agent/loop.js.map +1 -1
- package/dist/agent/prompts.js +4 -3
- package/dist/agent/prompts.js.map +1 -1
- package/dist/agent/providers/openai.js +28 -6
- package/dist/agent/providers/openai.js.map +1 -1
- package/dist/agent/providers/tool-protocol.js +21 -0
- package/dist/agent/providers/tool-protocol.js.map +1 -1
- package/dist/agent/recovery.js +95 -1
- package/dist/agent/recovery.js.map +1 -1
- package/dist/agent/response-cache.js +18 -2
- package/dist/agent/response-cache.js.map +1 -1
- package/dist/agent/tools.js +185 -1
- package/dist/agent/tools.js.map +1 -1
- package/dist/agent/transcript.js +2 -0
- package/dist/agent/transcript.js.map +1 -1
- package/dist/cli.js +77 -2
- package/dist/cli.js.map +1 -1
- package/dist/commands/check.js +33 -1
- package/dist/commands/check.js.map +1 -1
- package/dist/commands/create.js +282 -26
- package/dist/commands/create.js.map +1 -1
- package/dist/commands/doctor.js +211 -11
- package/dist/commands/doctor.js.map +1 -1
- package/dist/config.js +61 -4
- package/dist/config.js.map +1 -1
- package/dist/kicad/bootstrap.js +24 -3
- package/dist/kicad/bootstrap.js.map +1 -1
- package/dist/kicad/cli.js +7 -26
- package/dist/kicad/cli.js.map +1 -1
- package/dist/kicad/dossier.js +207 -0
- package/dist/kicad/dossier.js.map +1 -0
- package/dist/kicad/draft/draft.js +132 -0
- package/dist/kicad/draft/draft.js.map +1 -0
- package/dist/kicad/draft/engine.js +2389 -0
- package/dist/kicad/draft/engine.js.map +1 -0
- package/dist/kicad/draft/ir.js +368 -0
- package/dist/kicad/draft/ir.js.map +1 -0
- package/dist/kicad/draft/symsource.js +490 -0
- package/dist/kicad/draft/symsource.js.map +1 -0
- package/dist/kicad/emit.js +181 -0
- package/dist/kicad/emit.js.map +1 -0
- package/dist/kicad/fab.js +13 -0
- package/dist/kicad/fab.js.map +1 -1
- package/dist/kicad/legibility.js +561 -0
- package/dist/kicad/legibility.js.map +1 -0
- package/dist/kicad/score.js +261 -0
- package/dist/kicad/score.js.map +1 -0
- package/dist/kicad/sexp.js +262 -10
- package/dist/kicad/sexp.js.map +1 -1
- package/dist/kicad/symlib.js +346 -16
- package/dist/kicad/symlib.js.map +1 -1
- package/dist/memory/bom-table.js +108 -34
- package/dist/memory/bom-table.js.map +1 -1
- package/dist/memory/scaffold.js +6 -0
- package/dist/memory/scaffold.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 +12 -2
- package/dist/util/redact.js.map +1 -1
- package/package.json +9 -7
- package/src/agent/filetools.ts +26 -1
- package/src/agent/ledger.ts +24 -0
- package/src/agent/loop.ts +88 -65
- package/src/agent/prompts.ts +4 -3
- package/src/agent/providers/openai.ts +38 -4
- package/src/agent/providers/tool-protocol.ts +22 -0
- package/src/agent/recovery.ts +94 -1
- package/src/agent/response-cache.ts +17 -1
- package/src/agent/tools.ts +189 -1
- package/src/agent/transcript.ts +6 -0
- package/src/cli.ts +73 -2
- package/src/commands/check.ts +51 -1
- package/src/commands/create.ts +278 -22
- package/src/commands/doctor.ts +219 -12
- package/src/config.ts +107 -2
- package/src/kicad/bootstrap.ts +24 -3
- package/src/kicad/cli.ts +6 -19
- package/src/kicad/dossier.ts +217 -0
- package/src/kicad/draft/draft.ts +171 -0
- package/src/kicad/draft/engine.ts +2466 -0
- package/src/kicad/draft/ir.ts +416 -0
- package/src/kicad/draft/symsource.ts +535 -0
- package/src/kicad/emit.ts +236 -0
- package/src/kicad/fab.ts +15 -0
- package/src/kicad/legibility.ts +646 -0
- package/src/kicad/score.ts +323 -0
- package/src/kicad/sexp.ts +339 -10
- package/src/kicad/symlib.ts +364 -18
- package/src/memory/bom-table.ts +119 -31
- package/src/memory/scaffold.ts +6 -0
- package/src/openspec/cli.ts +3 -2
- package/src/util/preflight.ts +18 -0
- package/src/util/redact.ts +12 -2
- package/dist/memory/synap.js +0 -152
- package/dist/memory/synap.js.map +0 -1
- package/src/memory/synap.ts +0 -217
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,12 +5,22 @@
|
|
|
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,
|
|
18
|
+
// Gemini and Groq keys: same idea as sk- above, just a different prefix.
|
|
19
|
+
// Real keys are AIza + 35 chars, but AIzaSy (the common fifth/sixth pair)
|
|
20
|
+
// isn't the only prefix Google issues - match on AIza alone so other
|
|
21
|
+
// Google keys aren't left unredacted.
|
|
22
|
+
/AIza[A-Za-z0-9_-]{20,}/g,
|
|
23
|
+
/gsk_[A-Za-z0-9]{20,}/g,
|
|
14
24
|
];
|
|
15
25
|
|
|
16
26
|
export function redactSecrets(text: string): string {
|
package/dist/memory/synap.js
DELETED
|
@@ -1,152 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Cross-run memory via Synap (https://docs.maximem.ai).
|
|
3
|
-
*
|
|
4
|
-
* copperhead's own memory is per-repo and file-based: docs/DECISIONS.md, the
|
|
5
|
-
* constraint registry, the drift checker. Synap is the layer above that — what
|
|
6
|
-
* this user tends to want across every board they've ever asked about. It
|
|
7
|
-
* complements the docs; it does not replace them, and nothing here is a source
|
|
8
|
-
* of truth. Recalled text is advisory context in the system prompt, while
|
|
9
|
-
* as-built facts still come from the KiCad files.
|
|
10
|
-
*
|
|
11
|
-
* Three properties are load-bearing:
|
|
12
|
-
*
|
|
13
|
-
* 1. Opt-in. Inactive unless SYNAP_API_KEY is set, so the default install is
|
|
14
|
-
* unchanged and `check` stays network-free (it never constructs this).
|
|
15
|
-
* 2. Optional at runtime. @maximem/synap-js-sdk is an optionalDependency and is
|
|
16
|
-
* imported lazily through a non-literal specifier, so a missing package (or
|
|
17
|
-
* a host without the Python 3.11+ runtime its bridge needs) degrades to
|
|
18
|
-
* "no memory" instead of breaking the CLI.
|
|
19
|
-
* 3. Fail-soft on read, loud on write. A recall failure must never cost someone
|
|
20
|
-
* a design run; a record failure is reported, because silently losing writes
|
|
21
|
-
* lets memory drift away from what actually happened.
|
|
22
|
-
*/
|
|
23
|
-
import { randomUUID } from 'node:crypto';
|
|
24
|
-
import { execa } from 'execa';
|
|
25
|
-
import { redactSecrets } from '../util/redact.js';
|
|
26
|
-
/** Wall-clock ceiling on any single Synap call. The bridge is a Python subprocess. */
|
|
27
|
-
const RECALL_TIMEOUT_MS = 10_000;
|
|
28
|
-
const RECORD_TIMEOUT_MS = 15_000;
|
|
29
|
-
const MAX_RECALLED = 8;
|
|
30
|
-
export function synapEnabled(env = process.env) {
|
|
31
|
-
return Boolean(env.SYNAP_API_KEY);
|
|
32
|
-
}
|
|
33
|
-
/**
|
|
34
|
-
* Identity for memory scoping. The git committer email is the natural stable
|
|
35
|
-
* user id here: it is already the identity every run is attributed to.
|
|
36
|
-
*/
|
|
37
|
-
async function resolveUserId(repoRoot, env) {
|
|
38
|
-
if (env.SYNAP_USER_ID)
|
|
39
|
-
return env.SYNAP_USER_ID;
|
|
40
|
-
try {
|
|
41
|
-
const { stdout } = await execa('git', ['config', 'user.email'], { cwd: repoRoot });
|
|
42
|
-
if (stdout.trim())
|
|
43
|
-
return stdout.trim();
|
|
44
|
-
}
|
|
45
|
-
catch {
|
|
46
|
-
// not configured; fall through
|
|
47
|
-
}
|
|
48
|
-
return 'copperhead-local';
|
|
49
|
-
}
|
|
50
|
-
function withTimeout(p, ms, label) {
|
|
51
|
-
return new Promise((resolve, reject) => {
|
|
52
|
-
const timer = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms);
|
|
53
|
-
p.then((v) => {
|
|
54
|
-
clearTimeout(timer);
|
|
55
|
-
resolve(v);
|
|
56
|
-
}, (e) => {
|
|
57
|
-
clearTimeout(timer);
|
|
58
|
-
reject(e);
|
|
59
|
-
});
|
|
60
|
-
});
|
|
61
|
-
}
|
|
62
|
-
/**
|
|
63
|
-
* Returns null when memory is disabled or unavailable — every caller treats
|
|
64
|
-
* null as "run without cross-run memory".
|
|
65
|
-
*/
|
|
66
|
-
export async function openSynapMemory(opts) {
|
|
67
|
-
const env = opts.env ?? process.env;
|
|
68
|
-
const log = opts.log ?? (() => { });
|
|
69
|
-
if (!synapEnabled(env))
|
|
70
|
-
return null;
|
|
71
|
-
let client;
|
|
72
|
-
try {
|
|
73
|
-
// Non-literal specifier: keeps tsc from resolving an optional dependency at
|
|
74
|
-
// build time, so the package may legitimately be absent.
|
|
75
|
-
const specifier = '@maximem/synap-js-sdk';
|
|
76
|
-
const mod = (await import(specifier));
|
|
77
|
-
const createClient = mod.createClient ?? mod.default?.createClient;
|
|
78
|
-
if (!createClient)
|
|
79
|
-
throw new Error('createClient not exported');
|
|
80
|
-
client = createClient({ apiKey: env.SYNAP_API_KEY, requestTimeoutMs: RECORD_TIMEOUT_MS });
|
|
81
|
-
await withTimeout(client.init(), RECORD_TIMEOUT_MS, 'synap init');
|
|
82
|
-
}
|
|
83
|
-
catch (err) {
|
|
84
|
-
// Missing package, missing Python runtime, bad key: all non-fatal.
|
|
85
|
-
log(`synap memory unavailable (${err.message}); continuing without it`);
|
|
86
|
-
return null;
|
|
87
|
-
}
|
|
88
|
-
const userId = await resolveUserId(opts.repoRoot, env);
|
|
89
|
-
const customerId = env.SYNAP_CUSTOMER_ID ?? 'copperhead';
|
|
90
|
-
const conversationId = randomUUID();
|
|
91
|
-
return {
|
|
92
|
-
async recall(request) {
|
|
93
|
-
try {
|
|
94
|
-
const res = await withTimeout(client.searchMemory({
|
|
95
|
-
userId,
|
|
96
|
-
customerId,
|
|
97
|
-
query: redactSecrets(request),
|
|
98
|
-
maxResults: MAX_RECALLED,
|
|
99
|
-
}), RECALL_TIMEOUT_MS, 'synap recall');
|
|
100
|
-
const items = (res.results ?? []).filter((r) => r.memory?.trim());
|
|
101
|
-
if (!items.length)
|
|
102
|
-
return null;
|
|
103
|
-
const lines = items.map((r) => `- ${r.memory.trim()}${r.contextType ? ` _(${r.contextType})_` : ''}`);
|
|
104
|
-
return [
|
|
105
|
-
'## Recalled from prior runs (Synap)',
|
|
106
|
-
'',
|
|
107
|
-
'Context from earlier work by this user, possibly on other boards. Advisory only:',
|
|
108
|
-
'the KiCad files and this repo’s docs remain the source of truth. If any of this',
|
|
109
|
-
'conflicts with what you read in the repo, the repo wins — say so rather than',
|
|
110
|
-
'acting on a stale memory.',
|
|
111
|
-
'',
|
|
112
|
-
...lines,
|
|
113
|
-
].join('\n');
|
|
114
|
-
}
|
|
115
|
-
catch (err) {
|
|
116
|
-
log(`synap recall failed (${err.message}); continuing without it`);
|
|
117
|
-
return null;
|
|
118
|
-
}
|
|
119
|
-
},
|
|
120
|
-
async record(run) {
|
|
121
|
-
const assistant = [
|
|
122
|
-
`Outcome: ${run.outcome}`,
|
|
123
|
-
`Summary: ${run.summary}`,
|
|
124
|
-
`OpenSpec change: ${run.changeId ?? 'n/a'}`,
|
|
125
|
-
`Verification: ${run.verification}`,
|
|
126
|
-
run.filesTouched.length ? `Files: ${run.filesTouched.join(', ')}` : null,
|
|
127
|
-
run.decisions.length ? `Decisions:\n${run.decisions.map((d) => `- ${d}`).join('\n')}` : null,
|
|
128
|
-
]
|
|
129
|
-
.filter(Boolean)
|
|
130
|
-
.join('\n');
|
|
131
|
-
await withTimeout(client.addMemory({
|
|
132
|
-
userId,
|
|
133
|
-
customerId,
|
|
134
|
-
conversationId,
|
|
135
|
-
messages: [
|
|
136
|
-
{ role: 'user', content: redactSecrets(run.request) },
|
|
137
|
-
{ role: 'assistant', content: redactSecrets(assistant) },
|
|
138
|
-
],
|
|
139
|
-
metadata: { source: 'copperhead', outcome: run.outcome, changeId: run.changeId },
|
|
140
|
-
}), RECORD_TIMEOUT_MS, 'synap record');
|
|
141
|
-
},
|
|
142
|
-
async close() {
|
|
143
|
-
try {
|
|
144
|
-
await withTimeout(client.shutdown(), RECORD_TIMEOUT_MS, 'synap shutdown');
|
|
145
|
-
}
|
|
146
|
-
catch {
|
|
147
|
-
// Best effort: a failed shutdown must not change the run's outcome.
|
|
148
|
-
}
|
|
149
|
-
},
|
|
150
|
-
};
|
|
151
|
-
}
|
|
152
|
-
//# sourceMappingURL=synap.js.map
|
package/dist/memory/synap.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"synap.js","sourceRoot":"","sources":["../../src/memory/synap.ts"],"names":[],"mappings":"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"}
|
package/src/memory/synap.ts
DELETED
|
@@ -1,217 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Cross-run memory via Synap (https://docs.maximem.ai).
|
|
3
|
-
*
|
|
4
|
-
* copperhead's own memory is per-repo and file-based: docs/DECISIONS.md, the
|
|
5
|
-
* constraint registry, the drift checker. Synap is the layer above that — what
|
|
6
|
-
* this user tends to want across every board they've ever asked about. It
|
|
7
|
-
* complements the docs; it does not replace them, and nothing here is a source
|
|
8
|
-
* of truth. Recalled text is advisory context in the system prompt, while
|
|
9
|
-
* as-built facts still come from the KiCad files.
|
|
10
|
-
*
|
|
11
|
-
* Three properties are load-bearing:
|
|
12
|
-
*
|
|
13
|
-
* 1. Opt-in. Inactive unless SYNAP_API_KEY is set, so the default install is
|
|
14
|
-
* unchanged and `check` stays network-free (it never constructs this).
|
|
15
|
-
* 2. Optional at runtime. @maximem/synap-js-sdk is an optionalDependency and is
|
|
16
|
-
* imported lazily through a non-literal specifier, so a missing package (or
|
|
17
|
-
* a host without the Python 3.11+ runtime its bridge needs) degrades to
|
|
18
|
-
* "no memory" instead of breaking the CLI.
|
|
19
|
-
* 3. Fail-soft on read, loud on write. A recall failure must never cost someone
|
|
20
|
-
* a design run; a record failure is reported, because silently losing writes
|
|
21
|
-
* lets memory drift away from what actually happened.
|
|
22
|
-
*/
|
|
23
|
-
import { randomUUID } from 'node:crypto';
|
|
24
|
-
import { execa } from 'execa';
|
|
25
|
-
import { redactSecrets } from '../util/redact.js';
|
|
26
|
-
|
|
27
|
-
/** Wall-clock ceiling on any single Synap call. The bridge is a Python subprocess. */
|
|
28
|
-
const RECALL_TIMEOUT_MS = 10_000;
|
|
29
|
-
const RECORD_TIMEOUT_MS = 15_000;
|
|
30
|
-
const MAX_RECALLED = 8;
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Structural subset of @maximem/synap-js-sdk's surface. Declared locally rather
|
|
34
|
-
* than imported so `tsc` succeeds when the optional dependency is absent.
|
|
35
|
-
*/
|
|
36
|
-
interface SynapSearchItem {
|
|
37
|
-
memory: string;
|
|
38
|
-
score?: number;
|
|
39
|
-
contextType?: string;
|
|
40
|
-
}
|
|
41
|
-
interface SynapClientLike {
|
|
42
|
-
init(): Promise<void>;
|
|
43
|
-
searchMemory(input: {
|
|
44
|
-
userId: string;
|
|
45
|
-
customerId?: string;
|
|
46
|
-
query: string;
|
|
47
|
-
maxResults?: number;
|
|
48
|
-
}): Promise<{ results: SynapSearchItem[] }>;
|
|
49
|
-
addMemory(input: {
|
|
50
|
-
userId: string;
|
|
51
|
-
customerId: string;
|
|
52
|
-
conversationId?: string;
|
|
53
|
-
messages: Array<{ role?: 'user' | 'assistant'; content: string }>;
|
|
54
|
-
metadata?: Record<string, unknown>;
|
|
55
|
-
}): Promise<{ success: boolean }>;
|
|
56
|
-
shutdown(): Promise<void>;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export interface RunRecord {
|
|
60
|
-
request: string;
|
|
61
|
-
outcome: 'success' | 'refused';
|
|
62
|
-
summary: string;
|
|
63
|
-
changeId: string | null;
|
|
64
|
-
filesTouched: string[];
|
|
65
|
-
decisions: string[];
|
|
66
|
-
verification: string;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
export interface SynapMemory {
|
|
70
|
-
/** Prior context relevant to this request, as a prompt-ready markdown block. */
|
|
71
|
-
recall(request: string): Promise<string | null>;
|
|
72
|
-
/** Persist a finished run. Rejects if the write fails. */
|
|
73
|
-
record(run: RunRecord): Promise<void>;
|
|
74
|
-
/** Stop the bridge subprocess. Always call this, or the CLI will not exit. */
|
|
75
|
-
close(): Promise<void>;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
export function synapEnabled(env: NodeJS.ProcessEnv = process.env): boolean {
|
|
79
|
-
return Boolean(env.SYNAP_API_KEY);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Identity for memory scoping. The git committer email is the natural stable
|
|
84
|
-
* user id here: it is already the identity every run is attributed to.
|
|
85
|
-
*/
|
|
86
|
-
async function resolveUserId(repoRoot: string, env: NodeJS.ProcessEnv): Promise<string> {
|
|
87
|
-
if (env.SYNAP_USER_ID) return env.SYNAP_USER_ID;
|
|
88
|
-
try {
|
|
89
|
-
const { stdout } = await execa('git', ['config', 'user.email'], { cwd: repoRoot });
|
|
90
|
-
if (stdout.trim()) return stdout.trim();
|
|
91
|
-
} catch {
|
|
92
|
-
// not configured; fall through
|
|
93
|
-
}
|
|
94
|
-
return 'copperhead-local';
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
function withTimeout<T>(p: Promise<T>, ms: number, label: string): Promise<T> {
|
|
98
|
-
return new Promise<T>((resolve, reject) => {
|
|
99
|
-
const timer = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms);
|
|
100
|
-
p.then(
|
|
101
|
-
(v) => {
|
|
102
|
-
clearTimeout(timer);
|
|
103
|
-
resolve(v);
|
|
104
|
-
},
|
|
105
|
-
(e) => {
|
|
106
|
-
clearTimeout(timer);
|
|
107
|
-
reject(e);
|
|
108
|
-
},
|
|
109
|
-
);
|
|
110
|
-
});
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* Returns null when memory is disabled or unavailable — every caller treats
|
|
115
|
-
* null as "run without cross-run memory".
|
|
116
|
-
*/
|
|
117
|
-
export async function openSynapMemory(opts: {
|
|
118
|
-
repoRoot: string;
|
|
119
|
-
log?: (line: string) => void;
|
|
120
|
-
env?: NodeJS.ProcessEnv;
|
|
121
|
-
}): Promise<SynapMemory | null> {
|
|
122
|
-
const env = opts.env ?? process.env;
|
|
123
|
-
const log = opts.log ?? (() => {});
|
|
124
|
-
if (!synapEnabled(env)) return null;
|
|
125
|
-
|
|
126
|
-
let client: SynapClientLike;
|
|
127
|
-
try {
|
|
128
|
-
// Non-literal specifier: keeps tsc from resolving an optional dependency at
|
|
129
|
-
// build time, so the package may legitimately be absent.
|
|
130
|
-
const specifier = '@maximem/synap-js-sdk';
|
|
131
|
-
const mod = (await import(specifier)) as {
|
|
132
|
-
createClient?: (o?: Record<string, unknown>) => SynapClientLike;
|
|
133
|
-
default?: { createClient?: (o?: Record<string, unknown>) => SynapClientLike };
|
|
134
|
-
};
|
|
135
|
-
const createClient = mod.createClient ?? mod.default?.createClient;
|
|
136
|
-
if (!createClient) throw new Error('createClient not exported');
|
|
137
|
-
client = createClient({ apiKey: env.SYNAP_API_KEY, requestTimeoutMs: RECORD_TIMEOUT_MS });
|
|
138
|
-
await withTimeout(client.init(), RECORD_TIMEOUT_MS, 'synap init');
|
|
139
|
-
} catch (err) {
|
|
140
|
-
// Missing package, missing Python runtime, bad key: all non-fatal.
|
|
141
|
-
log(`synap memory unavailable (${(err as Error).message}); continuing without it`);
|
|
142
|
-
return null;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
const userId = await resolveUserId(opts.repoRoot, env);
|
|
146
|
-
const customerId = env.SYNAP_CUSTOMER_ID ?? 'copperhead';
|
|
147
|
-
const conversationId = randomUUID();
|
|
148
|
-
|
|
149
|
-
return {
|
|
150
|
-
async recall(request) {
|
|
151
|
-
try {
|
|
152
|
-
const res = await withTimeout(
|
|
153
|
-
client.searchMemory({
|
|
154
|
-
userId,
|
|
155
|
-
customerId,
|
|
156
|
-
query: redactSecrets(request),
|
|
157
|
-
maxResults: MAX_RECALLED,
|
|
158
|
-
}),
|
|
159
|
-
RECALL_TIMEOUT_MS,
|
|
160
|
-
'synap recall',
|
|
161
|
-
);
|
|
162
|
-
const items = (res.results ?? []).filter((r) => r.memory?.trim());
|
|
163
|
-
if (!items.length) return null;
|
|
164
|
-
const lines = items.map((r) => `- ${r.memory.trim()}${r.contextType ? ` _(${r.contextType})_` : ''}`);
|
|
165
|
-
return [
|
|
166
|
-
'## Recalled from prior runs (Synap)',
|
|
167
|
-
'',
|
|
168
|
-
'Context from earlier work by this user, possibly on other boards. Advisory only:',
|
|
169
|
-
'the KiCad files and this repo’s docs remain the source of truth. If any of this',
|
|
170
|
-
'conflicts with what you read in the repo, the repo wins — say so rather than',
|
|
171
|
-
'acting on a stale memory.',
|
|
172
|
-
'',
|
|
173
|
-
...lines,
|
|
174
|
-
].join('\n');
|
|
175
|
-
} catch (err) {
|
|
176
|
-
log(`synap recall failed (${(err as Error).message}); continuing without it`);
|
|
177
|
-
return null;
|
|
178
|
-
}
|
|
179
|
-
},
|
|
180
|
-
|
|
181
|
-
async record(run) {
|
|
182
|
-
const assistant = [
|
|
183
|
-
`Outcome: ${run.outcome}`,
|
|
184
|
-
`Summary: ${run.summary}`,
|
|
185
|
-
`OpenSpec change: ${run.changeId ?? 'n/a'}`,
|
|
186
|
-
`Verification: ${run.verification}`,
|
|
187
|
-
run.filesTouched.length ? `Files: ${run.filesTouched.join(', ')}` : null,
|
|
188
|
-
run.decisions.length ? `Decisions:\n${run.decisions.map((d) => `- ${d}`).join('\n')}` : null,
|
|
189
|
-
]
|
|
190
|
-
.filter(Boolean)
|
|
191
|
-
.join('\n');
|
|
192
|
-
|
|
193
|
-
await withTimeout(
|
|
194
|
-
client.addMemory({
|
|
195
|
-
userId,
|
|
196
|
-
customerId,
|
|
197
|
-
conversationId,
|
|
198
|
-
messages: [
|
|
199
|
-
{ role: 'user', content: redactSecrets(run.request) },
|
|
200
|
-
{ role: 'assistant', content: redactSecrets(assistant) },
|
|
201
|
-
],
|
|
202
|
-
metadata: { source: 'copperhead', outcome: run.outcome, changeId: run.changeId },
|
|
203
|
-
}),
|
|
204
|
-
RECORD_TIMEOUT_MS,
|
|
205
|
-
'synap record',
|
|
206
|
-
);
|
|
207
|
-
},
|
|
208
|
-
|
|
209
|
-
async close() {
|
|
210
|
-
try {
|
|
211
|
-
await withTimeout(client.shutdown(), RECORD_TIMEOUT_MS, 'synap shutdown');
|
|
212
|
-
} catch {
|
|
213
|
-
// Best effort: a failed shutdown must not change the run's outcome.
|
|
214
|
-
}
|
|
215
|
-
},
|
|
216
|
-
};
|
|
217
|
-
}
|