pi-supernova 0.0.8 → 0.0.15
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/CHANGELOG.md +85 -2
- package/README.md +36 -12
- package/bottleneck.js +1 -1
- package/catalog.js +20 -15
- package/check.js +166 -0
- package/config.default.json +1 -0
- package/config.js +1 -1
- package/decode.js +0 -10
- package/evidence.js +429 -0
- package/fuzzy.js +182 -0
- package/guest-worker.js +14 -11
- package/host-bridge.js +184 -74
- package/index.js +65 -42
- package/ledger.js +178 -0
- package/omp-frame.js +7 -22
- package/outline.js +80 -0
- package/package.json +8 -1
- package/render-measure.js +2 -30
- package/render.js +46 -150
- package/repo-index.js +273 -0
- package/runtime.js +3 -20
- package/search.js +141 -0
- package/snap.js +86 -108
- package/surface.js +24 -12
- package/vfs.js +7 -6
- package/workspace.js +33 -2
package/host-bridge.js
CHANGED
|
@@ -8,9 +8,16 @@ import { unknownToolMessage } from "./catalog.js";
|
|
|
8
8
|
import { extractStructuralSurface } from "./surface.js";
|
|
9
9
|
import { buildEditDiff, buildMultiEditDiff, buildPatchDiff, buildWriteDiff } from "./diff.js";
|
|
10
10
|
import { executeSnap } from "./snap.js";
|
|
11
|
+
import { selectEvidence } from "./evidence.js";
|
|
12
|
+
import { WorkspaceIndex } from "./repo-index.js";
|
|
13
|
+
import { outlineFile } from "./outline.js";
|
|
14
|
+
import { SeenLedger } from "./ledger.js";
|
|
15
|
+
import { quickCheck } from "./check.js";
|
|
16
|
+
import { declaredName } from "./repo-index.js";
|
|
11
17
|
import { CausalVfs } from "./vfs.js";
|
|
12
18
|
import { applyPatchToText } from "./patch.js";
|
|
13
|
-
import { resolveWorkspacePath, runCommand } from "./workspace.js";
|
|
19
|
+
import { resolveWorkspacePath, runCommand, clearPathCache, relativeSlash } from "./workspace.js";
|
|
20
|
+
import { fuzzyFind, grepIndexed, listIndexed, listWithTools, rgGrepArgs } from "./search.js";
|
|
14
21
|
|
|
15
22
|
function textResult(text, details) {
|
|
16
23
|
return {
|
|
@@ -61,15 +68,6 @@ async function probeExistingFile(cwd, targetParam, vfs) {
|
|
|
61
68
|
}
|
|
62
69
|
}
|
|
63
70
|
|
|
64
|
-
function patchModeOf(params) {
|
|
65
|
-
return (
|
|
66
|
-
isString(params?.patch) ||
|
|
67
|
-
(params?.newText === undefined &&
|
|
68
|
-
isString(params?.oldText) &&
|
|
69
|
-
(params.oldText.includes("@@ -") || params.oldText.startsWith("---")))
|
|
70
|
-
);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
71
|
function applyReplacements(target, content, requestedEdits) {
|
|
74
72
|
if (requestedEdits.length === 0) throw new Error("edit requires at least one replacement");
|
|
75
73
|
const matches = requestedEdits.map((replacement) => {
|
|
@@ -113,7 +111,7 @@ async function formatLsEntry(dirPath, entry) {
|
|
|
113
111
|
return `${entry.name}${isDir ? "/" : ""} (${typeLabel}${sizeSuffix})`;
|
|
114
112
|
}
|
|
115
113
|
|
|
116
|
-
function createNativeAdapters(getCwd, vfs, config) {
|
|
114
|
+
function createNativeAdapters(getCwd, vfs, config, index, ledger) {
|
|
117
115
|
async function readAdapter(params, signal) {
|
|
118
116
|
const cwd = getCwd();
|
|
119
117
|
const targetParam = params?.path ?? params?.target;
|
|
@@ -128,31 +126,153 @@ function createNativeAdapters(getCwd, vfs, config) {
|
|
|
128
126
|
|
|
129
127
|
if (looksLikePath(targetParam)) {
|
|
130
128
|
const targetPath = await resolveWorkspacePath(cwd, targetParam, "read", false);
|
|
131
|
-
|
|
132
|
-
return textResult(sliceLines(text, params?.offset, params?.limit), { path: targetPath });
|
|
129
|
+
return readFile(targetPath, params);
|
|
133
130
|
}
|
|
134
131
|
|
|
135
132
|
const existing = await probeExistingFile(cwd, targetParam, vfs);
|
|
136
|
-
if (existing)
|
|
137
|
-
const text = await vfs.read(existing);
|
|
138
|
-
return textResult(sliceLines(text, params?.offset, params?.limit), { path: existing });
|
|
139
|
-
}
|
|
133
|
+
if (existing) return readFile(existing, params);
|
|
140
134
|
|
|
141
135
|
if (isString(targetParam) && targetParam.trim()) {
|
|
142
136
|
try {
|
|
143
137
|
const snapRes = await executeSnap({
|
|
144
138
|
query: targetParam,
|
|
145
139
|
searchDir: cwd,
|
|
146
|
-
|
|
147
|
-
|
|
140
|
+
index,
|
|
141
|
+
overlayText: (p) => vfs.getOverlay(p),
|
|
142
|
+
pendingPaths: vfs.getOverlayPaths(),
|
|
148
143
|
});
|
|
149
144
|
return textResult(JSON.stringify(snapRes, null, 2), { ...snapRes, isSnap: true });
|
|
150
145
|
} catch {}
|
|
151
146
|
}
|
|
152
147
|
|
|
153
148
|
const targetPath = await resolveWorkspacePath(cwd, targetParam, "read", false);
|
|
154
|
-
|
|
155
|
-
|
|
149
|
+
return readFile(targetPath, params);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Plain text, a line window, or (with `about`) a relevance-folded outline of the whole file. */
|
|
153
|
+
async function readFile(targetPath, params) {
|
|
154
|
+
const cwd = getCwd();
|
|
155
|
+
const rel = relativeSlash(cwd, targetPath);
|
|
156
|
+
const text = await vfs.read(targetPath);
|
|
157
|
+
index.touch(rel);
|
|
158
|
+
if (isString(params?.about)) {
|
|
159
|
+
const pending = vfs.getOverlay(targetPath);
|
|
160
|
+
const entry = pending === undefined ? index.entry(targetPath) : WorkspaceIndex.fromText(targetPath, pending);
|
|
161
|
+
const outline = entry && outlineFile(entry, rel, params.about, outlineOptions(params, await referenceFinder(cwd, targetPath)));
|
|
162
|
+
if (outline) {
|
|
163
|
+
recordOutlineOrigins(rel, outline.text);
|
|
164
|
+
return textResult(outline.text, { path: targetPath, outline: true, expanded: outline.expanded, declarations: outline.declarations });
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
const explicit = isNumber(params?.offset) || isNumber(params?.limit);
|
|
168
|
+
const firstLine = isNumber(params?.offset) ? Math.max(1, Math.floor(params.offset)) : 1;
|
|
169
|
+
const sliced = sliceLines(text, params?.offset, params?.limit);
|
|
170
|
+
ledger.recordOrigin(rel, firstLine, sliced.split("\n"), explicit);
|
|
171
|
+
return textResult(sliced, { path: targetPath });
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* The edit result answers the follow-ups a model would otherwise spend turns on: the post-edit
|
|
176
|
+
* lines with numbers (so no verification re-read), a quick structural check, and every other
|
|
177
|
+
* place a changed declaration is referenced (so callers are not forgotten).
|
|
178
|
+
*/
|
|
179
|
+
async function editSummary(cwd, target, original, updated, diff) {
|
|
180
|
+
const rel = relativeSlash(cwd, target);
|
|
181
|
+
const newLines = updated.split("\n");
|
|
182
|
+
const added = diff.lines.filter((l) => l.type === "add");
|
|
183
|
+
const first = added.length ? Math.max(1, added[0].lineNum - 2) : 1;
|
|
184
|
+
const last = added.length ? Math.min(newLines.length, added[added.length - 1].lineNum + 2) : Math.min(newLines.length, first + 6);
|
|
185
|
+
const window = [];
|
|
186
|
+
for (let l = first; l <= last && window.length < 40; l++) window.push(String(l).padStart(5) + " " + newLines[l - 1]);
|
|
187
|
+
ledger.recordOrigin(rel, first, newLines.slice(first - 1, first - 1 + window.length));
|
|
188
|
+
let out = `edited ${rel}:${first}–${first + window.length - 1}\n${window.join("\n")}`;
|
|
189
|
+
const check = quickCheck(updated, path.extname(target));
|
|
190
|
+
if (check && !check.ok) out += `\ncheck: ${check.message}`;
|
|
191
|
+
const refs = await changedDeclarationRefs(cwd, target, original, updated, diff);
|
|
192
|
+
if (refs) out += `\n${refs}`;
|
|
193
|
+
return out;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async function changedDeclarationRefs(cwd, target, original, updated, diff) {
|
|
197
|
+
// Diff rows carry the replaced fragments; declarations live on whole file lines.
|
|
198
|
+
const oldLines = original.split("\n");
|
|
199
|
+
const newLines = updated.split("\n");
|
|
200
|
+
const names = new Set();
|
|
201
|
+
for (const l of diff.lines) {
|
|
202
|
+
if (l.type === "context") continue;
|
|
203
|
+
const name = declaredName((l.type === "remove" ? oldLines : newLines)[l.lineNum - 1] ?? "");
|
|
204
|
+
if (name) names.add(name);
|
|
205
|
+
}
|
|
206
|
+
if (names.size === 0) return "";
|
|
207
|
+
const find = await referenceFinder(cwd, target);
|
|
208
|
+
const parts = [];
|
|
209
|
+
for (const name of [...names].slice(0, 3)) {
|
|
210
|
+
const refs = find(name).filter((r) => !r.startsWith(relativeSlash(cwd, target) + ":"));
|
|
211
|
+
if (refs.length) parts.push(`${name} also referenced in ${refs.slice(0, 6).join(", ")}${refs.length > 6 ? " (+" + (refs.length - 6) + ")" : ""}`);
|
|
212
|
+
}
|
|
213
|
+
return parts.join("\n");
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const SOURCE_REF = /((?:[\w.@-]+\/)*[\w.@-]+\.(?:m?[jt]sx?|c[jt]s|py|rs|go|java|kt|rb|php|c|cc|cpp|h|hpp|cs|swift|json|ya?ml|toml))(?::|\()(\d+)/g;
|
|
217
|
+
|
|
218
|
+
/** Source window (±2 lines, ► on the cited line) for one path:line, or null when it is outside the workspace/index. */
|
|
219
|
+
function sourceWindow(cwd, file, lineNo) {
|
|
220
|
+
const candidate = path.resolve(cwd, file);
|
|
221
|
+
if (!candidate.startsWith(path.resolve(cwd) + path.sep)) return null;
|
|
222
|
+
const entry = index.entry(candidate);
|
|
223
|
+
if (!entry) return null;
|
|
224
|
+
const { raw } = WorkspaceIndex.linesOf(entry);
|
|
225
|
+
if (lineNo < 1 || lineNo > raw.length) return null;
|
|
226
|
+
const rel = relativeSlash(cwd, candidate);
|
|
227
|
+
const start = Math.max(1, lineNo - 2);
|
|
228
|
+
const rows = [];
|
|
229
|
+
for (let l = start; l <= Math.min(raw.length, lineNo + 2); l++) rows.push((l === lineNo ? "►" : " ") + String(l).padStart(4) + " " + raw[l - 1]);
|
|
230
|
+
ledger.recordOrigin(rel, start, rows);
|
|
231
|
+
return rel + ":" + lineNo + "\n" + rows.join("\n");
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** A failing command names path:line; the model wants those lines next. Attach them (≤4 sites). */
|
|
235
|
+
function sourceForReferences(cwd, output) {
|
|
236
|
+
const seen = new Set();
|
|
237
|
+
const blocks = [];
|
|
238
|
+
for (const m of output.matchAll(SOURCE_REF)) {
|
|
239
|
+
const key = m[1] + ":" + m[2];
|
|
240
|
+
if (seen.has(key)) continue;
|
|
241
|
+
seen.add(key);
|
|
242
|
+
const block = sourceWindow(cwd, m[1], Number(m[2]));
|
|
243
|
+
if (block) blocks.push(block);
|
|
244
|
+
if (blocks.length >= 4) break;
|
|
245
|
+
}
|
|
246
|
+
return blocks.length ? "\n--- source\n" + blocks.join("\n") : "";
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function outlineOptions(params, references) {
|
|
250
|
+
const options = { references };
|
|
251
|
+
if (params?.maxChars) options.maxChars = params.maxChars;
|
|
252
|
+
return options;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** Outline lines carry their own line numbers (" 330 text"); provenance follows them. */
|
|
256
|
+
function recordOutlineOrigins(rel, outlineText) {
|
|
257
|
+
for (const line of outlineText.split("\n")) {
|
|
258
|
+
const m = /^\s*(\d+) (.*)$/.exec(line);
|
|
259
|
+
if (m && !/ … \d+ lines$/.test(line)) ledger.recordOrigin(rel, Number(m[1]), [line]);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** Where else a name appears (declaration line excluded), for outlines and edit results. */
|
|
264
|
+
async function referenceFinder(cwd, targetPath) {
|
|
265
|
+
const files = await index.files(cwd);
|
|
266
|
+
if (!index.canScan(files)) return () => [];
|
|
267
|
+
return (name, excludeLine) => {
|
|
268
|
+
if (!name || name.length < 3) return [];
|
|
269
|
+
const escaped = name.replace(/[$]/g, (c) => "\\" + c);
|
|
270
|
+
const regex = new RegExp("\\b" + escaped + "\\b");
|
|
271
|
+
return index
|
|
272
|
+
.grepRows(files, regex, cwd)
|
|
273
|
+
.filter((r) => !(r.line === excludeLine && r.rel === relativeSlash(cwd, targetPath)))
|
|
274
|
+
.map((r) => r.rel + ":" + r.line);
|
|
275
|
+
};
|
|
156
276
|
}
|
|
157
277
|
|
|
158
278
|
return {
|
|
@@ -167,6 +287,7 @@ function createNativeAdapters(getCwd, vfs, config) {
|
|
|
167
287
|
prevText = await vfs.read(target);
|
|
168
288
|
} catch {}
|
|
169
289
|
const { speculative } = await vfs.write(target, content);
|
|
290
|
+
index.touch(relativeSlash(cwd, target));
|
|
170
291
|
const diff = buildWriteDiff(target, prevText, content);
|
|
171
292
|
const tag = speculative ? " (speculative)" : "";
|
|
172
293
|
return textResult(`wrote ${target}${tag}`, { path: target, speculative, diff });
|
|
@@ -176,33 +297,20 @@ function createNativeAdapters(getCwd, vfs, config) {
|
|
|
176
297
|
const target = await resolveWorkspacePath(cwd, params?.path, "edit", false);
|
|
177
298
|
if (signal?.aborted) throw new Error("aborted");
|
|
178
299
|
|
|
179
|
-
if (patchModeOf(params)) {
|
|
180
|
-
const patchContent = params.patch || params.oldText;
|
|
181
|
-
const original = await vfs.read(target);
|
|
182
|
-
const { resultText, hunkCount } = applyPatchToText(original, patchContent);
|
|
183
|
-
const { speculative } = await vfs.write(target, resultText);
|
|
184
|
-
const diff = buildPatchDiff(target, patchContent);
|
|
185
|
-
const tag = speculative ? " (speculative)" : "";
|
|
186
|
-
return textResult(`applied ${hunkCount} hunk(s) to ${target}${tag}`, {
|
|
187
|
-
path: target,
|
|
188
|
-
hunks: hunkCount,
|
|
189
|
-
speculative,
|
|
190
|
-
diff,
|
|
191
|
-
});
|
|
192
|
-
}
|
|
193
|
-
|
|
194
300
|
const requestedEdits = Array.isArray(params?.edits)
|
|
195
301
|
? params.edits
|
|
196
302
|
: [{ oldText: params?.oldText, newText: params?.newText }];
|
|
197
303
|
const content = await vfs.read(target);
|
|
198
304
|
const { updated, matches } = applyReplacements(target, content, requestedEdits);
|
|
199
305
|
const { speculative } = await vfs.write(target, updated);
|
|
306
|
+
index.touch(relativeSlash(cwd, target));
|
|
200
307
|
const diff =
|
|
201
308
|
matches.length === 1
|
|
202
309
|
? buildEditDiff(target, content, matches[0].oldText, matches[0].newText)
|
|
203
310
|
: buildMultiEditDiff(target, content, matches);
|
|
204
311
|
const tag = speculative ? " (speculative)" : "";
|
|
205
|
-
|
|
312
|
+
const summary = await editSummary(cwd, target, content, updated, diff);
|
|
313
|
+
return textResult(summary, { path: target, speculative, diff });
|
|
206
314
|
},
|
|
207
315
|
async apply_patch(params, signal) {
|
|
208
316
|
const cwd = getCwd();
|
|
@@ -243,19 +351,26 @@ function createNativeAdapters(getCwd, vfs, config) {
|
|
|
243
351
|
const res = await executeSnap({
|
|
244
352
|
query: params.query,
|
|
245
353
|
searchDir: snapTarget,
|
|
354
|
+
root: cwd,
|
|
246
355
|
includeHidden,
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
// Jail each snap candidate (symlink files must not escape the workspace).
|
|
250
|
-
const jailed = await resolveWorkspacePath(cwd, candidate, "snap", false);
|
|
251
|
-
return vfs.read(jailed);
|
|
252
|
-
},
|
|
253
|
-
},
|
|
254
|
-
runCommand: (argv, opts) => runCommand(argv, { cwd: snapTarget, signal, ...opts }),
|
|
356
|
+
index,
|
|
357
|
+
overlayText: (p) => vfs.getOverlay(p),
|
|
255
358
|
pendingPaths: vfs.getOverlayPaths(),
|
|
256
359
|
});
|
|
257
360
|
return textResult(JSON.stringify(res, null, 2), res);
|
|
258
361
|
},
|
|
362
|
+
async evidence(params, signal) {
|
|
363
|
+
const cwd = getCwd();
|
|
364
|
+
if (!isString(params?.query) || !params.query.trim()) throw new Error("evidence requires query");
|
|
365
|
+
if (signal?.aborted) throw new Error("aborted");
|
|
366
|
+
const searchDir = params?.path ? await resolveWorkspacePath(cwd, params.path, "evidence", true) : cwd;
|
|
367
|
+
const options = {};
|
|
368
|
+
if (Number.isInteger(params?.k) && params.k > 0) options.k = params.k;
|
|
369
|
+
if (Number.isInteger(params?.maxChars) && params.maxChars > 0) options.maxChars = params.maxChars;
|
|
370
|
+
const res = await selectEvidence({ query: params.query, root: cwd, searchDir, index, overlayText: (p) => vfs.getOverlay(p), options });
|
|
371
|
+
for (const span of res.spans) ledger.recordOrigin(span.path, span.lines[0], span.text.split("\n"));
|
|
372
|
+
return textResult(JSON.stringify(res), { route: res.route, count: res.spans.length });
|
|
373
|
+
},
|
|
259
374
|
async surface(params, signal) {
|
|
260
375
|
const cwd = getCwd();
|
|
261
376
|
const target = await resolveWorkspacePath(cwd, params?.path, "surface", false);
|
|
@@ -286,9 +401,11 @@ function createNativeAdapters(getCwd, vfs, config) {
|
|
|
286
401
|
});
|
|
287
402
|
} finally {
|
|
288
403
|
vfs.invalidateCache();
|
|
404
|
+
index.invalidate();
|
|
289
405
|
}
|
|
290
406
|
const { stdout, stderr } = res;
|
|
291
|
-
|
|
407
|
+
let text = stdout && stderr ? stdout + (stdout.endsWith("\n") ? "" : "\n") + stderr : stdout || stderr;
|
|
408
|
+
if (res.exitCode !== 0) text += sourceForReferences(cwd, text);
|
|
292
409
|
return {
|
|
293
410
|
content: [{ type: "text", text }],
|
|
294
411
|
details: { exitCode: res.exitCode, outputTruncated: res.outputTruncated, transactionBarrier },
|
|
@@ -300,11 +417,10 @@ function createNativeAdapters(getCwd, vfs, config) {
|
|
|
300
417
|
const pattern = String(params?.pattern || "");
|
|
301
418
|
if (!pattern) throw new Error("grep requires pattern");
|
|
302
419
|
const searchPath = params?.path ? await resolveWorkspacePath(cwd, params.path, "grep", true) : cwd;
|
|
303
|
-
const
|
|
304
|
-
if (
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
const res = await runCommand(["rg", ...args], { cwd, timeoutMs: 30_000, signal });
|
|
420
|
+
const indexed = await grepIndexed(index, pattern, params, searchPath, cwd);
|
|
421
|
+
if (indexed !== null) return textResult(indexed, { exitCode: indexed ? 0 : 1, via: "index" });
|
|
422
|
+
// Large tree: real rg keeps its own output format.
|
|
423
|
+
const res = await runCommand(["rg", ...rgGrepArgs(pattern, params, searchPath)], { cwd, timeoutMs: 30_000, signal });
|
|
308
424
|
if (res.exitCode !== 0 && res.exitCode !== 1) {
|
|
309
425
|
throw new Error(res.stderr.trim() || `rg exited ${res.exitCode}`);
|
|
310
426
|
}
|
|
@@ -314,6 +430,10 @@ function createNativeAdapters(getCwd, vfs, config) {
|
|
|
314
430
|
const cwd = getCwd();
|
|
315
431
|
const pattern = String(params?.pattern || "");
|
|
316
432
|
if (!pattern) throw new Error("glob requires pattern");
|
|
433
|
+
const fuzzy = await fuzzyFind(index, cwd, cwd, pattern);
|
|
434
|
+
if (fuzzy !== null) return textResult(fuzzy, { via: "fuzzy" });
|
|
435
|
+
const indexed = await listIndexed(index, cwd, cwd, pattern);
|
|
436
|
+
if (indexed !== null) return textResult(indexed, { via: "index" });
|
|
317
437
|
const rg = await runCommand(["rg", "--files", "-g", pattern], { cwd, timeoutMs: 30_000, signal }).catch(
|
|
318
438
|
() => null,
|
|
319
439
|
);
|
|
@@ -333,16 +453,12 @@ function createNativeAdapters(getCwd, vfs, config) {
|
|
|
333
453
|
const searchDir = params?.path ? await resolveWorkspacePath(cwd, params.path, "find", true) : cwd;
|
|
334
454
|
const pattern = params?.pattern || params?.glob;
|
|
335
455
|
if (signal?.aborted) throw new Error("aborted");
|
|
336
|
-
const
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
const findArgs = [searchDir];
|
|
343
|
-
if (pattern) findArgs.push("-name", String(pattern));
|
|
344
|
-
const findRes = await runCommand(["find", ...findArgs], { cwd, timeoutMs: 30_000, signal });
|
|
345
|
-
return textResult(findRes.stdout, { via: "find" });
|
|
456
|
+
const globPattern = pattern ? String(pattern) : null;
|
|
457
|
+
const fuzzy = await fuzzyFind(index, searchDir, cwd, globPattern);
|
|
458
|
+
if (fuzzy !== null) return textResult(fuzzy, { via: "fuzzy" });
|
|
459
|
+
const indexed = await listIndexed(index, searchDir, cwd, globPattern);
|
|
460
|
+
if (indexed !== null) return textResult(indexed, { via: "index" });
|
|
461
|
+
return listWithTools(searchDir, globPattern, cwd, signal);
|
|
346
462
|
},
|
|
347
463
|
async ls(params, signal) {
|
|
348
464
|
const cwd = getCwd();
|
|
@@ -359,9 +475,11 @@ function createNativeAdapters(getCwd, vfs, config) {
|
|
|
359
475
|
}
|
|
360
476
|
|
|
361
477
|
export function createHostBridge({ pi, config, getCwd }) {
|
|
362
|
-
const
|
|
478
|
+
const index = new WorkspaceIndex((argv, opts) => runCommand(argv, opts));
|
|
479
|
+
const ledger = new SeenLedger({ window: config.seenWindow ?? 40 });
|
|
480
|
+
const vfs = new CausalVfs(() => index.invalidate());
|
|
363
481
|
const executors = new Map();
|
|
364
|
-
const natives = createNativeAdapters(getCwd, vfs, config);
|
|
482
|
+
const natives = createNativeAdapters(getCwd, vfs, config, index, ledger);
|
|
365
483
|
let callCount = 0;
|
|
366
484
|
let activeCtx = null;
|
|
367
485
|
let activeSignal = undefined;
|
|
@@ -395,6 +513,7 @@ export function createHostBridge({ pi, config, getCwd }) {
|
|
|
395
513
|
trace = [];
|
|
396
514
|
// Files may change between programs (editor, git); never serve a stale run.
|
|
397
515
|
vfs.invalidateCache();
|
|
516
|
+
clearPathCache();
|
|
398
517
|
}
|
|
399
518
|
|
|
400
519
|
function getTrace() {
|
|
@@ -405,10 +524,6 @@ export function createHostBridge({ pi, config, getCwd }) {
|
|
|
405
524
|
callListener = isFunction(fn) ? fn : null;
|
|
406
525
|
}
|
|
407
526
|
|
|
408
|
-
function hasExecutor(name) {
|
|
409
|
-
return executors.has(name) || Object.hasOwn(natives, name);
|
|
410
|
-
}
|
|
411
|
-
|
|
412
527
|
function beginSpeculation() {
|
|
413
528
|
return vfs.begin();
|
|
414
529
|
}
|
|
@@ -421,10 +536,6 @@ export function createHostBridge({ pi, config, getCwd }) {
|
|
|
421
536
|
return vfs.rollback();
|
|
422
537
|
}
|
|
423
538
|
|
|
424
|
-
function clearVfsCache() {
|
|
425
|
-
vfs.clear();
|
|
426
|
-
}
|
|
427
|
-
|
|
428
539
|
function resultDiff(response) {
|
|
429
540
|
let details = response?.details;
|
|
430
541
|
if (isString(details)) {
|
|
@@ -514,6 +625,7 @@ export function createHostBridge({ pi, config, getCwd }) {
|
|
|
514
625
|
} catch (error) {
|
|
515
626
|
record.ok = false;
|
|
516
627
|
record.ms = Date.now() - record.time;
|
|
628
|
+
record.error = error instanceof Error ? error.message : String(error);
|
|
517
629
|
notifyCall(record);
|
|
518
630
|
throw error;
|
|
519
631
|
}
|
|
@@ -559,15 +671,13 @@ export function createHostBridge({ pi, config, getCwd }) {
|
|
|
559
671
|
resetCallBudget,
|
|
560
672
|
getTrace,
|
|
561
673
|
setCallListener,
|
|
562
|
-
hasExecutor,
|
|
563
674
|
beginSpeculation,
|
|
564
675
|
commitSpeculation,
|
|
565
676
|
rollbackSpeculation,
|
|
566
|
-
clearVfsCache,
|
|
567
677
|
getVfsCacheSize: () => vfs.getCacheSize(),
|
|
568
678
|
getOverlayDepth: () => vfs.getOverlayDepth(),
|
|
569
679
|
call,
|
|
570
680
|
callMany,
|
|
571
|
-
|
|
681
|
+
ledger,
|
|
572
682
|
};
|
|
573
683
|
}
|
package/index.js
CHANGED
|
@@ -4,16 +4,11 @@ import { buildCatalog, searchCatalog, describeTool, mergeNativeToolDefinitions }
|
|
|
4
4
|
import { loadConfig } from "./config.js";
|
|
5
5
|
import { createHostBridge } from "./host-bridge.js";
|
|
6
6
|
import { runGuestProgram, warmGuestWorker } from "./runtime.js";
|
|
7
|
-
import {
|
|
8
|
-
extractOperationsFromCode,
|
|
9
|
-
renderSupernovaCall,
|
|
10
|
-
renderSupernovaResult,
|
|
11
|
-
SafeText,
|
|
12
|
-
} from "./render.js";
|
|
7
|
+
import { renderSupernovaCall, renderSupernovaResult } from "./render.js";
|
|
13
8
|
|
|
14
|
-
export {
|
|
9
|
+
export { renderSupernovaCall, renderSupernovaResult };
|
|
15
10
|
|
|
16
|
-
// Sync only
|
|
11
|
+
// Sync only, never top-level await. Dynamic import of host/deps hung OMP plugin load.
|
|
17
12
|
const require = createRequire(import.meta.url);
|
|
18
13
|
let Type;
|
|
19
14
|
try {
|
|
@@ -31,57 +26,84 @@ function result(text, details) {
|
|
|
31
26
|
return { content: [{ type: "text", text }], details };
|
|
32
27
|
}
|
|
33
28
|
|
|
34
|
-
|
|
29
|
+
const PROGRESS_FRAME_MS = 40;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Live trace updates for the card. The first update is immediate (seeds the result slot);
|
|
33
|
+
* later ones are coalesced to one host re-render per frame so a tight loop of nova.calls
|
|
34
|
+
* is not throttled by the TUI. A throwing host callback must never break the run.
|
|
35
|
+
*/
|
|
35
36
|
function progressEmitter(onUpdate) {
|
|
36
|
-
if (!isFunction(onUpdate)) return () => {};
|
|
37
|
-
|
|
37
|
+
if (!isFunction(onUpdate)) return Object.assign(() => {}, { flush() {} });
|
|
38
|
+
let pending = null;
|
|
39
|
+
let timer = null;
|
|
40
|
+
const send = (trace) => {
|
|
38
41
|
try {
|
|
39
42
|
onUpdate({ content: [{ type: "text", text: "" }], details: { trace, running: true } });
|
|
40
43
|
} catch {}
|
|
41
44
|
};
|
|
45
|
+
const flush = () => {
|
|
46
|
+
timer = null;
|
|
47
|
+
if (pending === null) return;
|
|
48
|
+
const trace = pending;
|
|
49
|
+
pending = null;
|
|
50
|
+
send(trace);
|
|
51
|
+
};
|
|
52
|
+
const emit = (trace) => {
|
|
53
|
+
if (timer === null && pending === null) {
|
|
54
|
+
send(trace);
|
|
55
|
+
timer = setTimeout(flush, PROGRESS_FRAME_MS);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
pending = trace;
|
|
59
|
+
if (timer === null) timer = setTimeout(flush, PROGRESS_FRAME_MS);
|
|
60
|
+
};
|
|
61
|
+
emit.flush = () => {
|
|
62
|
+
if (timer !== null) clearTimeout(timer);
|
|
63
|
+
pending = null;
|
|
64
|
+
timer = null;
|
|
65
|
+
};
|
|
66
|
+
return emit;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function sessionStats({ programs, returnedChars, collapsedChars, collapsedRuns }) {
|
|
70
|
+
const total = returnedChars + collapsedChars;
|
|
71
|
+
const pct = total ? Math.round((collapsedChars / total) * 100) : 0;
|
|
72
|
+
return `this session: ${programs} programs · ~${Math.round(returnedChars / 4)} tokens returned · ~${Math.round(collapsedChars / 4)} already-seen tokens not re-sent (${pct}%, ${collapsedRuns} runs)`;
|
|
42
73
|
}
|
|
43
74
|
|
|
44
75
|
function logsBlock(outcome, tail = "") {
|
|
45
76
|
return outcome.logs?.length ? `\n--- logs\n${outcome.logs.join("\n")}${tail}` : "";
|
|
46
77
|
}
|
|
47
78
|
|
|
48
|
-
function errorText(outcome) {
|
|
49
|
-
return `error ${outcome.wallMs}ms: ${outcome.error}${logsBlock(outcome)}`;
|
|
79
|
+
function errorText(outcome, call) {
|
|
80
|
+
return `error #${call} ${outcome.wallMs}ms: ${outcome.error}${logsBlock(outcome)}`;
|
|
50
81
|
}
|
|
51
82
|
|
|
52
|
-
function successText(outcome) {
|
|
83
|
+
function successText(outcome, call) {
|
|
53
84
|
const truncated = outcome.returnTruncated ? " [return truncated]" : "";
|
|
54
|
-
const hint = outcome.undefinedReturn ? " (no return statement
|
|
55
|
-
return `ok ${outcome.wallMs}ms${truncated}${logsBlock(outcome, "\n--- result")}\n${outcome.resultText}${hint}`;
|
|
85
|
+
const hint = outcome.undefinedReturn ? " (no return statement; add \`return\` to get a value)" : "";
|
|
86
|
+
return `ok #${call} ${outcome.wallMs}ms${truncated}${logsBlock(outcome, "\n--- result")}\n${outcome.resultText}${hint}`;
|
|
56
87
|
}
|
|
57
|
-
function unwrapStructuredResult(response, operation) {
|
|
58
|
-
if (response?.ok === false) {
|
|
59
|
-
throw new Error(response.value || response.error || `${operation} failed`);
|
|
60
|
-
}
|
|
61
|
-
const value = isObject(response) && "value" in response ? response.value : response;
|
|
62
|
-
if (!isString(value)) return value;
|
|
63
|
-
try {
|
|
64
|
-
return JSON.parse(value);
|
|
65
|
-
} catch {
|
|
66
|
-
return value;
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
88
|
const TOOL_DESCRIPTION = `Run one JavaScript program that composes host tools. Async body or arrow; \`return\` a small shaped value (compact literal, capped; strings raw; console.log is captured).
|
|
71
89
|
|
|
72
90
|
Globals (async):
|
|
73
|
-
read(path|paths, offset?, limit?) → text | text[]
|
|
74
|
-
write(path, text) · edit(path, oldText, newText) · patch(path, unifiedDiff)
|
|
91
|
+
read(path|paths, offset?, limit?) → text | text[] · read(path, {about}) → whole-file outline, only relevant bodies expanded
|
|
92
|
+
write(path, text) · edit(path, oldText, newText) → post-edit lines (no re-read needed) · patch(path, unifiedDiff)
|
|
75
93
|
bash(cmd, {cwd?, timeoutMs?}) → output, throws on non-zero exit · exec(cmd, argv?) quotes argv
|
|
94
|
+
evidence(query, {k?}) → {spans: [{path, lines, name, text}]} top-K spans that answer a question; use before read
|
|
76
95
|
snap(query, root?) → {path, line, signature, context} · surface(path) → {items: [{name, kind, line}]}
|
|
77
96
|
nova.call(name, args) → {ok, value} for any host tool · nova.callMany([{name, args}]) parallel when read-only
|
|
78
97
|
nova.search(query) → [{name, description}] · nova.describe(name) → parameters · nova.has(name) sync
|
|
79
|
-
parallel(thunks) · pipeline(items, ...stages)
|
|
98
|
+
parallel(thunks) · pipeline(items, ...stages)
|
|
99
|
+
|
|
100
|
+
Already-seen lines collapse to "⋯ N lines same as #12 · path:a–b ⋯"; read(path, a, n) re-shows them.`;
|
|
80
101
|
|
|
81
102
|
export default function piSupernova(pi) {
|
|
82
103
|
const config = loadConfig();
|
|
83
104
|
let cwd = process.cwd();
|
|
84
105
|
let catalog = [];
|
|
106
|
+
let programSeq = 0;
|
|
85
107
|
|
|
86
108
|
const bridge = createHostBridge({
|
|
87
109
|
pi,
|
|
@@ -133,12 +155,6 @@ export default function piSupernova(pi) {
|
|
|
133
155
|
const cat = catalog.length ? catalog : refreshCatalog();
|
|
134
156
|
return [...new Set([...cat.map((t) => t.name), ...bridge.executors.keys(), ...Object.keys(bridge.natives)])];
|
|
135
157
|
},
|
|
136
|
-
async surface(filePath) {
|
|
137
|
-
return unwrapStructuredResult(await bridge.call("surface", { path: filePath }), "surface");
|
|
138
|
-
},
|
|
139
|
-
async snap(query, targetPath) {
|
|
140
|
-
return unwrapStructuredResult(await bridge.call("snap", { query, path: targetPath }), "snap");
|
|
141
|
-
},
|
|
142
158
|
};
|
|
143
159
|
}
|
|
144
160
|
|
|
@@ -148,7 +164,7 @@ export default function piSupernova(pi) {
|
|
|
148
164
|
description: TOOL_DESCRIPTION,
|
|
149
165
|
promptSnippet: "Compose host tools in one JavaScript program",
|
|
150
166
|
promptGuidelines: [
|
|
151
|
-
"Use supernova for multi-step tool work: loops, filtering, parallel reads, read→edit chains. Return a compact shaped value; keep raw tool output inside the program.",
|
|
167
|
+
"Use supernova for multi-step tool work: loops, filtering, parallel reads, read→edit chains. To understand code: evidence(question) across the repo, or read(path, {about: question}) for one file: full structure, only relevant bodies expanded. Plain read(path) only for lines you will edit. Return a compact shaped value; keep raw tool output inside the program.",
|
|
152
168
|
],
|
|
153
169
|
parameters: Type.Object({
|
|
154
170
|
code: Type.String({ description: "JavaScript program: async body or arrow function." }),
|
|
@@ -171,6 +187,8 @@ export default function piSupernova(pi) {
|
|
|
171
187
|
bridge.resetCallBudget();
|
|
172
188
|
refreshCatalog();
|
|
173
189
|
bridge.beginSpeculation();
|
|
190
|
+
const call = ++programSeq;
|
|
191
|
+
bridge.ledger.beginProgram(call);
|
|
174
192
|
const emitProgress = progressEmitter(onUpdate);
|
|
175
193
|
bridge.setCallListener((_record, allTrace) => emitProgress(allTrace));
|
|
176
194
|
emitProgress([]);
|
|
@@ -180,16 +198,17 @@ export default function piSupernova(pi) {
|
|
|
180
198
|
outcome = await runProgram(params, runController.signal, abortRun);
|
|
181
199
|
} finally {
|
|
182
200
|
bridge.setCallListener(null);
|
|
201
|
+
emitProgress.flush();
|
|
183
202
|
signal?.removeEventListener("abort", abortRun);
|
|
184
203
|
}
|
|
185
204
|
|
|
186
205
|
const trace = bridge.getTrace();
|
|
187
206
|
if (!outcome.ok) {
|
|
188
207
|
bridge.rollbackSpeculation();
|
|
189
|
-
return result(errorText(outcome), { ok: false, error: outcome.error, wallMs: outcome.wallMs, logs: outcome.logs, trace });
|
|
208
|
+
return result(bridge.ledger.dedupe(errorText(outcome, call), call), { ok: false, error: outcome.error, wallMs: outcome.wallMs, logs: outcome.logs, trace });
|
|
190
209
|
}
|
|
191
210
|
await bridge.commitSpeculation();
|
|
192
|
-
return result(successText(outcome), {
|
|
211
|
+
return result(bridge.ledger.dedupe(successText(outcome, call), call), {
|
|
193
212
|
ok: true,
|
|
194
213
|
wallMs: outcome.wallMs,
|
|
195
214
|
returnTruncated: outcome.returnTruncated,
|
|
@@ -221,6 +240,9 @@ export default function piSupernova(pi) {
|
|
|
221
240
|
|
|
222
241
|
pi.on("session_start", (_event, ctx) => {
|
|
223
242
|
if (ctx && isString(ctx.cwd) && ctx.cwd) cwd = ctx.cwd;
|
|
243
|
+
// A new session is a new model context: nothing has been seen yet.
|
|
244
|
+
bridge.ledger.reset();
|
|
245
|
+
programSeq = 0;
|
|
224
246
|
refreshCatalog();
|
|
225
247
|
warmGuestWorker(config).catch(() => {});
|
|
226
248
|
});
|
|
@@ -233,9 +255,10 @@ export default function piSupernova(pi) {
|
|
|
233
255
|
const natives = Object.keys(bridge.natives).sort();
|
|
234
256
|
const lines = [
|
|
235
257
|
`pi-supernova catalog: ${catalog.length} tools`,
|
|
236
|
-
`captured executors: ${captured.length ? captured.join(", ") : "(none yet
|
|
258
|
+
`captured executors: ${captured.length ? captured.join(", ") : "(none yet; load this package early)"}`,
|
|
237
259
|
`native adapters: ${natives.join(", ")}`,
|
|
238
260
|
`timeoutMs=${config.timeoutMs} maxCallResultChars=${config.maxCallResultChars} maxReturnChars=${config.maxReturnChars} maxBridgeCalls=${config.maxBridgeCalls} maxHeapMb=${config.maxHeapMb}`,
|
|
261
|
+
sessionStats(bridge.ledger.stats),
|
|
239
262
|
];
|
|
240
263
|
ctx.ui.notify(lines.join("\n"), "info");
|
|
241
264
|
},
|