pi-supernova 0.0.7 → 0.0.11
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 +56 -0
- package/README.md +27 -16
- package/bottleneck.js +32 -41
- package/catalog.js +72 -22
- package/config.default.json +2 -1
- package/config.js +19 -10
- package/diff.js +12 -4
- package/evidence.js +446 -0
- package/format.js +95 -0
- package/guest-worker.js +345 -0
- package/host-bridge.js +215 -463
- package/index.js +107 -97
- package/omp-frame.js +68 -51
- package/package.json +8 -1
- package/parallel.js +19 -20
- package/patch.js +106 -0
- package/render-measure.js +63 -49
- package/render.js +203 -166
- package/repo-index.js +179 -0
- package/runtime.js +266 -241
- package/snap.js +153 -151
- package/surface.js +16 -30
- package/vfs.js +144 -0
- package/workspace.js +129 -0
package/host-bridge.js
CHANGED
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
|
|
2
2
|
import * as fs from "node:fs/promises";
|
|
3
3
|
import * as path from "node:path";
|
|
4
|
-
import { spawn } from "node:child_process";
|
|
5
4
|
import { packageHostResult } from "./bottleneck.js";
|
|
6
5
|
import { isString, isNumber, isFunction, isObject } from "./decode.js";
|
|
7
6
|
import { isMutatingTool, runParallelWave } from "./parallel.js";
|
|
7
|
+
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, globToRegExp } from "./repo-index.js";
|
|
13
|
+
import { CausalVfs } from "./vfs.js";
|
|
14
|
+
import { applyPatchToText } from "./patch.js";
|
|
15
|
+
import { resolveWorkspacePath, runCommand, clearPathCache } from "./workspace.js";
|
|
11
16
|
|
|
12
17
|
function textResult(text, details) {
|
|
13
18
|
return {
|
|
@@ -27,341 +32,144 @@ function unwrapIfFullyQuoted(s) {
|
|
|
27
32
|
return inner;
|
|
28
33
|
}
|
|
29
34
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
cachedResolvedCwd = path.resolve(cwd);
|
|
37
|
-
return cachedResolvedCwd;
|
|
35
|
+
function sliceLines(text, offset, limit) {
|
|
36
|
+
if (!isNumber(offset) && !isNumber(limit)) return text;
|
|
37
|
+
const lines = text.split("\n");
|
|
38
|
+
const startIndex = (isNumber(offset) ? Math.max(1, Math.floor(offset)) : 1) - 1;
|
|
39
|
+
const count = isNumber(limit) ? Math.max(0, Math.floor(limit)) : lines.length;
|
|
40
|
+
return lines.slice(startIndex, startIndex + count).join("\n");
|
|
38
41
|
}
|
|
39
42
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
throw new Error(`${opName} path escapes workspace`);
|
|
49
|
-
}
|
|
50
|
-
if (!allowRoot && target === resolvedCwd) {
|
|
51
|
-
throw new Error(`${opName} path cannot be the workspace root directory`);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const realRoot = await fs.realpath(resolvedCwd);
|
|
55
|
-
let probe = target;
|
|
56
|
-
while (true) {
|
|
57
|
-
try {
|
|
58
|
-
probe = await fs.realpath(probe);
|
|
59
|
-
break;
|
|
60
|
-
} catch (err) {
|
|
61
|
-
if (err?.code !== "ENOENT" && err?.code !== "ENOTDIR") throw err;
|
|
62
|
-
const parent = path.dirname(probe);
|
|
63
|
-
if (parent === probe) throw err;
|
|
64
|
-
probe = parent;
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
const realRel = path.relative(realRoot, probe);
|
|
68
|
-
if (realRel === ".." || realRel.startsWith(`..${path.sep}`) || path.isAbsolute(realRel)) {
|
|
69
|
-
throw new Error(`${opName} path escapes workspace through symlink`);
|
|
70
|
-
}
|
|
71
|
-
return target;
|
|
43
|
+
function looksLikePath(target) {
|
|
44
|
+
return (
|
|
45
|
+
isString(target) &&
|
|
46
|
+
(target.includes("/") ||
|
|
47
|
+
target.includes("\\") ||
|
|
48
|
+
target.startsWith(".") ||
|
|
49
|
+
(!/\s/.test(target) && path.extname(target).length > 0))
|
|
50
|
+
);
|
|
72
51
|
}
|
|
73
52
|
|
|
74
|
-
async function
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
let stdout = "";
|
|
85
|
-
let stderr = "";
|
|
86
|
-
let settled = false;
|
|
87
|
-
let outputTruncated = false;
|
|
88
|
-
let onAbort;
|
|
89
|
-
|
|
90
|
-
const cleanup = () => {
|
|
91
|
-
clearTimeout(timer);
|
|
92
|
-
if (options.signal && onAbort) options.signal.removeEventListener("abort", onAbort);
|
|
93
|
-
};
|
|
94
|
-
const fail = (err) => {
|
|
95
|
-
if (settled) return;
|
|
96
|
-
settled = true;
|
|
97
|
-
cleanup();
|
|
98
|
-
reject(err);
|
|
99
|
-
};
|
|
100
|
-
const append = (current, chunk) => {
|
|
101
|
-
const remaining = Math.max(0, maxOutputChars - current.length);
|
|
102
|
-
if (chunk.length > remaining) outputTruncated = true;
|
|
103
|
-
return remaining > 0 ? current + chunk.slice(0, remaining) : current;
|
|
104
|
-
};
|
|
105
|
-
const timer = setTimeout(() => {
|
|
106
|
-
child.kill("SIGTERM");
|
|
107
|
-
fail(new Error(`command timed out after ${timeoutMs}ms: ${argv.join(" ")}`));
|
|
108
|
-
}, timeoutMs);
|
|
109
|
-
|
|
110
|
-
child.stdout.setEncoding("utf8");
|
|
111
|
-
child.stderr.setEncoding("utf8");
|
|
112
|
-
child.stdout.on("data", (chunk) => {
|
|
113
|
-
stdout = append(stdout, chunk);
|
|
114
|
-
});
|
|
115
|
-
child.stderr.on("data", (chunk) => {
|
|
116
|
-
stderr = append(stderr, chunk);
|
|
117
|
-
});
|
|
118
|
-
child.on("error", fail);
|
|
119
|
-
child.on("close", (code) => {
|
|
120
|
-
if (settled) return;
|
|
121
|
-
settled = true;
|
|
122
|
-
cleanup();
|
|
123
|
-
resolve({ stdout, stderr, exitCode: code ?? 0, outputTruncated });
|
|
124
|
-
});
|
|
125
|
-
if (options.signal) {
|
|
126
|
-
onAbort = () => {
|
|
127
|
-
child.kill("SIGTERM");
|
|
128
|
-
fail(new Error("aborted"));
|
|
129
|
-
};
|
|
130
|
-
if (options.signal.aborted) onAbort();
|
|
131
|
-
else options.signal.addEventListener("abort", onAbort, { once: true });
|
|
132
|
-
}
|
|
133
|
-
});
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
function parseHunkHeader(line) {
|
|
137
|
-
const match = /^@@\s+-(\d+)(?:,(\d+))?\s+\+(\d+)(?:,(\d+))?\s+@@/.exec(line);
|
|
138
|
-
if (!match) return null;
|
|
139
|
-
return {
|
|
140
|
-
oldStart: parseInt(match[1], 10),
|
|
141
|
-
oldLength: match[2] !== undefined ? parseInt(match[2], 10) : 1,
|
|
142
|
-
newStart: parseInt(match[3], 10),
|
|
143
|
-
newLength: match[4] !== undefined ? parseInt(match[4], 10) : 1,
|
|
144
|
-
lines: [],
|
|
145
|
-
};
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
export function parsePatchHunks(patchText) {
|
|
149
|
-
const patchLines = patchText.replace(/\r\n/g, "\n").split("\n");
|
|
150
|
-
const hunks = [];
|
|
151
|
-
let current = null;
|
|
152
|
-
|
|
153
|
-
for (const line of patchLines) {
|
|
154
|
-
const header = parseHunkHeader(line);
|
|
155
|
-
if (header) {
|
|
156
|
-
if (current) hunks.push(current);
|
|
157
|
-
current = header;
|
|
158
|
-
} else if (current && (line.startsWith("+") || line.startsWith("-") || line.startsWith(" "))) {
|
|
159
|
-
current.lines.push(line);
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
if (current) hunks.push(current);
|
|
163
|
-
if (hunks.length === 0) {
|
|
164
|
-
throw new Error("no valid patch hunks found (expected @@ -old,len +new,len @@)");
|
|
165
|
-
}
|
|
166
|
-
return hunks;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
function findHunkMatch(fileLines, expectedOld, nominal) {
|
|
170
|
-
const matchAt = (idx) => {
|
|
171
|
-
if (idx < 0 || idx + expectedOld.length > fileLines.length) return false;
|
|
172
|
-
for (let j = 0; j < expectedOld.length; j++) {
|
|
173
|
-
if (fileLines[idx + j] !== expectedOld[j]) return false;
|
|
174
|
-
}
|
|
175
|
-
return true;
|
|
176
|
-
};
|
|
177
|
-
|
|
178
|
-
if (matchAt(nominal)) return nominal;
|
|
179
|
-
const maxDelta = Math.max(fileLines.length, 100);
|
|
180
|
-
for (let delta = 1; delta <= maxDelta; delta++) {
|
|
181
|
-
if (matchAt(nominal + delta)) return nominal + delta;
|
|
182
|
-
if (matchAt(nominal - delta)) return nominal - delta;
|
|
53
|
+
async function probeExistingFile(cwd, targetParam, vfs) {
|
|
54
|
+
const targetPath = await resolveWorkspacePath(cwd, targetParam, "read", false);
|
|
55
|
+
if (vfs.getOverlay(targetPath) !== undefined || vfs.cache.has(targetPath)) return targetPath;
|
|
56
|
+
try {
|
|
57
|
+
const st = await fs.stat(targetPath);
|
|
58
|
+
if (st.isDirectory()) throw new Error(`read path is a directory, not a file: ${targetPath} (use ls)`);
|
|
59
|
+
return targetPath;
|
|
60
|
+
} catch (err) {
|
|
61
|
+
if (err?.code !== "ENOENT" && err?.code !== "ENOTDIR") throw err;
|
|
62
|
+
return null;
|
|
183
63
|
}
|
|
184
|
-
return -1;
|
|
185
64
|
}
|
|
186
65
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
const hasTrailingNewline = originalText.endsWith("\n");
|
|
195
|
-
let offsetShift = 0;
|
|
196
|
-
|
|
197
|
-
for (let h = 0; h < hunks.length; h++) {
|
|
198
|
-
const hunk = hunks[h];
|
|
199
|
-
const expectedOld = [];
|
|
200
|
-
const newLines = [];
|
|
201
|
-
|
|
202
|
-
for (const hLine of hunk.lines) {
|
|
203
|
-
if (hLine.startsWith("-")) {
|
|
204
|
-
expectedOld.push(hLine.slice(1));
|
|
205
|
-
} else if (hLine.startsWith("+")) {
|
|
206
|
-
newLines.push(hLine.slice(1));
|
|
207
|
-
} else {
|
|
208
|
-
const val = hLine.startsWith(" ") ? hLine.slice(1) : "";
|
|
209
|
-
expectedOld.push(val);
|
|
210
|
-
newLines.push(val);
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
if (expectedOld.length !== hunk.oldLength || newLines.length !== hunk.newLength) {
|
|
215
|
-
throw new Error(`patch hunk ${h + 1} length does not match its header`);
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
const nominal = Math.max(0, hunk.oldStart - 1 + offsetShift);
|
|
219
|
-
const matchIdx = findHunkMatch(fileLines, expectedOld, nominal);
|
|
220
|
-
if (matchIdx === -1) {
|
|
221
|
-
throw new Error(`patch hunk ${h + 1} rejected at line ${hunk.oldStart}: context did not match`);
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
fileLines.splice(matchIdx, expectedOld.length, ...newLines);
|
|
225
|
-
offsetShift += (matchIdx - nominal) + (newLines.length - expectedOld.length);
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
let resultText = fileLines.join("\n");
|
|
229
|
-
if (hasTrailingNewline && !resultText.endsWith("\n")) resultText += "\n";
|
|
230
|
-
return { resultText, hunkCount: hunks.length };
|
|
66
|
+
function patchModeOf(params) {
|
|
67
|
+
return (
|
|
68
|
+
isString(params?.patch) ||
|
|
69
|
+
(params?.newText === undefined &&
|
|
70
|
+
isString(params?.oldText) &&
|
|
71
|
+
(params.oldText.includes("@@ -") || params.oldText.startsWith("---")))
|
|
72
|
+
);
|
|
231
73
|
}
|
|
232
74
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
this.overlays = [];
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
setCache(target, content) {
|
|
242
|
-
if (this.cache.size >= VFS_CACHE_MAX && !this.cache.has(target)) {
|
|
243
|
-
const oldest = this.cache.keys().next().value;
|
|
244
|
-
if (oldest !== undefined) this.cache.delete(oldest);
|
|
75
|
+
function applyReplacements(target, content, requestedEdits) {
|
|
76
|
+
if (requestedEdits.length === 0) throw new Error("edit requires at least one replacement");
|
|
77
|
+
const matches = requestedEdits.map((replacement) => {
|
|
78
|
+
if (!isString(replacement?.oldText) || replacement.oldText.length === 0) {
|
|
79
|
+
throw new Error("edit requires non-empty oldText");
|
|
245
80
|
}
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
for (let i = this.overlays.length - 1; i >= 0; i--) {
|
|
251
|
-
if (this.overlays[i].has(target)) return this.overlays[i].get(target);
|
|
81
|
+
if (!isString(replacement?.newText)) throw new Error("edit requires newText");
|
|
82
|
+
const index = content.indexOf(replacement.oldText);
|
|
83
|
+
if (index < 0) {
|
|
84
|
+
throw new Error(`edit target not found in ${target}: oldText must match the file byte-for-byte (read() it first; check whitespace and quotes)`);
|
|
252
85
|
}
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
getOverlayPaths() {
|
|
257
|
-
const paths = new Set();
|
|
258
|
-
for (const overlay of this.overlays) {
|
|
259
|
-
for (const target of overlay.keys()) paths.add(target);
|
|
86
|
+
if (content.indexOf(replacement.oldText, index + replacement.oldText.length) >= 0) {
|
|
87
|
+
throw new Error(`edit target is not unique in ${target}: include more surrounding lines in oldText, or pass edits:[{oldText,newText},…]`);
|
|
260
88
|
}
|
|
261
|
-
return
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
if (overlay !== undefined) return overlay;
|
|
267
|
-
|
|
268
|
-
const cached = this.cache.get(target);
|
|
269
|
-
if (cached !== undefined) return cached;
|
|
270
|
-
|
|
271
|
-
try {
|
|
272
|
-
const text = await fs.readFile(target, "utf8");
|
|
273
|
-
this.setCache(target, text);
|
|
274
|
-
return text;
|
|
275
|
-
} catch (err) {
|
|
276
|
-
if (err.code === "EISDIR") {
|
|
277
|
-
throw new Error(`read path is a directory, not a file: ${target}`);
|
|
278
|
-
}
|
|
279
|
-
throw err;
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
async write(target, content) {
|
|
284
|
-
if (this.overlays.length > 0) {
|
|
285
|
-
this.overlays[this.overlays.length - 1].set(target, content);
|
|
286
|
-
return { speculative: true };
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
try {
|
|
290
|
-
const stat = await fs.stat(target);
|
|
291
|
-
if (stat.isDirectory()) {
|
|
292
|
-
throw new Error(`cannot write to a directory: ${target}`);
|
|
293
|
-
}
|
|
294
|
-
} catch (err) {
|
|
295
|
-
if (err.code !== "ENOENT") throw err;
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
await fs.mkdir(path.dirname(target), { recursive: true });
|
|
299
|
-
await fs.writeFile(target, content, "utf8");
|
|
300
|
-
this.setCache(target, content);
|
|
301
|
-
return { speculative: false };
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
begin() {
|
|
305
|
-
this.overlays.push(new Map());
|
|
306
|
-
return this.overlays.length;
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
async commit() {
|
|
310
|
-
if (this.overlays.length === 0) return { committed: 0, depth: 0 };
|
|
311
|
-
const top = this.overlays.pop();
|
|
312
|
-
if (this.overlays.length > 0) {
|
|
313
|
-
const parent = this.overlays[this.overlays.length - 1];
|
|
314
|
-
for (const [k, v] of top.entries()) parent.set(k, v);
|
|
315
|
-
return { committed: top.size, depth: this.overlays.length };
|
|
316
|
-
}
|
|
317
|
-
for (const [filePath, fileContent] of top.entries()) {
|
|
318
|
-
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
319
|
-
await fs.writeFile(filePath, fileContent, "utf8");
|
|
320
|
-
this.setCache(filePath, fileContent);
|
|
321
|
-
}
|
|
322
|
-
return { committed: top.size, depth: 0 };
|
|
89
|
+
return { ...replacement, index, end: index + replacement.oldText.length };
|
|
90
|
+
});
|
|
91
|
+
matches.sort((a, b) => a.index - b.index);
|
|
92
|
+
for (let i = 1; i < matches.length; i++) {
|
|
93
|
+
if (matches[i].index < matches[i - 1].end) throw new Error(`edit targets overlap in ${target}`);
|
|
323
94
|
}
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
return { rolledBack: top.size, depth: this.overlays.length };
|
|
95
|
+
let updated = content;
|
|
96
|
+
for (let i = matches.length - 1; i >= 0; i--) {
|
|
97
|
+
const match = matches[i];
|
|
98
|
+
updated = updated.slice(0, match.index) + match.newText + updated.slice(match.end);
|
|
329
99
|
}
|
|
100
|
+
return { updated, matches };
|
|
101
|
+
}
|
|
330
102
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
await fs.
|
|
339
|
-
|
|
340
|
-
this.setCache(filePath, fileContent);
|
|
103
|
+
async function formatLsEntry(dirPath, entry) {
|
|
104
|
+
const isDir = entry.isDirectory();
|
|
105
|
+
const isSym = entry.isSymbolicLink();
|
|
106
|
+
const typeLabel = isDir ? "dir" : isSym ? "sym" : "file";
|
|
107
|
+
let size = 0;
|
|
108
|
+
try {
|
|
109
|
+
if (!isDir && !isSym) {
|
|
110
|
+
const st = await fs.stat(path.join(dirPath, entry.name));
|
|
111
|
+
size = st.size;
|
|
341
112
|
}
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
}
|
|
113
|
+
} catch {}
|
|
114
|
+
const sizeSuffix = size ? `, ${size} bytes` : "";
|
|
115
|
+
return `${entry.name}${isDir ? "/" : ""} (${typeLabel}${sizeSuffix})`;
|
|
116
|
+
}
|
|
345
117
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
118
|
+
function rgGrepArgs(pattern, params, searchPath) {
|
|
119
|
+
const args = ["--line-number", "--no-heading", "--color", "never"];
|
|
120
|
+
if (params?.caseSensitive !== true) args.push("--ignore-case");
|
|
121
|
+
if (params?.glob) args.push("--glob", String(params.glob));
|
|
122
|
+
args.push("--", pattern, searchPath);
|
|
123
|
+
return args;
|
|
124
|
+
}
|
|
349
125
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
126
|
+
/** rg --files, then find(1) when rg is unavailable; both accept an optional glob/name pattern. */
|
|
127
|
+
async function listWithTools(searchDir, pattern, cwd, signal) {
|
|
128
|
+
const args = ["--files"];
|
|
129
|
+
if (pattern) args.push("-g", pattern);
|
|
130
|
+
const res = await runCommand(["rg", ...args, searchDir], { cwd, timeoutMs: 30_000, signal }).catch(() => null);
|
|
131
|
+
if (res && (res.exitCode === 0 || res.exitCode === 1)) return textResult(res.stdout, { via: "rg" });
|
|
132
|
+
const findArgs = [searchDir];
|
|
133
|
+
if (pattern) findArgs.push("-name", pattern);
|
|
134
|
+
const findRes = await runCommand(["find", ...findArgs], { cwd, timeoutMs: 30_000, signal });
|
|
135
|
+
return textResult(findRes.stdout, { via: "find" });
|
|
136
|
+
}
|
|
354
137
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
138
|
+
/** rg-compatible grep served from the index; null when the pattern or tree needs real rg. */
|
|
139
|
+
async function grepIndexed(index, pattern, params, searchPath, cwd) {
|
|
140
|
+
let regex;
|
|
141
|
+
try {
|
|
142
|
+
regex = new RegExp(pattern, params?.caseSensitive === true ? "" : "i");
|
|
143
|
+
} catch {
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
let files = await index.files(searchPath);
|
|
147
|
+
if (!index.canScan(files)) return null;
|
|
148
|
+
if (params?.glob) {
|
|
149
|
+
const matcher = globToRegExp(String(params.glob));
|
|
150
|
+
files = files.filter((f) => matcher.test(path.relative(cwd, f).split(path.sep).join("/")));
|
|
151
|
+
}
|
|
152
|
+
const rows = index.grep(files, regex, cwd);
|
|
153
|
+
return rows.length ? rows.join("\n") + "\n" : "";
|
|
154
|
+
}
|
|
358
155
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
156
|
+
/** rg --files [-g pattern] served from the index; null when the tree is too large. */
|
|
157
|
+
async function listIndexed(index, root, cwd, pattern) {
|
|
158
|
+
const files = await index.files(root);
|
|
159
|
+
if (!index.canScan(files)) return null;
|
|
160
|
+
const rel = files.map((f) => path.relative(cwd, f).split(path.sep).join("/"));
|
|
161
|
+
if (!pattern) return rel.length ? rel.join("\n") + "\n" : "";
|
|
162
|
+
let matcher;
|
|
163
|
+
try {
|
|
164
|
+
matcher = globToRegExp(pattern);
|
|
165
|
+
} catch {
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
const hits = rel.filter((f) => matcher.test(f));
|
|
169
|
+
return hits.length ? hits.join("\n") + "\n" : "";
|
|
362
170
|
}
|
|
363
171
|
|
|
364
|
-
function createNativeAdapters(getCwd, vfs, config) {
|
|
172
|
+
function createNativeAdapters(getCwd, vfs, config, index) {
|
|
365
173
|
async function readAdapter(params, signal) {
|
|
366
174
|
const cwd = getCwd();
|
|
367
175
|
const targetParam = params?.path ?? params?.target;
|
|
@@ -370,57 +178,20 @@ function createNativeAdapters(getCwd, vfs, config) {
|
|
|
370
178
|
const results = await Promise.all(
|
|
371
179
|
targetParam.map((p) => readAdapter({ path: p, offset: params?.offset, limit: params?.limit }, signal)),
|
|
372
180
|
);
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
batch: true,
|
|
376
|
-
items: results.map((r) => r.value),
|
|
377
|
-
});
|
|
181
|
+
const items = results.map((r) => r.content[0].text);
|
|
182
|
+
return textResult(items.join("\n---\n"), { count: results.length, batch: true, items });
|
|
378
183
|
}
|
|
379
184
|
|
|
380
|
-
|
|
381
|
-
isString(targetParam) &&
|
|
382
|
-
(targetParam.includes("/") ||
|
|
383
|
-
targetParam.includes("\\") ||
|
|
384
|
-
targetParam.startsWith(".") ||
|
|
385
|
-
(!/\s/.test(targetParam) && path.extname(targetParam).length > 0));
|
|
386
|
-
|
|
387
|
-
if (looksLikePath) {
|
|
185
|
+
if (looksLikePath(targetParam)) {
|
|
388
186
|
const targetPath = await resolveWorkspacePath(cwd, targetParam, "read", false);
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
const lines = text.split("\n");
|
|
392
|
-
const offset = isNumber(params?.offset) ? Math.max(1, Math.floor(params.offset)) : 1;
|
|
393
|
-
const startIndex = offset - 1;
|
|
394
|
-
const limit = isNumber(params?.limit) ? Math.max(0, Math.floor(params.limit)) : lines.length;
|
|
395
|
-
text = lines.slice(startIndex, startIndex + limit).join("\n");
|
|
396
|
-
}
|
|
397
|
-
return textResult(text, { path: targetPath });
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
let isExistingFile = false;
|
|
401
|
-
let targetPath = await resolveWorkspacePath(cwd, targetParam, "read", false);
|
|
402
|
-
const overlay = vfs.getOverlay(targetPath);
|
|
403
|
-
if (overlay !== undefined || vfs.cache.has(targetPath)) {
|
|
404
|
-
isExistingFile = true;
|
|
405
|
-
} else {
|
|
406
|
-
try {
|
|
407
|
-
const st = await fs.stat(targetPath);
|
|
408
|
-
isExistingFile = !st.isDirectory();
|
|
409
|
-
} catch (err) {
|
|
410
|
-
if (err?.code !== "ENOENT" && err?.code !== "ENOTDIR") throw err;
|
|
411
|
-
}
|
|
187
|
+
const text = await vfs.read(targetPath);
|
|
188
|
+
return textResult(sliceLines(text, params?.offset, params?.limit), { path: targetPath });
|
|
412
189
|
}
|
|
413
190
|
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
const offset = isNumber(params?.offset) ? Math.max(1, Math.floor(params.offset)) : 1;
|
|
419
|
-
const startIndex = offset - 1;
|
|
420
|
-
const limit = isNumber(params?.limit) ? Math.max(0, Math.floor(params.limit)) : lines.length;
|
|
421
|
-
text = lines.slice(startIndex, startIndex + limit).join("\n");
|
|
422
|
-
}
|
|
423
|
-
return textResult(text, { path: targetPath });
|
|
191
|
+
const existing = await probeExistingFile(cwd, targetParam, vfs);
|
|
192
|
+
if (existing) {
|
|
193
|
+
const text = await vfs.read(existing);
|
|
194
|
+
return textResult(sliceLines(text, params?.offset, params?.limit), { path: existing });
|
|
424
195
|
}
|
|
425
196
|
|
|
426
197
|
if (isString(targetParam) && targetParam.trim()) {
|
|
@@ -428,24 +199,17 @@ function createNativeAdapters(getCwd, vfs, config) {
|
|
|
428
199
|
const snapRes = await executeSnap({
|
|
429
200
|
query: targetParam,
|
|
430
201
|
searchDir: cwd,
|
|
431
|
-
|
|
432
|
-
|
|
202
|
+
index,
|
|
203
|
+
overlayText: (p) => vfs.getOverlay(p),
|
|
204
|
+
pendingPaths: vfs.getOverlayPaths(),
|
|
433
205
|
});
|
|
434
206
|
return textResult(JSON.stringify(snapRes, null, 2), { ...snapRes, isSnap: true });
|
|
435
207
|
} catch {}
|
|
436
208
|
}
|
|
437
209
|
|
|
438
|
-
targetPath = await resolveWorkspacePath(cwd, targetParam, "read", false);
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
if (isNumber(params?.offset) || isNumber(params?.limit)) {
|
|
442
|
-
const lines = text.split("\n");
|
|
443
|
-
const offset = isNumber(params?.offset) ? Math.max(1, Math.floor(params.offset)) : 1;
|
|
444
|
-
const startIndex = offset - 1;
|
|
445
|
-
const limit = isNumber(params?.limit) ? Math.max(0, Math.floor(params.limit)) : lines.length;
|
|
446
|
-
text = lines.slice(startIndex, startIndex + limit).join("\n");
|
|
447
|
-
}
|
|
448
|
-
return textResult(text, { path: targetPath });
|
|
210
|
+
const targetPath = await resolveWorkspacePath(cwd, targetParam, "read", false);
|
|
211
|
+
const text = await vfs.read(targetPath);
|
|
212
|
+
return textResult(sliceLines(text, params?.offset, params?.limit), { path: targetPath });
|
|
449
213
|
}
|
|
450
214
|
|
|
451
215
|
return {
|
|
@@ -469,11 +233,7 @@ function createNativeAdapters(getCwd, vfs, config) {
|
|
|
469
233
|
const target = await resolveWorkspacePath(cwd, params?.path, "edit", false);
|
|
470
234
|
if (signal?.aborted) throw new Error("aborted");
|
|
471
235
|
|
|
472
|
-
|
|
473
|
-
isString(params?.patch) ||
|
|
474
|
-
(params?.newText === undefined && isString(params?.oldText) && (params.oldText.includes("@@ -") || params.oldText.startsWith("---")));
|
|
475
|
-
|
|
476
|
-
if (isPatchMode) {
|
|
236
|
+
if (patchModeOf(params)) {
|
|
477
237
|
const patchContent = params.patch || params.oldText;
|
|
478
238
|
const original = await vfs.read(target);
|
|
479
239
|
const { resultText, hunkCount } = applyPatchToText(original, patchContent);
|
|
@@ -491,31 +251,8 @@ function createNativeAdapters(getCwd, vfs, config) {
|
|
|
491
251
|
const requestedEdits = Array.isArray(params?.edits)
|
|
492
252
|
? params.edits
|
|
493
253
|
: [{ oldText: params?.oldText, newText: params?.newText }];
|
|
494
|
-
if (requestedEdits.length === 0) throw new Error("edit requires at least one replacement");
|
|
495
|
-
|
|
496
254
|
const content = await vfs.read(target);
|
|
497
|
-
const matches =
|
|
498
|
-
if (!isString(replacement?.oldText) || replacement.oldText.length === 0) {
|
|
499
|
-
throw new Error("edit requires non-empty oldText");
|
|
500
|
-
}
|
|
501
|
-
if (!isString(replacement?.newText)) throw new Error("edit requires newText");
|
|
502
|
-
const index = content.indexOf(replacement.oldText);
|
|
503
|
-
if (index < 0) throw new Error(`edit target not found in ${target}`);
|
|
504
|
-
if (content.indexOf(replacement.oldText, index + replacement.oldText.length) >= 0) {
|
|
505
|
-
throw new Error(`edit target is not unique in ${target}`);
|
|
506
|
-
}
|
|
507
|
-
return { ...replacement, index, end: index + replacement.oldText.length };
|
|
508
|
-
});
|
|
509
|
-
matches.sort((a, b) => a.index - b.index);
|
|
510
|
-
for (let i = 1; i < matches.length; i++) {
|
|
511
|
-
if (matches[i].index < matches[i - 1].end) throw new Error(`edit targets overlap in ${target}`);
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
let updated = content;
|
|
515
|
-
for (let i = matches.length - 1; i >= 0; i--) {
|
|
516
|
-
const match = matches[i];
|
|
517
|
-
updated = updated.slice(0, match.index) + match.newText + updated.slice(match.end);
|
|
518
|
-
}
|
|
255
|
+
const { updated, matches } = applyReplacements(target, content, requestedEdits);
|
|
519
256
|
const { speculative } = await vfs.write(target, updated);
|
|
520
257
|
const diff =
|
|
521
258
|
matches.length === 1
|
|
@@ -563,19 +300,25 @@ function createNativeAdapters(getCwd, vfs, config) {
|
|
|
563
300
|
const res = await executeSnap({
|
|
564
301
|
query: params.query,
|
|
565
302
|
searchDir: snapTarget,
|
|
303
|
+
root: cwd,
|
|
566
304
|
includeHidden,
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
// Jail each snap candidate (symlink files must not escape the workspace).
|
|
570
|
-
const jailed = await resolveWorkspacePath(cwd, candidate, "snap", false);
|
|
571
|
-
return vfs.read(jailed);
|
|
572
|
-
},
|
|
573
|
-
},
|
|
574
|
-
runCommand: (argv, opts) => runCommand(argv, { cwd: snapTarget, signal, ...opts }),
|
|
305
|
+
index,
|
|
306
|
+
overlayText: (p) => vfs.getOverlay(p),
|
|
575
307
|
pendingPaths: vfs.getOverlayPaths(),
|
|
576
308
|
});
|
|
577
309
|
return textResult(JSON.stringify(res, null, 2), res);
|
|
578
310
|
},
|
|
311
|
+
async evidence(params, signal) {
|
|
312
|
+
const cwd = getCwd();
|
|
313
|
+
if (!isString(params?.query) || !params.query.trim()) throw new Error("evidence requires query");
|
|
314
|
+
if (signal?.aborted) throw new Error("aborted");
|
|
315
|
+
const searchDir = params?.path ? await resolveWorkspacePath(cwd, params.path, "evidence", true) : cwd;
|
|
316
|
+
const options = {};
|
|
317
|
+
if (Number.isInteger(params?.k) && params.k > 0) options.k = params.k;
|
|
318
|
+
if (Number.isInteger(params?.maxChars) && params.maxChars > 0) options.maxChars = params.maxChars;
|
|
319
|
+
const res = await selectEvidence({ query: params.query, root: cwd, searchDir, index, overlayText: (p) => vfs.getOverlay(p), options });
|
|
320
|
+
return textResult(JSON.stringify(res), { route: res.route, count: res.spans.length });
|
|
321
|
+
},
|
|
579
322
|
async surface(params, signal) {
|
|
580
323
|
const cwd = getCwd();
|
|
581
324
|
const target = await resolveWorkspacePath(cwd, params?.path, "surface", false);
|
|
@@ -606,8 +349,10 @@ function createNativeAdapters(getCwd, vfs, config) {
|
|
|
606
349
|
});
|
|
607
350
|
} finally {
|
|
608
351
|
vfs.invalidateCache();
|
|
352
|
+
index.invalidate();
|
|
609
353
|
}
|
|
610
|
-
const
|
|
354
|
+
const { stdout, stderr } = res;
|
|
355
|
+
const text = stdout && stderr ? stdout + (stdout.endsWith("\n") ? "" : "\n") + stderr : stdout || stderr;
|
|
611
356
|
return {
|
|
612
357
|
content: [{ type: "text", text }],
|
|
613
358
|
details: { exitCode: res.exitCode, outputTruncated: res.outputTruncated, transactionBarrier },
|
|
@@ -619,11 +364,9 @@ function createNativeAdapters(getCwd, vfs, config) {
|
|
|
619
364
|
const pattern = String(params?.pattern || "");
|
|
620
365
|
if (!pattern) throw new Error("grep requires pattern");
|
|
621
366
|
const searchPath = params?.path ? await resolveWorkspacePath(cwd, params.path, "grep", true) : cwd;
|
|
622
|
-
const
|
|
623
|
-
if (
|
|
624
|
-
|
|
625
|
-
args.push("--", pattern, searchPath);
|
|
626
|
-
const res = await runCommand(["rg", ...args], { cwd, timeoutMs: 30_000, signal });
|
|
367
|
+
const indexed = await grepIndexed(index, pattern, params, searchPath, cwd);
|
|
368
|
+
if (indexed !== null) return textResult(indexed, { exitCode: indexed ? 0 : 1, via: "index" });
|
|
369
|
+
const res = await runCommand(["rg", ...rgGrepArgs(pattern, params, searchPath)], { cwd, timeoutMs: 30_000, signal });
|
|
627
370
|
if (res.exitCode !== 0 && res.exitCode !== 1) {
|
|
628
371
|
throw new Error(res.stderr.trim() || `rg exited ${res.exitCode}`);
|
|
629
372
|
}
|
|
@@ -633,6 +376,8 @@ function createNativeAdapters(getCwd, vfs, config) {
|
|
|
633
376
|
const cwd = getCwd();
|
|
634
377
|
const pattern = String(params?.pattern || "");
|
|
635
378
|
if (!pattern) throw new Error("glob requires pattern");
|
|
379
|
+
const indexed = await listIndexed(index, cwd, cwd, pattern);
|
|
380
|
+
if (indexed !== null) return textResult(indexed, { via: "index" });
|
|
636
381
|
const rg = await runCommand(["rg", "--files", "-g", pattern], { cwd, timeoutMs: 30_000, signal }).catch(
|
|
637
382
|
() => null,
|
|
638
383
|
);
|
|
@@ -652,16 +397,10 @@ function createNativeAdapters(getCwd, vfs, config) {
|
|
|
652
397
|
const searchDir = params?.path ? await resolveWorkspacePath(cwd, params.path, "find", true) : cwd;
|
|
653
398
|
const pattern = params?.pattern || params?.glob;
|
|
654
399
|
if (signal?.aborted) throw new Error("aborted");
|
|
655
|
-
const
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
return textResult(res.stdout, { via: "rg" });
|
|
660
|
-
}
|
|
661
|
-
const findArgs = [searchDir];
|
|
662
|
-
if (pattern) findArgs.push("-name", String(pattern));
|
|
663
|
-
const findRes = await runCommand(["find", ...findArgs], { cwd, timeoutMs: 30_000, signal });
|
|
664
|
-
return textResult(findRes.stdout, { via: "find" });
|
|
400
|
+
const globPattern = pattern ? String(pattern) : null;
|
|
401
|
+
const indexed = await listIndexed(index, searchDir, cwd, globPattern);
|
|
402
|
+
if (indexed !== null) return textResult(indexed, { via: "index" });
|
|
403
|
+
return listWithTools(searchDir, globPattern, cwd, signal);
|
|
665
404
|
},
|
|
666
405
|
async ls(params, signal) {
|
|
667
406
|
const cwd = getCwd();
|
|
@@ -670,17 +409,7 @@ function createNativeAdapters(getCwd, vfs, config) {
|
|
|
670
409
|
const entries = await fs.readdir(dirPath, { withFileTypes: true });
|
|
671
410
|
const lines = [];
|
|
672
411
|
for (const entry of entries) {
|
|
673
|
-
|
|
674
|
-
const isSym = entry.isSymbolicLink();
|
|
675
|
-
const typeLabel = isDir ? "dir" : isSym ? "sym" : "file";
|
|
676
|
-
let size = 0;
|
|
677
|
-
try {
|
|
678
|
-
if (!isDir && !isSym) {
|
|
679
|
-
const st = await fs.stat(path.join(dirPath, entry.name));
|
|
680
|
-
size = st.size;
|
|
681
|
-
}
|
|
682
|
-
} catch {}
|
|
683
|
-
lines.push(`${entry.name}${isDir ? "/" : ""} (${typeLabel}${size ? `, ${size} bytes` : ""})`);
|
|
412
|
+
lines.push(await formatLsEntry(dirPath, entry));
|
|
684
413
|
}
|
|
685
414
|
return textResult(lines.join("\n"), { path: dirPath, count: entries.length });
|
|
686
415
|
},
|
|
@@ -688,9 +417,10 @@ function createNativeAdapters(getCwd, vfs, config) {
|
|
|
688
417
|
}
|
|
689
418
|
|
|
690
419
|
export function createHostBridge({ pi, config, getCwd }) {
|
|
691
|
-
const
|
|
420
|
+
const index = new WorkspaceIndex((argv, opts) => runCommand(argv, opts));
|
|
421
|
+
const vfs = new CausalVfs(() => index.invalidate());
|
|
692
422
|
const executors = new Map();
|
|
693
|
-
const natives = createNativeAdapters(getCwd, vfs, config);
|
|
423
|
+
const natives = createNativeAdapters(getCwd, vfs, config, index);
|
|
694
424
|
let callCount = 0;
|
|
695
425
|
let activeCtx = null;
|
|
696
426
|
let activeSignal = undefined;
|
|
@@ -722,6 +452,9 @@ export function createHostBridge({ pi, config, getCwd }) {
|
|
|
722
452
|
function resetCallBudget() {
|
|
723
453
|
callCount = 0;
|
|
724
454
|
trace = [];
|
|
455
|
+
// Files may change between programs (editor, git); never serve a stale run.
|
|
456
|
+
vfs.invalidateCache();
|
|
457
|
+
clearPathCache();
|
|
725
458
|
}
|
|
726
459
|
|
|
727
460
|
function getTrace() {
|
|
@@ -771,15 +504,19 @@ export function createHostBridge({ pi, config, getCwd }) {
|
|
|
771
504
|
} catch {}
|
|
772
505
|
}
|
|
773
506
|
|
|
774
|
-
|
|
507
|
+
function checkCallBudget(name) {
|
|
775
508
|
const maxCalls = config.maxBridgeCalls ?? 256;
|
|
776
509
|
callCount += 1;
|
|
777
510
|
if (callCount > maxCalls) {
|
|
778
|
-
throw new Error(
|
|
511
|
+
throw new Error(
|
|
512
|
+
`host call budget exceeded (${maxCalls} calls per program): batch with read([paths]) or nova.callMany, or split the work across programs`,
|
|
513
|
+
);
|
|
779
514
|
}
|
|
780
515
|
if (activeSignal?.aborted) throw new Error("aborted");
|
|
781
516
|
if (!isString(name) || !name) throw new Error("tool name required");
|
|
517
|
+
}
|
|
782
518
|
|
|
519
|
+
function assertCallableTarget(name) {
|
|
783
520
|
// Never re-enter supernova or other excluded composition tools via the bridge.
|
|
784
521
|
const excluded = new Set(config.excludeTools || []);
|
|
785
522
|
if (name === "supernova" || excluded.has(name)) {
|
|
@@ -787,6 +524,30 @@ export function createHostBridge({ pi, config, getCwd }) {
|
|
|
787
524
|
`nova.call("${name}") is blocked (excluded / non-reentrant). Use nova.search/describe for discovery, or call a concrete host tool.`,
|
|
788
525
|
);
|
|
789
526
|
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
async function writeFallbackDiff(name, args) {
|
|
530
|
+
if (name !== "write" || !isString(args?.path) || !isString(args?.content)) return undefined;
|
|
531
|
+
const target = await resolveWorkspacePath(getCwd(), args.path, "write", false);
|
|
532
|
+
let previous = "";
|
|
533
|
+
try {
|
|
534
|
+
previous = await vfs.read(target);
|
|
535
|
+
} catch (error) {
|
|
536
|
+
if (error?.code !== "ENOENT") throw error;
|
|
537
|
+
}
|
|
538
|
+
return buildWriteDiff(target, previous, args.content);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
function completeRecord(record, res, fallbackDiff) {
|
|
542
|
+
const diff = resultDiff(res) || fallbackDiff;
|
|
543
|
+
finishRecord(record, res);
|
|
544
|
+
if (diff && record.ok) record.diff = diff;
|
|
545
|
+
notifyCall(record);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
async function invokeRaw(name, args) {
|
|
549
|
+
checkCallBudget(name);
|
|
550
|
+
assertCallableTarget(name);
|
|
790
551
|
|
|
791
552
|
const record = { name, args: args || {}, time: Date.now() };
|
|
792
553
|
trace.push(record);
|
|
@@ -795,46 +556,37 @@ export function createHostBridge({ pi, config, getCwd }) {
|
|
|
795
556
|
try {
|
|
796
557
|
const exec = executors.get(name);
|
|
797
558
|
if (exec) {
|
|
798
|
-
|
|
799
|
-
if (name === "write" && isString(args?.path) && isString(args?.content)) {
|
|
800
|
-
const target = await resolveWorkspacePath(getCwd(), args.path, "write", false);
|
|
801
|
-
let previous = "";
|
|
802
|
-
try {
|
|
803
|
-
previous = await vfs.read(target);
|
|
804
|
-
} catch (error) {
|
|
805
|
-
if (error?.code !== "ENOENT") throw error;
|
|
806
|
-
}
|
|
807
|
-
fallbackDiff = buildWriteDiff(target, previous, args.content);
|
|
808
|
-
}
|
|
559
|
+
const fallbackDiff = await writeFallbackDiff(name, args);
|
|
809
560
|
if (isMutatingTool(name, config)) await vfs.prepareExternalMutation(name);
|
|
810
561
|
const res = await exec(`supernova:${name}:${callCount}`, args || {}, activeSignal, undefined, activeCtx);
|
|
811
|
-
|
|
812
|
-
record.ok = res?.isError !== true && res?.details?.ok !== false;
|
|
813
|
-
if (diff && record.ok) record.diff = diff;
|
|
814
|
-
notifyCall(record);
|
|
562
|
+
completeRecord(record, res, fallbackDiff);
|
|
815
563
|
return res;
|
|
816
564
|
}
|
|
817
565
|
|
|
818
566
|
const native = natives[name];
|
|
819
567
|
if (native) {
|
|
820
568
|
const res = await native(args || {}, activeSignal);
|
|
821
|
-
|
|
822
|
-
record.ok = res?.isError !== true && res?.details?.ok !== false;
|
|
823
|
-
if (diff && record.ok) record.diff = diff;
|
|
824
|
-
notifyCall(record);
|
|
569
|
+
completeRecord(record, res);
|
|
825
570
|
return res;
|
|
826
571
|
}
|
|
827
572
|
|
|
828
|
-
throw new Error(
|
|
829
|
-
`no executor for tool "${name}" (not captured via registerTool and no native adapter). Use nova.describe to inspect; ensure pi-supernova loads before other extensions, or call a core adapter: ${Object.keys(natives).join(", ")}`,
|
|
830
|
-
);
|
|
573
|
+
throw new Error(unknownToolMessage(name, [...executors.keys(), ...Object.keys(natives)]));
|
|
831
574
|
} catch (error) {
|
|
832
575
|
record.ok = false;
|
|
576
|
+
record.ms = Date.now() - record.time;
|
|
577
|
+
record.error = error instanceof Error ? error.message : String(error);
|
|
833
578
|
notifyCall(record);
|
|
834
579
|
throw error;
|
|
835
580
|
}
|
|
836
581
|
}
|
|
837
582
|
|
|
583
|
+
function finishRecord(record, res) {
|
|
584
|
+
record.ms = Date.now() - record.time;
|
|
585
|
+
record.ok = res?.isError !== true && res?.details?.ok !== false;
|
|
586
|
+
const exitCode = isObject(res?.details) ? res.details.exitCode : undefined;
|
|
587
|
+
if (Number.isInteger(exitCode) && exitCode !== 0) record.exitCode = exitCode;
|
|
588
|
+
}
|
|
589
|
+
|
|
838
590
|
async function call(name, args) {
|
|
839
591
|
if (!isString(name) || !name) throw new Error("nova.call requires a tool name");
|
|
840
592
|
const raw = await invokeRaw(name, args);
|