pi-supernova 0.3.2 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +148 -29
- package/docs/CHANGELOG.md +52 -0
- package/docs/TOKEN_COSTS.md +173 -0
- package/index.js +155 -41
- package/package.json +3 -1
- package/src/bridge/catalog.js +35 -1
- package/src/bridge/host-bridge.js +411 -50
- package/src/bridge/native-tools.js +35 -1
- package/src/config/config.default.json +1 -1
- package/src/config/config.js +19 -0
- package/src/context/evidence.js +119 -5
- package/src/context/fuzzy.js +37 -0
- package/src/context/ledger.js +128 -6
- package/src/context/outline.js +24 -0
- package/src/context/repo-index.js +102 -4
- package/src/context/search.js +45 -0
- package/src/context/snap.js +118 -6
- package/src/context/spans.js +39 -0
- package/src/context/surface.js +33 -6
- package/src/fs/check.js +50 -0
- package/src/fs/diff.js +16 -0
- package/src/fs/json-read.js +91 -0
- package/src/fs/patch.js +26 -0
- package/src/fs/vfs.js +102 -5
- package/src/fs/workspace.js +62 -3
- package/src/output/bottleneck.js +63 -4
- package/src/output/format.js +136 -17
- package/src/runtime/guest-worker.js +169 -31
- package/src/runtime/parallel.js +33 -0
- package/src/runtime/program-batch.js +112 -0
- package/src/runtime/program-file.js +40 -0
- package/src/runtime/reference.js +18 -0
- package/src/runtime/runtime.js +90 -8
- package/src/shared/decode.js +40 -0
- package/src/ui/omp-frame.js +30 -1
- package/src/ui/render-measure.js +24 -0
- package/src/ui/render.js +96 -2
|
@@ -3,10 +3,11 @@ import * as fs from "node:fs/promises";
|
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
5
|
import { packageHostResult, hostResultFailed } from "../output/bottleneck.js";
|
|
6
|
-
import { isString, isNumber, isFunction, isObject } from "../shared/decode.js";
|
|
6
|
+
import { isString, isNumber, isFunction, isObject, looksLikePath } from "../shared/decode.js";
|
|
7
7
|
import { isMutatingTool, runParallelWave, createNativeScheduler } from "../runtime/parallel.js";
|
|
8
8
|
import { unknownToolMessage } from "./catalog.js";
|
|
9
9
|
import { extractStructuralSurface } from "../context/surface.js";
|
|
10
|
+
import { pickSpan } from "../context/spans.js";
|
|
10
11
|
import { buildEditDiff, buildMultiEditDiff, buildPatchDiff, buildWriteDiff } from "../fs/diff.js";
|
|
11
12
|
import { executeSnap } from "../context/snap.js";
|
|
12
13
|
import { selectEvidence } from "../context/evidence.js";
|
|
@@ -16,8 +17,9 @@ import { SeenLedger } from "../context/ledger.js";
|
|
|
16
17
|
import { quickCheck } from "../fs/check.js";
|
|
17
18
|
import { declaredName } from "../context/repo-index.js";
|
|
18
19
|
import { CausalVfs } from "../fs/vfs.js";
|
|
20
|
+
import { MAX_JSON_BYTES, jsonProjector, sessionJsonArgs, validateJsonRead } from "../fs/json-read.js";
|
|
19
21
|
import { applyPatchToText } from "../fs/patch.js";
|
|
20
|
-
import { resolveWorkspacePath, runCommand, clearPathCache, relativeSlash } from "../fs/workspace.js";
|
|
22
|
+
import { resolveWorkspacePath, runCommand, clearPathCache, relativeSlash, assertFilesystemPath } from "../fs/workspace.js";
|
|
21
23
|
import { fuzzyFind, grepIndexed, listIndexed, listWithTools, rgGrepArgs, referencesForNames } from "../context/search.js";
|
|
22
24
|
|
|
23
25
|
function textResult(text, details) {
|
|
@@ -27,14 +29,32 @@ function textResult(text, details) {
|
|
|
27
29
|
};
|
|
28
30
|
}
|
|
29
31
|
|
|
32
|
+
function resultDiff(response) {
|
|
33
|
+
let details = response?.details;
|
|
34
|
+
|
|
35
|
+
if (isString(details)) {
|
|
36
|
+
try {
|
|
37
|
+
details = JSON.parse(details);
|
|
38
|
+
} catch {
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return isObject(details) ? details.diff : undefined;
|
|
44
|
+
}
|
|
45
|
+
|
|
30
46
|
/** Unwrap a single matching quote pair around the whole string (`'git status'`). */
|
|
31
47
|
function unwrapIfFullyQuoted(s) {
|
|
32
48
|
if (s.length < 2) return s;
|
|
33
49
|
const q = s[0];
|
|
50
|
+
|
|
34
51
|
if (q !== "'" && q !== '"') return s;
|
|
52
|
+
|
|
35
53
|
if (s[s.length - 1] !== q) return s;
|
|
36
54
|
const inner = s.slice(1, -1);
|
|
55
|
+
|
|
37
56
|
if (inner.includes(q)) return s;
|
|
57
|
+
|
|
38
58
|
return inner;
|
|
39
59
|
}
|
|
40
60
|
|
|
@@ -43,68 +63,135 @@ function sliceLines(text, offset, limit) {
|
|
|
43
63
|
const lines = text.split("\n");
|
|
44
64
|
const startIndex = (isNumber(offset) ? Math.max(1, Math.floor(offset)) : 1) - 1;
|
|
45
65
|
const count = isNumber(limit) ? Math.max(0, Math.floor(limit)) : lines.length;
|
|
46
|
-
return lines.slice(startIndex, startIndex + count).join("\n");
|
|
47
|
-
}
|
|
48
66
|
|
|
49
|
-
|
|
50
|
-
return (
|
|
51
|
-
isString(target) &&
|
|
52
|
-
(target.includes("/") ||
|
|
53
|
-
target.includes("\\") ||
|
|
54
|
-
target.startsWith(".") ||
|
|
55
|
-
(!/\s/.test(target) && path.extname(target).length > 0))
|
|
56
|
-
);
|
|
67
|
+
return lines.slice(startIndex, startIndex + count).join("\n");
|
|
57
68
|
}
|
|
58
69
|
|
|
59
70
|
function resolveReadPath(cwd, target) {
|
|
60
71
|
if (!isString(target) || !target.trim()) throw new Error("read requires path");
|
|
61
|
-
const input = target
|
|
72
|
+
const input = assertFilesystemPath(target, "read");
|
|
73
|
+
|
|
62
74
|
return path.resolve(cwd, input === "~" ? homedir() : input.startsWith("~/") ? path.join(homedir(), input.slice(2)) : input);
|
|
63
75
|
}
|
|
64
76
|
|
|
65
77
|
async function probeExistingPath(cwd, targetParam, vfs) {
|
|
66
78
|
const targetPath = resolveReadPath(cwd, targetParam);
|
|
79
|
+
|
|
67
80
|
if (vfs.getOverlay(targetPath) !== undefined) return { path: targetPath, directory: false };
|
|
81
|
+
|
|
68
82
|
try {
|
|
69
83
|
const st = await fs.stat(targetPath);
|
|
84
|
+
|
|
70
85
|
return { path: targetPath, directory: st.isDirectory() };
|
|
71
86
|
} catch (err) {
|
|
72
87
|
if (err?.code !== "ENOENT" && err?.code !== "ENOTDIR") throw err;
|
|
88
|
+
|
|
73
89
|
if (vfs.getOverlayPaths().some(file => file.startsWith(targetPath + path.sep))) return { path: targetPath, directory: true };
|
|
90
|
+
|
|
74
91
|
return null;
|
|
75
92
|
}
|
|
76
93
|
}
|
|
77
94
|
|
|
95
|
+
const EDIT_PREVIEW_LINES = 16;
|
|
96
|
+
|
|
97
|
+
function sourceLines(content) {
|
|
98
|
+
const raw = content.split("\n");
|
|
99
|
+
|
|
100
|
+
if (raw.at(-1) === "") raw.pop();
|
|
101
|
+
|
|
102
|
+
return raw;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function lineNumberAt(content, index) {
|
|
106
|
+
let line = 1;
|
|
107
|
+
|
|
108
|
+
for (let i = 0; i < index; i++) if (content.charCodeAt(i) === 10) line++;
|
|
109
|
+
|
|
110
|
+
return line;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function formatNumberedLine(n, text) {
|
|
114
|
+
return String(n).padStart(5) + " " + text;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function numberedPreview(content, cap = EDIT_PREVIEW_LINES) {
|
|
118
|
+
const lines = sourceLines(content);
|
|
119
|
+
|
|
120
|
+
if (lines.length === 0) return "0 lines";
|
|
121
|
+
const shown = lines.slice(0, cap);
|
|
122
|
+
const body = shown.map((line, i) => formatNumberedLine(i + 1, line)).join("\n");
|
|
123
|
+
const suffix = lines.length > cap ? lines.length + " lines total" : lines.length + " lines";
|
|
124
|
+
|
|
125
|
+
return body + "\n" + suffix;
|
|
126
|
+
}
|
|
127
|
+
|
|
78
128
|
function applyReplacements(target, content, requestedEdits) {
|
|
79
129
|
if (requestedEdits.length === 0) throw new Error("edit requires at least one replacement");
|
|
130
|
+
|
|
80
131
|
const matches = requestedEdits.map((replacement) => {
|
|
81
132
|
if (!isString(replacement?.oldText) || replacement.oldText.length === 0) {
|
|
82
133
|
throw new Error("edit requires non-empty oldText");
|
|
83
134
|
}
|
|
135
|
+
|
|
84
136
|
if (!isString(replacement?.newText)) throw new Error("edit requires newText");
|
|
85
137
|
const index = content.indexOf(replacement.oldText);
|
|
138
|
+
|
|
86
139
|
if (index < 0) {
|
|
87
|
-
throw new Error(
|
|
140
|
+
throw new Error("edit target not found in " + target + ": oldText must match the file byte-for-byte\n" + numberedPreview(content));
|
|
88
141
|
}
|
|
89
|
-
|
|
90
|
-
|
|
142
|
+
const second = content.indexOf(replacement.oldText, index + 1);
|
|
143
|
+
|
|
144
|
+
if (second >= 0) {
|
|
145
|
+
const lines = sourceLines(content);
|
|
146
|
+
const a = lineNumberAt(content, index);
|
|
147
|
+
const b = lineNumberAt(content, second);
|
|
148
|
+
|
|
149
|
+
throw new Error("edit target is not unique in " + target + ": lines " + a + " and " + b + "; include more surrounding lines in oldText, or pass edits:[{oldText,newText},…]\n" + formatNumberedLine(a, lines[a - 1] ?? "") + "\n" + formatNumberedLine(b, lines[b - 1] ?? ""));
|
|
91
150
|
}
|
|
151
|
+
|
|
92
152
|
return { ...replacement, index, end: index + replacement.oldText.length };
|
|
93
153
|
});
|
|
154
|
+
|
|
94
155
|
matches.sort((a, b) => a.index - b.index);
|
|
156
|
+
|
|
95
157
|
for (let i = 1; i < matches.length; i++) {
|
|
96
158
|
if (matches[i].index < matches[i - 1].end) throw new Error(`edit targets overlap in ${target}`);
|
|
97
159
|
}
|
|
160
|
+
|
|
98
161
|
let updated = content;
|
|
162
|
+
|
|
99
163
|
for (let i = matches.length - 1; i >= 0; i--) {
|
|
100
164
|
const match = matches[i];
|
|
101
165
|
updated = updated.slice(0, match.index) + match.newText + updated.slice(match.end);
|
|
102
166
|
}
|
|
167
|
+
|
|
103
168
|
return { updated, matches };
|
|
104
169
|
}
|
|
105
170
|
|
|
171
|
+
function applyViewReplace(target, content, start, end, oldText, newText) {
|
|
172
|
+
const current = sliceLines(content, start, end - start + 1);
|
|
173
|
+
|
|
174
|
+
if (current !== oldText) {
|
|
175
|
+
const shown = current.length ? current : content;
|
|
176
|
+
|
|
177
|
+
throw new Error("edit view is stale in " + target + ": lines " + start + "-" + end + " changed\n" + numberedPreview(shown));
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const hadTrail = content.endsWith("\n");
|
|
181
|
+
const lines = content.split("\n");
|
|
182
|
+
|
|
183
|
+
if (hadTrail && lines.at(-1) === "") lines.pop();
|
|
184
|
+
const insert = newText.split("\n");
|
|
185
|
+
|
|
186
|
+
if (newText.endsWith("\n") && insert.at(-1) === "") insert.pop();
|
|
187
|
+
const updated = [...lines.slice(0, start - 1), ...insert, ...lines.slice(end)].join("\n") + (hadTrail ? "\n" : "");
|
|
188
|
+
|
|
189
|
+
return { updated, oldText, newText };
|
|
190
|
+
}
|
|
191
|
+
|
|
106
192
|
function formatDirectoryEntry(name, type, size = 0) {
|
|
107
193
|
const sizeSuffix = size ? `, ${size} bytes` : "";
|
|
194
|
+
|
|
108
195
|
return `${name}${type === "dir" ? "/" : ""} (${type}${sizeSuffix})`;
|
|
109
196
|
}
|
|
110
197
|
|
|
@@ -113,37 +200,48 @@ async function formatLsEntry(dirPath, entry) {
|
|
|
113
200
|
const isSym = entry.isSymbolicLink();
|
|
114
201
|
const typeLabel = isDir ? "dir" : isSym ? "sym" : "file";
|
|
115
202
|
let size = 0;
|
|
203
|
+
|
|
116
204
|
try {
|
|
117
205
|
if (!isDir && !isSym) {
|
|
118
206
|
const st = await fs.stat(path.join(dirPath, entry.name));
|
|
119
207
|
size = st.size;
|
|
120
208
|
}
|
|
121
209
|
} catch {}
|
|
210
|
+
|
|
122
211
|
return formatDirectoryEntry(entry.name, typeLabel, size);
|
|
123
212
|
}
|
|
124
213
|
|
|
125
214
|
function createNativeAdapters(getCwd, vfs, config, index, ledger, hooks) {
|
|
126
215
|
const reads = createNativeScheduler();
|
|
216
|
+
|
|
127
217
|
async function sourceRead(query, searchDir, signal, params = {}) {
|
|
218
|
+
params = { ...params, resolve: params.resolve !== false };
|
|
128
219
|
const cwd = getCwd();
|
|
220
|
+
|
|
129
221
|
const includeHidden = path.relative(cwd, searchDir).split(path.sep)
|
|
130
222
|
.some(segment => segment.startsWith(".") && segment.length > 1);
|
|
223
|
+
|
|
131
224
|
const result = await executeSnap({ query, searchDir, root: cwd, includeHidden,
|
|
132
225
|
pathContext: { frecency: index.frecency, currentFile: index.lastTouched },
|
|
133
226
|
overlayText: p => vfs.getOverlay(p), pendingPaths: vfs.getOverlayPaths(), signal });
|
|
134
|
-
|
|
227
|
+
|
|
228
|
+
return openSource(result, params, signal, undefined, query);
|
|
135
229
|
}
|
|
136
230
|
|
|
137
|
-
async function openSource(result, params, signal, resolvedPath) {
|
|
231
|
+
async function openSource(result, params, signal, resolvedPath, query) {
|
|
138
232
|
const cwd = getCwd();
|
|
233
|
+
|
|
139
234
|
if (result.status !== "found") return textResult(JSON.stringify(result), { isSnap: true });
|
|
140
235
|
signal?.throwIfAborted();
|
|
141
|
-
const opened = await readFile(resolvedPath ?? path.resolve(cwd, result.path), { ...params, about: undefined }, result.line, result.path);
|
|
236
|
+
const opened = await readFile(resolvedPath ?? path.resolve(cwd, result.path), { ...params, about: undefined }, result.line, result.path, query);
|
|
142
237
|
const block = opened.content[0];
|
|
238
|
+
|
|
143
239
|
if (block.type !== "text") throw new Error("source resolution requires a text file; read the image path directly");
|
|
144
240
|
const { firstLine, lastLine, sourceChars, nextOffset, complete } = opened.details;
|
|
241
|
+
|
|
145
242
|
const source = { status: "found", path: result.path, line: result.line, lines: [firstLine, lastLine],
|
|
146
243
|
text: block.text.slice(0, sourceChars), complete, nextOffset };
|
|
244
|
+
|
|
147
245
|
return textResult(params.resolve ? JSON.stringify(source) : "// " + result.path + ":" + firstLine + "-" + lastLine + "\n" + block.text,
|
|
148
246
|
{ ...opened.details, isSnap: true });
|
|
149
247
|
}
|
|
@@ -151,20 +249,26 @@ function createNativeAdapters(getCwd, vfs, config, index, ledger, hooks) {
|
|
|
151
249
|
async function readDirectory(dirPath, signal) {
|
|
152
250
|
signal?.throwIfAborted();
|
|
153
251
|
const rows = new Map();
|
|
252
|
+
|
|
154
253
|
for (const file of vfs.getOverlayPaths()) {
|
|
155
254
|
const relative = path.relative(dirPath, file);
|
|
255
|
+
|
|
156
256
|
if (!relative || relative === ".." || relative.startsWith(".." + path.sep) || path.isAbsolute(relative)) continue;
|
|
157
257
|
const [name, child] = relative.split(path.sep);
|
|
158
258
|
rows.set(name, child === undefined
|
|
159
259
|
? formatDirectoryEntry(name, "file", Buffer.byteLength(vfs.getOverlay(file), "utf8"))
|
|
160
260
|
: formatDirectoryEntry(name, "dir"));
|
|
161
261
|
}
|
|
262
|
+
|
|
162
263
|
let entries;
|
|
264
|
+
|
|
163
265
|
try { entries = await fs.readdir(dirPath, { withFileTypes: true }); } catch (error) {
|
|
164
266
|
if (error.code !== "ENOENT" || rows.size === 0) throw error;
|
|
165
267
|
entries = [];
|
|
166
268
|
}
|
|
269
|
+
|
|
167
270
|
for (const entry of entries) if (!rows.has(entry.name)) rows.set(entry.name, await formatLsEntry(dirPath, entry));
|
|
271
|
+
|
|
168
272
|
return textResult([...rows.values()].join("\n"), { path: dirPath, count: rows.size });
|
|
169
273
|
}
|
|
170
274
|
|
|
@@ -172,126 +276,216 @@ function createNativeAdapters(getCwd, vfs, config, index, ledger, hooks) {
|
|
|
172
276
|
signal?.throwIfAborted();
|
|
173
277
|
const cwd = getCwd();
|
|
174
278
|
const targetParam = params?.path ?? params?.target;
|
|
279
|
+
|
|
175
280
|
if (Array.isArray(targetParam)) {
|
|
176
|
-
if (targetParam.some(p => !isString(p) || !p.trim())) throw new Error("read paths must be non-empty strings");
|
|
177
281
|
if (targetParam.length > 64) throw new Error("read accepts at most 64 paths per batch");
|
|
282
|
+
|
|
283
|
+
for (const p of targetParam) if (!isString(p) || !p.trim()) throw new Error("read paths must be non-empty strings");
|
|
284
|
+
|
|
178
285
|
const results = await Promise.all(targetParam.map(async p => {
|
|
179
286
|
try {
|
|
180
287
|
const block = (await readAdapter({ ...params, path: p }, signal)).content[0];
|
|
288
|
+
|
|
181
289
|
return { text: block.type === "image" ? block : block.text };
|
|
182
290
|
} catch (error) {
|
|
183
291
|
signal?.throwIfAborted();
|
|
292
|
+
|
|
184
293
|
return { text: `[read error: ${p}] ${error.message}`, error: { path: p, message: error.message } };
|
|
185
294
|
}
|
|
186
295
|
}));
|
|
296
|
+
|
|
187
297
|
signal?.throwIfAborted();
|
|
188
298
|
const response = textResult("", { count: results.length, batch: true, independent: params._independent === true, items: results.map(r => r.text), itemErrors: results.map(r => r.error?.message ?? null), errors: results.filter(r => r.error).map(r => r.error) });
|
|
189
299
|
response.isError = params._independent !== true && results.some(r => r.error);
|
|
300
|
+
|
|
190
301
|
return response;
|
|
191
302
|
}
|
|
303
|
+
|
|
192
304
|
return reads.schedule("read", () => readSingle(params, cwd, targetParam, signal), signal);
|
|
193
305
|
}
|
|
194
306
|
|
|
195
307
|
async function resolveSessionResource(uri, signal) {
|
|
196
308
|
const match = /^(agent|artifact):\/\/([^/?#]+)$/i.exec(uri);
|
|
309
|
+
|
|
197
310
|
if (!match) throw new Error("session resource reads support bare agent://<id> and artifact://<number>; use offset/limit for pagination");
|
|
198
311
|
const kind = match[1].toLowerCase();
|
|
199
312
|
const id = decodeURIComponent(match[2]);
|
|
313
|
+
|
|
200
314
|
if (!id || id === "." || id === ".." || (/[/\\]/u.test(id) || Array.from(id).some(char => char.charCodeAt(0) < 32)) || (kind === "artifact" && !/^\d+$/.test(id))) throw new Error("invalid session resource ID");
|
|
201
315
|
const dir = hooks.artifactsDir?.();
|
|
316
|
+
|
|
202
317
|
if (!isString(dir) || !dir) throw new Error("this host session does not expose an artifacts directory for " + uri);
|
|
203
318
|
signal?.throwIfAborted();
|
|
204
319
|
const root = await fs.realpath(dir);
|
|
205
320
|
let file = id + ".md";
|
|
321
|
+
|
|
206
322
|
if (kind === "artifact") {
|
|
207
323
|
const matches = [];
|
|
208
324
|
let count = 0;
|
|
325
|
+
|
|
209
326
|
for await (const entry of await fs.opendir(root)) {
|
|
210
327
|
signal?.throwIfAborted();
|
|
328
|
+
|
|
211
329
|
if (++count > 4096) throw new Error("session artifact lookup exceeded its directory budget");
|
|
330
|
+
|
|
212
331
|
if (entry.name.startsWith(id + ".") && !entry.isDirectory()) matches.push(entry.name);
|
|
213
332
|
}
|
|
333
|
+
|
|
214
334
|
if (matches.length !== 1) throw new Error(matches.length ? "ambiguous session artifact: " + uri : "session artifact not found: " + uri);
|
|
215
335
|
file = matches[0];
|
|
216
336
|
}
|
|
337
|
+
|
|
217
338
|
const target = await fs.realpath(path.join(root, file));
|
|
339
|
+
|
|
218
340
|
if (!target.startsWith(root + path.sep)) throw new Error("session resource escapes its artifacts directory");
|
|
341
|
+
|
|
219
342
|
if (!(await fs.stat(target)).isFile()) throw new Error("session resource is not a file: " + uri);
|
|
220
343
|
signal?.throwIfAborted();
|
|
344
|
+
|
|
221
345
|
return target;
|
|
222
346
|
}
|
|
223
347
|
|
|
224
348
|
async function readSingle(params, cwd, targetParam, signal) {
|
|
349
|
+
params = sessionJsonArgs({ ...params, path: targetParam });
|
|
350
|
+
validateJsonRead(params);
|
|
351
|
+
targetParam = params.path;
|
|
352
|
+
|
|
225
353
|
if (isString(targetParam) && /^(?:agent|artifact):\/\//i.test(targetParam)) {
|
|
226
354
|
const target = await resolveSessionResource(targetParam, signal);
|
|
355
|
+
|
|
227
356
|
return params.resolve
|
|
228
357
|
? openSource({status:"found",path:targetParam,line:params.offset ?? 1}, params, signal, target)
|
|
229
358
|
: readFile(target, params, undefined, targetParam);
|
|
230
359
|
}
|
|
360
|
+
|
|
231
361
|
if (isString(params?.query)) {
|
|
232
362
|
const scope = targetParam && targetParam !== params.query ? resolveReadPath(cwd, targetParam) : cwd;
|
|
363
|
+
|
|
233
364
|
return sourceRead(params.query, scope, signal, params);
|
|
234
365
|
}
|
|
366
|
+
|
|
235
367
|
const existing = await probeExistingPath(cwd, targetParam, vfs);
|
|
368
|
+
|
|
236
369
|
if (existing) {
|
|
237
370
|
if (!existing.directory) return params.resolve
|
|
238
371
|
? openSource({ status: "found", path: relativeSlash(cwd, existing.path), line: params.offset ?? 1 }, params, signal)
|
|
239
372
|
: readFile(existing.path, params);
|
|
373
|
+
|
|
374
|
+
if (params.json !== undefined) throw new Error("JSON read requires a file, not a directory");
|
|
375
|
+
|
|
240
376
|
return isString(params?.about) ? sourceRead(params.about, existing.path, signal, params) : readDirectory(existing.path, signal);
|
|
241
377
|
}
|
|
242
|
-
|
|
378
|
+
|
|
379
|
+
if (params.json === undefined && !looksLikePath(targetParam)) return sourceRead(targetParam, cwd, signal, params);
|
|
243
380
|
const targetPath = resolveReadPath(cwd, targetParam);
|
|
381
|
+
|
|
244
382
|
return readFile(targetPath, params);
|
|
245
383
|
}
|
|
246
384
|
|
|
247
385
|
/** Plain text, a line window, or (with `about`) a relevance-folded outline of the whole file. */
|
|
248
|
-
async function readFile(targetPath, params, sourceLine, displayPath) {
|
|
386
|
+
async function readFile(targetPath, params, sourceLine, displayPath, query) {
|
|
249
387
|
const cwd = getCwd();
|
|
250
388
|
const rel = displayPath ?? relativeSlash(cwd, targetPath);
|
|
389
|
+
|
|
390
|
+
if (params.json !== undefined) {
|
|
391
|
+
const project = jsonProjector(params.json);
|
|
392
|
+
const text = await vfs.read(targetPath, { maxBytes: MAX_JSON_BYTES });
|
|
393
|
+
let document;
|
|
394
|
+
|
|
395
|
+
try { document = JSON.parse(text); }
|
|
396
|
+
catch { throw new Error("invalid JSON in " + rel + "; the entire document must parse before projection"); }
|
|
397
|
+
|
|
398
|
+
const many = Array.isArray(params.json);
|
|
399
|
+
let remaining = Math.max(1, Math.min(config.maxCallResultChars ?? 65536, config.maxReturnChars ?? 32000) - 256) - (many ? params.json.length + 1 : 0);
|
|
400
|
+
const parts = [];
|
|
401
|
+
|
|
402
|
+
for (const value of project(document)) {
|
|
403
|
+
const encoded = JSON.stringify(value);
|
|
404
|
+
remaining -= encoded.length;
|
|
405
|
+
|
|
406
|
+
if (remaining < 0) throw new Error("JSON selection exceeds the read budget; select narrower fields or an array slice such as .items[0:10]");
|
|
407
|
+
parts.push(encoded);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
return textResult(many ? "[" + parts.join(",") + "]" : parts[0], { path: targetPath, json: true, complete: true });
|
|
411
|
+
}
|
|
412
|
+
|
|
251
413
|
const mime = { ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".gif": "image/gif", ".webp": "image/webp", ".bmp": "image/bmp" }[path.extname(targetPath).toLowerCase()];
|
|
414
|
+
|
|
252
415
|
if (mime) {
|
|
253
416
|
if ((await fs.stat(targetPath)).size > 20 * 1024 * 1024) throw new Error("image exceeds 20 MiB; resize it before reading");
|
|
254
417
|
const staged = vfs.getOverlay(targetPath);
|
|
255
418
|
const bytes = staged === undefined ? await fs.readFile(targetPath) : Buffer.from(staged);
|
|
419
|
+
|
|
256
420
|
return { content: [{ type: "image", mimeType: mime, data: bytes.toString("base64") }], details: { path: targetPath } };
|
|
257
421
|
}
|
|
422
|
+
|
|
258
423
|
const text = await vfs.read(targetPath);
|
|
259
424
|
index.touch(rel);
|
|
425
|
+
|
|
260
426
|
if (isString(params?.about)) {
|
|
261
427
|
const entry = WorkspaceIndex.fromText(targetPath, text);
|
|
262
428
|
const outline = entry && outlineFile(entry, rel, params.about, outlineOptions(params, await referenceFinder(cwd, targetPath)));
|
|
429
|
+
|
|
263
430
|
if (outline) {
|
|
264
431
|
recordOutlineOrigins(rel, outline.text);
|
|
432
|
+
|
|
265
433
|
return textResult(outline.text, { path: targetPath, outline: true, expanded: outline.expanded, declarations: outline.declarations });
|
|
266
434
|
}
|
|
267
435
|
}
|
|
436
|
+
|
|
268
437
|
const explicit = isNumber(params?.offset) || isNumber(params?.limit);
|
|
269
438
|
const budget = Math.max(1, Math.min(config.maxCallResultChars ?? 65536, config.maxReturnChars ?? 32000) - (params.resolve ? 1024 : 256));
|
|
270
|
-
|
|
439
|
+
let offset = params?.offset;
|
|
440
|
+
let limit = params?.limit;
|
|
441
|
+
|
|
442
|
+
if (!explicit && params.resolve) {
|
|
443
|
+
const spans = WorkspaceIndex.spansOf(WorkspaceIndex.fromText(targetPath, text));
|
|
444
|
+
const span = pickSpan(spans, { line: sourceLine, name: query });
|
|
445
|
+
|
|
446
|
+
if (span) {
|
|
447
|
+
offset = span.start;
|
|
448
|
+
limit = span.end - span.start + 1;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
offset ??= sourceLine && text.length > budget ? Math.max(1, sourceLine - 2) : 1;
|
|
271
453
|
const firstLine = isNumber(offset) ? Math.max(1, Math.floor(offset)) : 1;
|
|
272
|
-
const sliced = sliceLines(text, offset,
|
|
454
|
+
const sliced = sliceLines(text, offset, limit);
|
|
455
|
+
|
|
273
456
|
if (params.complete === true && (sliced !== text || sliced.length > budget || (params.resolve && JSON.stringify(sliced).length > budget))) {
|
|
274
|
-
throw new Error(`incomplete read of ${rel}: complete:true requires the entire file within the read budget; use edit() for replacements or reconstruct resolve:true source windows`);
|
|
457
|
+
throw new Error(`incomplete read of ${rel}: complete:true requires the entire file within the read budget; use json:".field" for JSON reports, about for text selection, edit() for replacements, or reconstruct resolve:true source windows`);
|
|
275
458
|
}
|
|
459
|
+
|
|
276
460
|
if (sliced.length > budget || (params.resolve && JSON.stringify(sliced).length > budget)) {
|
|
461
|
+
if (!explicit && !params.resolve && path.extname(targetPath).toLowerCase() === ".json") throw new Error("incomplete JSON read of " + rel + "; use the json selector option to parse the whole document before projection, or explicit offset/limit for raw text windows");
|
|
277
462
|
let cap = budget - 160;
|
|
463
|
+
|
|
278
464
|
if (params.resolve) {
|
|
279
465
|
// Budget the actual JSON string, not a pessimistic fixed escape multiplier.
|
|
280
466
|
let low = 0, high = Math.max(0, cap);
|
|
467
|
+
|
|
281
468
|
while (low < high) {
|
|
282
469
|
const mid = Math.ceil((low + high) / 2);
|
|
470
|
+
|
|
283
471
|
if (JSON.stringify(sliced.slice(0, mid)).length <= budget - 160) low = mid;
|
|
284
472
|
else high = mid - 1;
|
|
285
473
|
}
|
|
474
|
+
|
|
286
475
|
cap = low;
|
|
287
476
|
}
|
|
477
|
+
|
|
288
478
|
const end = sliced.lastIndexOf("\n", cap);
|
|
479
|
+
|
|
289
480
|
if (end < 0) throw new Error(`line ${firstLine} exceeds the read budget; use bash to inspect a bounded substring`);
|
|
290
481
|
const body = sliced.slice(0, end + 1);
|
|
291
482
|
const next = firstLine + body.split("\n").length - 1;
|
|
483
|
+
|
|
292
484
|
return textResult(body + `\n[read truncated; continue with read({path:${JSON.stringify(rel)}, offset:${next}})]`, { path: targetPath, outputTruncated: true, nextOffset: next, firstLine, lastLine: next - 1, sourceChars: body.length, complete: false });
|
|
293
485
|
}
|
|
486
|
+
|
|
294
487
|
ledger.recordOrigin(rel, firstLine, sliced.split("\n"), explicit);
|
|
488
|
+
|
|
295
489
|
return textResult(sliced, { path: targetPath, firstLine, lastLine: firstLine + sliced.split("\n").length - 1 - Number(sliced.endsWith("\n")), sourceChars: sliced.length, complete: sliced === text });
|
|
296
490
|
}
|
|
297
491
|
|
|
@@ -300,31 +494,45 @@ function createNativeAdapters(getCwd, vfs, config, index, ledger, hooks) {
|
|
|
300
494
|
* lines with numbers, a quick structural check, and bounded lexical reference hints.
|
|
301
495
|
* These do not replace tests or semantic caller resolution.
|
|
302
496
|
*/
|
|
303
|
-
async function editSummary(cwd, target, original, updated, diff, signal) {
|
|
497
|
+
async function editSummary(cwd, target, original, updated, diff, signal, span) {
|
|
304
498
|
const rel = relativeSlash(cwd, target);
|
|
305
499
|
const newLines = updated.split("\n");
|
|
306
500
|
const ranges = [];
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
501
|
+
|
|
502
|
+
if (span && Number.isInteger(span.start) && Number.isInteger(span.end) && span.start >= 1 && span.end >= span.start) {
|
|
503
|
+
ranges.push({ start: span.start, end: Math.min(newLines.length, span.end) });
|
|
504
|
+
} else {
|
|
505
|
+
const positions = diff.lines.filter(row => row.type !== "context")
|
|
506
|
+
.map(row => Math.min(newLines.length, row.newLineNum ?? row.lineNum)).sort((a, b) => a - b);
|
|
507
|
+
|
|
508
|
+
for (const line of positions) {
|
|
509
|
+
const start = Math.max(1, line - 2), end = Math.min(newLines.length, line + 2);
|
|
510
|
+
|
|
511
|
+
if (ranges.length && start <= ranges.at(-1).end + 1) ranges.at(-1).end = Math.max(ranges.at(-1).end, end);
|
|
512
|
+
else ranges.push({ start, end });
|
|
513
|
+
}
|
|
313
514
|
}
|
|
515
|
+
|
|
314
516
|
const blocks = [];
|
|
315
517
|
const perRange = Math.max(1, Math.floor(40 / Math.max(1, ranges.length)));
|
|
518
|
+
|
|
316
519
|
for (const { start, end } of ranges) {
|
|
317
520
|
const last = Math.min(end, start + perRange - 1);
|
|
318
521
|
const lines = newLines.slice(start - 1, last);
|
|
319
522
|
ledger.recordOrigin(rel, start, lines);
|
|
320
523
|
blocks.push("edited " + rel + ":" + start + "-" + last + "\n" + lines.map((line, i) => String(start + i).padStart(5) + " " + line).join("\n"));
|
|
524
|
+
|
|
321
525
|
if (last < end) blocks.push("[continue with read({path:" + JSON.stringify(rel) + ",offset:" + (last + 1) + ",limit:" + (end - last) + "})]");
|
|
322
526
|
}
|
|
527
|
+
|
|
323
528
|
let out = blocks.join("\n");
|
|
324
529
|
const check = quickCheck(updated, path.extname(target));
|
|
530
|
+
|
|
325
531
|
if (check && !check.ok) out += `\ncheck: ${check.message}`;
|
|
326
532
|
const refs = await changedDeclarationRefs(cwd, target, original, updated, diff, signal);
|
|
533
|
+
|
|
327
534
|
if (refs) out += `\n${refs}`;
|
|
535
|
+
|
|
328
536
|
return out;
|
|
329
537
|
}
|
|
330
538
|
|
|
@@ -334,30 +542,41 @@ function createNativeAdapters(getCwd, vfs, config, index, ledger, hooks) {
|
|
|
334
542
|
const newLines = updated.split("\n");
|
|
335
543
|
const names = new Set();
|
|
336
544
|
const spans = new Map();
|
|
545
|
+
|
|
337
546
|
for (const l of diff.lines) {
|
|
338
547
|
if (l.type === "context") continue;
|
|
339
548
|
const number = l.type === "remove" ? l.lineNum : l.newLineNum ?? l.lineNum;
|
|
340
549
|
const name = declaredName((l.type === "remove" ? oldLines : newLines)[number - 1] ?? "");
|
|
550
|
+
|
|
341
551
|
if (name) names.add(name);
|
|
342
552
|
else {
|
|
343
553
|
if (!spans.has(l.type)) spans.set(l.type, WorkspaceIndex.spansOf(WorkspaceIndex.fromText(target, l.type === "remove" ? original : updated)));
|
|
344
554
|
const owner = spans.get(l.type).find(span => span.start <= number && number <= span.end);
|
|
555
|
+
|
|
345
556
|
if (owner?.name) names.add(owner.name);
|
|
346
557
|
}
|
|
558
|
+
|
|
347
559
|
if (names.size >= 3) break;
|
|
348
560
|
}
|
|
561
|
+
|
|
349
562
|
if (names.size === 0) return "";
|
|
563
|
+
|
|
350
564
|
try {
|
|
351
565
|
const { references, incomplete } = await referencesForNames({ root: cwd, names: [...names].slice(0, 3),
|
|
352
566
|
excludePath: target, overlayText: file => vfs.getOverlay(file), pendingPaths: vfs.getOverlayPaths(), signal });
|
|
567
|
+
|
|
353
568
|
const parts = [];
|
|
569
|
+
|
|
354
570
|
for (const [name, refs] of references) {
|
|
355
571
|
if (refs.length) parts.push(name + " also referenced in " + refs.slice(0, 6).join(", ") + (refs.length > 6 ? " (more matches)" : ""));
|
|
356
572
|
}
|
|
573
|
+
|
|
357
574
|
if (incomplete) parts.push("references incomplete: search budget reached");
|
|
575
|
+
|
|
358
576
|
return parts.join("\n");
|
|
359
577
|
} catch (error) {
|
|
360
578
|
signal?.throwIfAborted();
|
|
579
|
+
|
|
361
580
|
return "references unavailable: " + error.message;
|
|
362
581
|
}
|
|
363
582
|
}
|
|
@@ -368,20 +587,27 @@ function createNativeAdapters(getCwd, vfs, config, index, ledger, hooks) {
|
|
|
368
587
|
async function sourceWindow(cwd, commandCwd, file, lineNo) {
|
|
369
588
|
const candidate = path.resolve(commandCwd, file);
|
|
370
589
|
let text, rel;
|
|
590
|
+
|
|
371
591
|
try {
|
|
372
592
|
const root = await fs.realpath(cwd);
|
|
593
|
+
|
|
373
594
|
if (!candidate.startsWith(path.resolve(cwd) + path.sep) && !candidate.startsWith(root + path.sep)) return null;
|
|
374
595
|
const real = await fs.realpath(candidate);
|
|
596
|
+
|
|
375
597
|
if (!real.startsWith(root + path.sep) || (await fs.stat(real)).size > 1024 * 1024) return null;
|
|
376
598
|
text = await fs.readFile(real, "utf8");
|
|
377
599
|
rel = relativeSlash(root, real);
|
|
378
600
|
} catch { return null; }
|
|
601
|
+
|
|
379
602
|
const raw = text.split("\n");
|
|
603
|
+
|
|
380
604
|
if (lineNo < 1 || lineNo > raw.length) return null;
|
|
381
605
|
const start = Math.max(1, lineNo - 2);
|
|
382
606
|
const rows = [];
|
|
607
|
+
|
|
383
608
|
for (let l = start; l <= Math.min(raw.length, lineNo + 2); l++) rows.push((l === lineNo ? "►" : " ") + String(l).padStart(4) + " " + raw[l - 1]);
|
|
384
609
|
ledger.recordOrigin(rel, start, rows);
|
|
610
|
+
|
|
385
611
|
return rel + ":" + lineNo + "\n" + rows.join("\n");
|
|
386
612
|
}
|
|
387
613
|
|
|
@@ -389,20 +615,27 @@ function createNativeAdapters(getCwd, vfs, config, index, ledger, hooks) {
|
|
|
389
615
|
async function sourceForReferences(cwd, commandCwd, output) {
|
|
390
616
|
const seen = new Set();
|
|
391
617
|
const blocks = [];
|
|
618
|
+
|
|
392
619
|
for (const m of output.matchAll(SOURCE_REF)) {
|
|
393
620
|
const key = m[1] + ":" + m[2];
|
|
621
|
+
|
|
394
622
|
if (seen.has(key)) continue;
|
|
623
|
+
|
|
395
624
|
if (seen.size >= 4) break;
|
|
396
625
|
seen.add(key);
|
|
397
626
|
const block = await sourceWindow(cwd, commandCwd, m[1], Number(m[2]));
|
|
627
|
+
|
|
398
628
|
if (block) blocks.push(block);
|
|
399
629
|
}
|
|
630
|
+
|
|
400
631
|
return blocks.length ? "\n--- source\n" + blocks.join("\n") : "";
|
|
401
632
|
}
|
|
402
633
|
|
|
403
634
|
function outlineOptions(params, references) {
|
|
404
635
|
const options = { references };
|
|
636
|
+
|
|
405
637
|
if (params?.maxChars) options.maxChars = params.maxChars;
|
|
638
|
+
|
|
406
639
|
return options;
|
|
407
640
|
}
|
|
408
641
|
|
|
@@ -410,6 +643,7 @@ function createNativeAdapters(getCwd, vfs, config, index, ledger, hooks) {
|
|
|
410
643
|
function recordOutlineOrigins(rel, outlineText) {
|
|
411
644
|
for (const line of outlineText.split("\n")) {
|
|
412
645
|
const m = /^\s*(\d+) (.*)$/.exec(line);
|
|
646
|
+
|
|
413
647
|
if (m && !/ … \d+ lines$/.test(line)) ledger.recordOrigin(rel, Number(m[1]), [line]);
|
|
414
648
|
}
|
|
415
649
|
}
|
|
@@ -417,11 +651,14 @@ function createNativeAdapters(getCwd, vfs, config, index, ledger, hooks) {
|
|
|
417
651
|
/** Where else a name appears (declaration line excluded), for outlines and edit results. */
|
|
418
652
|
async function referenceFinder(cwd, targetPath) {
|
|
419
653
|
const files = [...new Set([...await index.files(cwd), ...vfs.getOverlayPaths()])];
|
|
654
|
+
|
|
420
655
|
if (!index.canScan(files)) return () => [];
|
|
656
|
+
|
|
421
657
|
return (name, excludeLine) => {
|
|
422
658
|
if (!name || name.length < 3) return [];
|
|
423
659
|
const escaped = name.replace(/[$]/g, (c) => "\\" + c);
|
|
424
660
|
const regex = new RegExp("\\b" + escaped + "\\b");
|
|
661
|
+
|
|
425
662
|
return index
|
|
426
663
|
.grepRows(files, regex, cwd, file => vfs.getOverlay(file))
|
|
427
664
|
.filter((r) => !(r.line === excludeLine && r.rel === relativeSlash(cwd, targetPath)))
|
|
@@ -430,22 +667,30 @@ function createNativeAdapters(getCwd, vfs, config, index, ledger, hooks) {
|
|
|
430
667
|
}
|
|
431
668
|
|
|
432
669
|
hooks.summarizeEdit = editSummary;
|
|
670
|
+
|
|
433
671
|
return {
|
|
434
672
|
read: readAdapter,
|
|
435
673
|
async write(params, signal) {
|
|
436
674
|
const cwd = getCwd();
|
|
437
675
|
const target = await resolveWorkspacePath(cwd, params?.path, "write", false);
|
|
676
|
+
|
|
438
677
|
if (signal?.aborted) throw new Error("aborted");
|
|
678
|
+
|
|
439
679
|
if (!isString(params?.content)) throw new Error("write requires string content");
|
|
680
|
+
|
|
440
681
|
if (params.append !== undefined && params.append !== true && params.append !== false) throw new Error("write append must be a boolean");
|
|
441
682
|
let content = params.content;
|
|
683
|
+
|
|
442
684
|
if (params.allowReadArtifacts !== true && /\[read truncated;|…\[(?:host-result|output|value) truncated \d+ chars\]…/u.test(content)) {
|
|
443
685
|
throw new Error("refusing to write truncated read output; use edit() or reconstruct complete source windows. Set allowReadArtifacts:true only to intentionally write literal truncation-marker text");
|
|
444
686
|
}
|
|
687
|
+
|
|
445
688
|
let prevText = "";
|
|
689
|
+
|
|
446
690
|
try {
|
|
447
691
|
prevText = await vfs.read(target, { preserveRead: true });
|
|
448
692
|
} catch (error) { if (error.code !== "ENOENT") throw error; }
|
|
693
|
+
|
|
449
694
|
if (params.append === true) content = prevText + content;
|
|
450
695
|
const { speculative } = await vfs.write(target, content);
|
|
451
696
|
index.touch(relativeSlash(cwd, target));
|
|
@@ -453,38 +698,66 @@ function createNativeAdapters(getCwd, vfs, config, index, ledger, hooks) {
|
|
|
453
698
|
const tag = speculative ? " (speculative)" : "";
|
|
454
699
|
const check = quickCheck(content, path.extname(target));
|
|
455
700
|
const warning = check && !check.ok ? "\ncheck: " + check.message : "";
|
|
701
|
+
|
|
456
702
|
return textResult(`wrote ${target}${tag}${warning}`, { path: target, speculative, diff });
|
|
457
703
|
},
|
|
458
704
|
async edit(params, signal) {
|
|
459
705
|
const cwd = getCwd();
|
|
460
706
|
const target = await resolveWorkspacePath(cwd, params?.path, "edit", false);
|
|
707
|
+
|
|
461
708
|
if (signal?.aborted) throw new Error("aborted");
|
|
462
709
|
|
|
710
|
+
const content = await vfs.read(target);
|
|
711
|
+
|
|
712
|
+
if (isNumber(params?.viewStart) && isNumber(params?.viewEnd) && isString(params?.viewText) && isString(params?.newText)) {
|
|
713
|
+
const windowNext = isString(params.oldText)
|
|
714
|
+
? applyReplacements(target, params.viewText, [{ oldText: params.oldText, newText: params.newText }]).updated
|
|
715
|
+
: params.newText;
|
|
716
|
+
const { updated } = applyViewReplace(target, content, params.viewStart, params.viewEnd, params.viewText, windowNext);
|
|
717
|
+
const { speculative } = await vfs.write(target, updated);
|
|
718
|
+
index.touch(relativeSlash(cwd, target));
|
|
719
|
+
const diffFrom = isString(params.oldText) ? params.oldText : params.viewText;
|
|
720
|
+
const diffTo = isString(params.oldText) ? params.newText : windowNext;
|
|
721
|
+
const diff = buildEditDiff(target, content, diffFrom, diffTo);
|
|
722
|
+
const inserted = sourceLines(windowNext);
|
|
723
|
+
const spanEnd = params.viewStart + Math.max(inserted.length, 1) - 1;
|
|
724
|
+
const summary = await editSummary(cwd, target, content, updated, diff, signal, { start: params.viewStart, end: spanEnd });
|
|
725
|
+
|
|
726
|
+
return textResult(summary, { path: target, speculative, diff });
|
|
727
|
+
}
|
|
728
|
+
|
|
463
729
|
const requestedEdits = Array.isArray(params?.edits)
|
|
464
730
|
? params.edits
|
|
465
731
|
: [{ oldText: params?.oldText, newText: params?.newText }];
|
|
466
|
-
const content = await vfs.read(target);
|
|
467
732
|
const { updated, matches } = applyReplacements(target, content, requestedEdits);
|
|
468
733
|
const { speculative } = await vfs.write(target, updated);
|
|
469
734
|
index.touch(relativeSlash(cwd, target));
|
|
735
|
+
|
|
470
736
|
const diff =
|
|
471
737
|
matches.length === 1
|
|
472
738
|
? buildEditDiff(target, content, matches[0].oldText, matches[0].newText)
|
|
473
739
|
: buildMultiEditDiff(target, content, matches);
|
|
740
|
+
|
|
474
741
|
const summary = await editSummary(cwd, target, content, updated, diff, signal);
|
|
742
|
+
|
|
475
743
|
return textResult(summary, { path: target, speculative, diff });
|
|
476
744
|
},
|
|
477
745
|
async apply_patch(params, signal) {
|
|
478
746
|
const cwd = getCwd();
|
|
479
747
|
let inputPath = params?.path;
|
|
748
|
+
|
|
480
749
|
if (!inputPath && isString(params?.patch)) {
|
|
481
750
|
const headerMatch = /^\+\+\+\s+[ab]\/(.+)$/m.exec(params.patch) || /^---\s+[ab]\/(.+)$/m.exec(params.patch);
|
|
751
|
+
|
|
482
752
|
if (headerMatch) inputPath = headerMatch[1].trim();
|
|
483
753
|
}
|
|
754
|
+
|
|
484
755
|
const target = await resolveWorkspacePath(cwd, inputPath, "apply_patch", false);
|
|
756
|
+
|
|
485
757
|
if (!isString(params?.patch) || !params.patch.trim()) {
|
|
486
758
|
throw new Error("apply_patch requires patch");
|
|
487
759
|
}
|
|
760
|
+
|
|
488
761
|
if (signal?.aborted) throw new Error("aborted");
|
|
489
762
|
|
|
490
763
|
const original = await vfs.read(target);
|
|
@@ -493,6 +766,7 @@ function createNativeAdapters(getCwd, vfs, config, index, ledger, hooks) {
|
|
|
493
766
|
const diff = buildPatchDiff(target, params.patch);
|
|
494
767
|
index.touch(relativeSlash(cwd, target));
|
|
495
768
|
const summary = await editSummary(cwd, target, original, resultText, diff, signal);
|
|
769
|
+
|
|
496
770
|
return textResult(summary, {
|
|
497
771
|
path: target,
|
|
498
772
|
hunks: hunkCount,
|
|
@@ -502,15 +776,19 @@ function createNativeAdapters(getCwd, vfs, config, index, ledger, hooks) {
|
|
|
502
776
|
},
|
|
503
777
|
async snap(params, signal) {
|
|
504
778
|
const cwd = getCwd();
|
|
779
|
+
|
|
505
780
|
if (!isString(params?.query) || !params.query.trim()) {
|
|
506
781
|
throw new Error("snap requires query");
|
|
507
782
|
}
|
|
783
|
+
|
|
508
784
|
if (signal?.aborted) throw new Error("aborted");
|
|
509
785
|
const snapTarget = params?.path ? await resolveWorkspacePath(cwd, params.path, "snap", true) : cwd;
|
|
510
786
|
const relativeRoot = path.relative(cwd, snapTarget);
|
|
787
|
+
|
|
511
788
|
const includeHidden = Boolean(params?.path) && relativeRoot
|
|
512
789
|
.split(path.sep)
|
|
513
790
|
.some((segment) => segment.startsWith(".") && segment.length > 1);
|
|
791
|
+
|
|
514
792
|
const res = await executeSnap({
|
|
515
793
|
query: params.query,
|
|
516
794
|
searchDir: snapTarget,
|
|
@@ -520,34 +798,45 @@ function createNativeAdapters(getCwd, vfs, config, index, ledger, hooks) {
|
|
|
520
798
|
pendingPaths: vfs.getOverlayPaths(),
|
|
521
799
|
signal,
|
|
522
800
|
});
|
|
801
|
+
|
|
523
802
|
return textResult(JSON.stringify(res, null, 2), res);
|
|
524
803
|
},
|
|
525
804
|
async evidence(params, signal) {
|
|
526
805
|
const cwd = getCwd();
|
|
806
|
+
|
|
527
807
|
if (!isString(params?.query) || !params.query.trim()) throw new Error("evidence requires query");
|
|
808
|
+
|
|
528
809
|
if (signal?.aborted) throw new Error("aborted");
|
|
529
810
|
const searchDir = params?.path ? await resolveWorkspacePath(cwd, params.path, "evidence", true) : cwd;
|
|
530
811
|
const options = {};
|
|
812
|
+
|
|
531
813
|
if (Number.isInteger(params?.k) && params.k > 0) options.k = params.k;
|
|
814
|
+
|
|
532
815
|
if (Number.isInteger(params?.maxChars) && params.maxChars > 0) options.maxChars = params.maxChars;
|
|
533
816
|
const res = await selectEvidence({ query: params.query, root: cwd, searchDir, index, overlayText: (p) => vfs.getOverlay(p), pendingPaths: vfs.getOverlayPaths(), options });
|
|
817
|
+
|
|
534
818
|
for (const span of res.spans) ledger.recordOrigin(span.path, span.lines[0], span.text.split("\n"));
|
|
819
|
+
|
|
535
820
|
return textResult(JSON.stringify(res), { route: res.route, count: res.spans.length });
|
|
536
821
|
},
|
|
537
822
|
async surface(params, signal) {
|
|
538
823
|
const cwd = getCwd();
|
|
539
824
|
const target = await resolveWorkspacePath(cwd, params?.path, "surface", false);
|
|
825
|
+
|
|
540
826
|
if (signal?.aborted) throw new Error("aborted");
|
|
541
827
|
const text = await vfs.read(target);
|
|
542
828
|
const ext = path.extname(target);
|
|
543
829
|
const outline = extractStructuralSurface(text, ext);
|
|
830
|
+
|
|
544
831
|
return textResult(JSON.stringify(outline, null, 2), { path: target, count: outline.items.length });
|
|
545
832
|
},
|
|
546
833
|
async bash(params, signal) {
|
|
547
834
|
const cwd = getCwd();
|
|
548
|
-
const literal = params?.
|
|
835
|
+
const literal = Array.isArray(params?.args) && process.platform !== "win32" && params.args.length === Object.keys(params.args).length && params.args.every(isString);
|
|
836
|
+
|
|
549
837
|
if (literal && (!isString(params.command) || !Array.isArray(params.args) || params.args.some(arg => !isString(arg)))) throw new Error("bash argv requires a command string and an array of string args");
|
|
550
838
|
const command = literal ? String(params.command) : unwrapIfFullyQuoted(String(params?.command ?? "").trim());
|
|
839
|
+
|
|
551
840
|
if (!command.trim()) throw new Error("bash requires command");
|
|
552
841
|
const targetCwd = params?.cwd ? await resolveWorkspacePath(cwd, params.cwd, "bash cwd", true) : cwd;
|
|
553
842
|
|
|
@@ -556,6 +845,7 @@ function createNativeAdapters(getCwd, vfs, config, index, ledger, hooks) {
|
|
|
556
845
|
|
|
557
846
|
const transactionBarrier = await vfs.prepareExternalMutation("bash");
|
|
558
847
|
let res;
|
|
848
|
+
|
|
559
849
|
try {
|
|
560
850
|
res = await runCommand(argv, {
|
|
561
851
|
cwd: targetCwd,
|
|
@@ -574,9 +864,12 @@ function createNativeAdapters(getCwd, vfs, config, index, ledger, hooks) {
|
|
|
574
864
|
clearPathCache();
|
|
575
865
|
hooks.workspaceChanged();
|
|
576
866
|
}
|
|
867
|
+
|
|
577
868
|
const { stdout, stderr } = res;
|
|
578
869
|
let text = stdout && stderr ? stdout + (stdout.endsWith("\n") ? "" : "\n") + stderr : stdout || stderr;
|
|
870
|
+
|
|
579
871
|
if (res.exitCode !== 0) text += await sourceForReferences(cwd, targetCwd, text);
|
|
872
|
+
|
|
580
873
|
return {
|
|
581
874
|
content: [{ type: "text", text }],
|
|
582
875
|
details: { exitCode: res.exitCode, signal: res.signal, outputTruncated: res.outputTruncated, transactionBarrier },
|
|
@@ -586,66 +879,90 @@ function createNativeAdapters(getCwd, vfs, config, index, ledger, hooks) {
|
|
|
586
879
|
async grep(params, signal) {
|
|
587
880
|
const cwd = getCwd();
|
|
588
881
|
const pattern = String(params?.pattern || "");
|
|
882
|
+
|
|
589
883
|
if (!pattern) throw new Error("grep requires pattern");
|
|
590
884
|
const searchPath = params?.path ? await resolveWorkspacePath(cwd, params.path, "grep", true) : cwd;
|
|
591
885
|
const indexed = await grepIndexed(index, pattern, params, searchPath, cwd);
|
|
886
|
+
|
|
592
887
|
if (indexed !== null) return textResult(indexed, { exitCode: indexed ? 0 : 1, via: "index" });
|
|
593
888
|
// Large tree: real rg keeps its own output format.
|
|
594
889
|
const res = await runCommand(["rg", ...rgGrepArgs(pattern, params, searchPath)], { cwd, timeoutMs: 30_000, signal });
|
|
890
|
+
|
|
595
891
|
if (res.exitCode !== 0 && res.exitCode !== 1) {
|
|
596
892
|
throw new Error(res.stderr.trim() || `rg exited ${res.exitCode}`);
|
|
597
893
|
}
|
|
894
|
+
|
|
598
895
|
return textResult(res.stdout, { exitCode: res.exitCode });
|
|
599
896
|
},
|
|
600
897
|
async glob(params, signal) {
|
|
601
898
|
const cwd = getCwd();
|
|
602
899
|
const pattern = String(params?.pattern || "");
|
|
900
|
+
|
|
603
901
|
if (!pattern) throw new Error("glob requires pattern");
|
|
604
902
|
const fuzzy = await fuzzyFind(index, cwd, cwd, pattern);
|
|
903
|
+
|
|
605
904
|
if (fuzzy !== null) return textResult(fuzzy, { via: "fuzzy" });
|
|
606
905
|
const indexed = await listIndexed(index, cwd, cwd, pattern);
|
|
906
|
+
|
|
607
907
|
if (indexed !== null) return textResult(indexed, { via: "index" });
|
|
908
|
+
|
|
608
909
|
const rg = await runCommand(["rg", "--files", "-g", pattern], { cwd, timeoutMs: 30_000, signal }).catch(
|
|
609
910
|
() => null,
|
|
610
911
|
);
|
|
912
|
+
|
|
611
913
|
if (rg && (rg.exitCode === 0 || rg.exitCode === 1)) {
|
|
612
914
|
return textResult(rg.stdout, { via: "rg" });
|
|
613
915
|
}
|
|
916
|
+
|
|
614
917
|
const findPattern = pattern.startsWith("./") ? pattern : `./${pattern}`;
|
|
918
|
+
|
|
615
919
|
const fallback = await runCommand(["find", ".", "-type", "f", "-path", findPattern], {
|
|
616
920
|
cwd,
|
|
617
921
|
timeoutMs: 30_000,
|
|
618
922
|
signal,
|
|
619
923
|
});
|
|
924
|
+
|
|
620
925
|
return textResult(fallback.stdout, { via: "find" });
|
|
621
926
|
},
|
|
622
927
|
async find(params, signal) {
|
|
623
928
|
const cwd = getCwd();
|
|
624
929
|
const searchDir = params?.path ? await resolveWorkspacePath(cwd, params.path, "find", true) : cwd;
|
|
625
930
|
const pattern = params?.pattern || params?.glob;
|
|
931
|
+
|
|
626
932
|
if (signal?.aborted) throw new Error("aborted");
|
|
627
933
|
const globPattern = pattern ? String(pattern) : null;
|
|
628
934
|
const fuzzy = await fuzzyFind(index, searchDir, cwd, globPattern);
|
|
935
|
+
|
|
629
936
|
if (fuzzy !== null) return textResult(fuzzy, { via: "fuzzy" });
|
|
630
937
|
const indexed = await listIndexed(index, searchDir, cwd, globPattern);
|
|
938
|
+
|
|
631
939
|
if (indexed !== null) return textResult(indexed, { via: "index" });
|
|
940
|
+
|
|
632
941
|
return listWithTools(searchDir, globPattern, cwd, signal);
|
|
633
942
|
},
|
|
634
943
|
async ls(params, signal) {
|
|
635
944
|
const cwd = getCwd();
|
|
636
945
|
const dirPath = params?.path ? await resolveWorkspacePath(cwd, params.path, "ls", true) : cwd;
|
|
946
|
+
|
|
637
947
|
return readDirectory(dirPath, signal);
|
|
638
948
|
},
|
|
639
949
|
};
|
|
640
950
|
}
|
|
641
951
|
|
|
642
|
-
|
|
952
|
+
/**
|
|
953
|
+
* Fused INVOKE kernel. Guest RPC is the only caller; fuel is cwd + vfs + signal.
|
|
954
|
+
* BIND stays downward (see tests/contracts/layers.test.mjs). Do not split this
|
|
955
|
+
* closure into pass-through files that re-import each other.
|
|
956
|
+
*/
|
|
957
|
+
export function createHostBridge({ pi, config, getCwd, registry, ledger: runLedger, budget }) {
|
|
643
958
|
const index = registry?.index ?? new WorkspaceIndex((argv, opts) => runCommand(argv, opts));
|
|
644
|
-
const ledger = runLedger ?? new SeenLedger({ window: config.seenWindow ??
|
|
959
|
+
const ledger = runLedger ?? new SeenLedger({ window: config.seenWindow ?? 0 });
|
|
960
|
+
|
|
645
961
|
const vfs = new CausalVfs(paths => {
|
|
646
962
|
index.invalidate();
|
|
647
963
|
notifyWorkspaceChanged(paths);
|
|
648
964
|
}, target => resolveWorkspacePath(getCwd(), target, "commit", false, true));
|
|
965
|
+
|
|
649
966
|
const executors = registry?.executors ?? new Map();
|
|
650
967
|
const definitions = registry?.definitions ?? new Map();
|
|
651
968
|
const sharedRegistry = registry ?? { executors, definitions, index, callSeq: 0 };
|
|
@@ -660,20 +977,25 @@ export function createHostBridge({ pi, config, getCwd, registry, ledger: runLedg
|
|
|
660
977
|
let trace = [];
|
|
661
978
|
let callListener = null;
|
|
662
979
|
const scheduler = createNativeScheduler();
|
|
980
|
+
|
|
663
981
|
// Advisory host event, not a tool or a transaction participant. Consumers
|
|
664
982
|
// invalidate synchronously; failures must never affect committed bytes.
|
|
665
983
|
function notifyWorkspaceChanged(paths = null) {
|
|
666
984
|
if (!isFunction(pi?.events?.emit)) return;
|
|
985
|
+
|
|
667
986
|
const event = Object.freeze({
|
|
668
987
|
version: 1, cwd: path.resolve(getCwd()),
|
|
669
988
|
paths: paths === null ? null : Object.freeze([...new Set(paths)]),
|
|
670
989
|
});
|
|
990
|
+
|
|
671
991
|
try { pi.events.emit("workspace:changed", event)?.catch?.(() => {}); } catch {}
|
|
672
992
|
}
|
|
993
|
+
|
|
673
994
|
hooks.workspaceChanged = notifyWorkspaceChanged;
|
|
674
995
|
hooks.artifactsDir = () => activeCtx?.sessionManager?.getArtifactsDir?.();
|
|
675
996
|
hooks.commandEnv = () => {
|
|
676
997
|
const env = { ...process.env };
|
|
998
|
+
|
|
677
999
|
const current = {
|
|
678
1000
|
PI_SESSION_ID: activeCtx?.sessionManager?.getSessionId?.(),
|
|
679
1001
|
PI_SESSION_FILE: activeCtx?.sessionManager?.getSessionFile?.(),
|
|
@@ -681,10 +1003,12 @@ export function createHostBridge({ pi, config, getCwd, registry, ledger: runLedg
|
|
|
681
1003
|
PI_MODEL: activeCtx?.model?.id,
|
|
682
1004
|
PI_REASONING_LEVEL: activeCtx?.thinkingLevel,
|
|
683
1005
|
};
|
|
1006
|
+
|
|
684
1007
|
for (const [key, value] of Object.entries(current)) {
|
|
685
1008
|
if (isString(value)) env[key] = value;
|
|
686
1009
|
else delete env[key];
|
|
687
1010
|
}
|
|
1011
|
+
|
|
688
1012
|
return env;
|
|
689
1013
|
};
|
|
690
1014
|
|
|
@@ -702,6 +1026,7 @@ export function createHostBridge({ pi, config, getCwd, registry, ledger: runLedg
|
|
|
702
1026
|
executors.set(tool.name, tool.execute.bind(tool));
|
|
703
1027
|
definitions.set(tool.name, tool);
|
|
704
1028
|
}
|
|
1029
|
+
|
|
705
1030
|
return original(tool);
|
|
706
1031
|
};
|
|
707
1032
|
}
|
|
@@ -721,33 +1046,45 @@ export function createHostBridge({ pi, config, getCwd, registry, ledger: runLedg
|
|
|
721
1046
|
function hostTool(name) {
|
|
722
1047
|
if (!hostSession) return undefined;
|
|
723
1048
|
const metadata = definitions.get(name);
|
|
1049
|
+
|
|
724
1050
|
// Keep Supernova's transactional adapters for ordinary built-ins. Respect overrides.
|
|
725
1051
|
if (Object.hasOwn(natives, name) && metadata?.sourceInfo?.source === "builtin") return undefined;
|
|
1052
|
+
|
|
726
1053
|
return hostSession.getToolForEvalBridge?.(name);
|
|
727
1054
|
}
|
|
728
1055
|
|
|
729
1056
|
function isCallable(name) {
|
|
730
1057
|
if (name === "supernova" || (config.excludeTools ?? []).includes(name)) return false;
|
|
1058
|
+
|
|
731
1059
|
if (hostSession && (hostSession.isDisposed || hostSession.sessionManager.getSessionId() !== boundSessionId)) return false;
|
|
1060
|
+
|
|
732
1061
|
// An internal adapter belongs to Supernova, not the host's visible tool list.
|
|
733
1062
|
const nativeOwned = Object.hasOwn(natives, name) && !executors.has(name)
|
|
734
1063
|
&& (!hostSession || !definitions.has(name) || definitions.get(name).sourceInfo?.source === "builtin");
|
|
1064
|
+
|
|
735
1065
|
if (nativeOwned) return true;
|
|
1066
|
+
|
|
736
1067
|
if (hostSession) {
|
|
737
1068
|
if (!hostSession.getEvalBridgeToolNames().includes(name) && definitions.has(name)) return false;
|
|
1069
|
+
|
|
738
1070
|
return !!hostTool(name) || (Object.hasOwn(natives, name) && (!definitions.has(name) || definitions.get(name).sourceInfo?.source === "builtin"));
|
|
739
1071
|
}
|
|
1072
|
+
|
|
740
1073
|
if (definitions.has(name) && isFunction(pi?.getActiveTools) && !pi.getActiveTools().includes(name)) return false;
|
|
1074
|
+
|
|
741
1075
|
return executors.has(name) || Object.hasOwn(natives, name);
|
|
742
1076
|
}
|
|
743
1077
|
|
|
744
1078
|
function refreshTools() {
|
|
745
1079
|
const tools = pi?.getAllTools?.() ?? [];
|
|
1080
|
+
|
|
746
1081
|
for (const tool of tools) {
|
|
747
1082
|
if (!isString(tool?.name)) continue;
|
|
748
1083
|
definitions.set(tool.name, { ...definitions.get(tool.name), ...tool });
|
|
1084
|
+
|
|
749
1085
|
if (!hostSession && isFunction(tool.execute)) executors.set(tool.name, tool.execute.bind(tool));
|
|
750
1086
|
}
|
|
1087
|
+
|
|
751
1088
|
return [...definitions.values()].filter(tool => isCallable(tool.name));
|
|
752
1089
|
}
|
|
753
1090
|
|
|
@@ -785,20 +1122,9 @@ export function createHostBridge({ pi, config, getCwd, registry, ledger: runLedg
|
|
|
785
1122
|
return vfs.rollback();
|
|
786
1123
|
}
|
|
787
1124
|
|
|
788
|
-
function resultDiff(response) {
|
|
789
|
-
let details = response?.details;
|
|
790
|
-
if (isString(details)) {
|
|
791
|
-
try {
|
|
792
|
-
details = JSON.parse(details);
|
|
793
|
-
} catch {
|
|
794
|
-
return undefined;
|
|
795
|
-
}
|
|
796
|
-
}
|
|
797
|
-
return isObject(details) ? details.diff : undefined;
|
|
798
|
-
}
|
|
799
|
-
|
|
800
1125
|
function notifyCall(record) {
|
|
801
1126
|
if (!callListener) return;
|
|
1127
|
+
|
|
802
1128
|
try {
|
|
803
1129
|
callListener(record, [...trace]);
|
|
804
1130
|
} catch {}
|
|
@@ -807,19 +1133,25 @@ export function createHostBridge({ pi, config, getCwd, registry, ledger: runLedg
|
|
|
807
1133
|
function checkCallBudget(name) {
|
|
808
1134
|
if (closed) throw new Error("program is already complete");
|
|
809
1135
|
const maxCalls = config.maxBridgeCalls ?? 256;
|
|
1136
|
+
|
|
1137
|
+
if (budget && ++budget.calls > maxCalls) throw new Error("host call budget exceeded (" + maxCalls + " calls per program batch): split the batch");
|
|
810
1138
|
callCount += 1;
|
|
1139
|
+
|
|
811
1140
|
if (callCount > maxCalls) {
|
|
812
1141
|
throw new Error(
|
|
813
1142
|
`host call budget exceeded (${maxCalls} calls per program): split the work across programs`,
|
|
814
1143
|
);
|
|
815
1144
|
}
|
|
1145
|
+
|
|
816
1146
|
if (activeSignal?.aborted) throw new Error("aborted");
|
|
1147
|
+
|
|
817
1148
|
if (!isString(name) || !name) throw new Error("tool name required");
|
|
818
1149
|
}
|
|
819
1150
|
|
|
820
1151
|
function assertCallableTarget(name) {
|
|
821
1152
|
// Never re-enter supernova or other excluded composition tools via the bridge.
|
|
822
1153
|
const excluded = new Set(config.excludeTools || []);
|
|
1154
|
+
|
|
823
1155
|
if (name === "supernova" || excluded.has(name)) {
|
|
824
1156
|
throw new Error(
|
|
825
1157
|
`${name} is blocked (excluded / non-reentrant).`,
|
|
@@ -831,17 +1163,20 @@ export function createHostBridge({ pi, config, getCwd, registry, ledger: runLedg
|
|
|
831
1163
|
if (name !== "write" || !isString(args?.path) || !isString(args?.content)) return undefined;
|
|
832
1164
|
const target = await resolveWorkspacePath(getCwd(), args.path, "write", false);
|
|
833
1165
|
let previous = "";
|
|
1166
|
+
|
|
834
1167
|
try {
|
|
835
1168
|
previous = await vfs.read(target);
|
|
836
1169
|
} catch (error) {
|
|
837
1170
|
if (error?.code !== "ENOENT") throw error;
|
|
838
1171
|
}
|
|
1172
|
+
|
|
839
1173
|
return buildWriteDiff(target, previous, args.content);
|
|
840
1174
|
}
|
|
841
1175
|
|
|
842
1176
|
function completeRecord(record, res, fallbackDiff) {
|
|
843
1177
|
const diff = resultDiff(res) || fallbackDiff;
|
|
844
1178
|
finishRecord(record, res);
|
|
1179
|
+
|
|
845
1180
|
if (diff && record.ok) record.diff = diff;
|
|
846
1181
|
notifyCall(record);
|
|
847
1182
|
}
|
|
@@ -850,6 +1185,7 @@ export function createHostBridge({ pi, config, getCwd, registry, ledger: runLedg
|
|
|
850
1185
|
checkCallBudget(name);
|
|
851
1186
|
const callId = ++sharedRegistry.callSeq;
|
|
852
1187
|
assertCallableTarget(name);
|
|
1188
|
+
|
|
853
1189
|
if (!isCallable(name)) throw new Error(unknownToolMessage(name, [...definitions.keys(), ...Object.keys(natives)].filter(isCallable)));
|
|
854
1190
|
|
|
855
1191
|
const command = { apply_patch: "edit", surface: "read", evidence: "read", snap: "read" }[name] ?? name;
|
|
@@ -860,18 +1196,28 @@ export function createHostBridge({ pi, config, getCwd, registry, ledger: runLedg
|
|
|
860
1196
|
try {
|
|
861
1197
|
const delegated = hostTool(name);
|
|
862
1198
|
const exec = delegated ? delegated.execute.bind(delegated) : hostSession ? undefined : executors.get(name);
|
|
863
|
-
|
|
1199
|
+
const argvOwned = name === "bash" && Array.isArray(args?.args) && process.platform !== "win32" && args.args.length === Object.keys(args.args).length && args.args.every(isString);
|
|
1200
|
+
|
|
1201
|
+
if (exec && !argvOwned) {
|
|
1202
|
+
if (name === "read" && (args?.json !== undefined || /^(agent|artifact):\/\/.*\?/i.test(String(args?.path)))) throw new Error("JSON projection requires the Supernova-owned read adapter, not an external override");
|
|
1203
|
+
|
|
864
1204
|
if (name === "write" && args?.append === true) throw new Error("append requires the Supernova-owned write adapter, not an external override");
|
|
865
1205
|
const fallbackDiff = await writeFallbackDiff(name, args);
|
|
866
1206
|
const mutating = isMutatingTool(name, config, args, definitions.get(name));
|
|
1207
|
+
|
|
867
1208
|
if (mutating) await vfs.prepareExternalMutation(name);
|
|
1209
|
+
|
|
868
1210
|
if (activeSignal?.aborted || closed) throw new Error("aborted");
|
|
1211
|
+
|
|
869
1212
|
if (!isCallable(name)) throw new Error("tool is no longer enabled in this session: " + name);
|
|
1213
|
+
|
|
870
1214
|
try {
|
|
871
1215
|
const res = await exec(`supernova:${name}:${callId}`, args || {}, activeSignal, undefined, delegated
|
|
872
1216
|
? { ...activeCtx, settings: hostSession.settings, toolNames: hostSession.getEvalBridgeToolNames(), autoApprove: false }
|
|
873
1217
|
: activeCtx);
|
|
1218
|
+
|
|
874
1219
|
completeRecord(record, res, fallbackDiff);
|
|
1220
|
+
|
|
875
1221
|
return res;
|
|
876
1222
|
} finally {
|
|
877
1223
|
if (mutating) { vfs.invalidateCache(); index.invalidate(); clearPathCache(); notifyWorkspaceChanged(); }
|
|
@@ -879,9 +1225,11 @@ export function createHostBridge({ pi, config, getCwd, registry, ledger: runLedg
|
|
|
879
1225
|
}
|
|
880
1226
|
|
|
881
1227
|
const native = Object.hasOwn(natives, name) ? natives[name] : undefined;
|
|
1228
|
+
|
|
882
1229
|
if (native) {
|
|
883
1230
|
const res = await native(args || {}, activeSignal);
|
|
884
1231
|
completeRecord(record, res);
|
|
1232
|
+
|
|
885
1233
|
return res;
|
|
886
1234
|
}
|
|
887
1235
|
|
|
@@ -899,25 +1247,36 @@ export function createHostBridge({ pi, config, getCwd, registry, ledger: runLedg
|
|
|
899
1247
|
record.ms = Date.now() - record.time;
|
|
900
1248
|
record.ok = !hostResultFailed(res);
|
|
901
1249
|
const exitCode = isObject(res?.details) ? res.details.exitCode : undefined;
|
|
1250
|
+
|
|
902
1251
|
if (Number.isInteger(exitCode) && exitCode !== 0) record.exitCode = exitCode;
|
|
1252
|
+
const text = isObject(res) && Array.isArray(res.content)
|
|
1253
|
+
? res.content.filter(part => part?.type === "text" && isString(part.text)).map(part => part.text).join("\n")
|
|
1254
|
+
: undefined;
|
|
1255
|
+
|
|
1256
|
+
if (text) record.resultText = text;
|
|
903
1257
|
}
|
|
904
1258
|
|
|
905
1259
|
async function call(name, args) {
|
|
906
1260
|
if (!isString(name) || !name) throw new Error("nova.call requires a tool name");
|
|
907
1261
|
const invoke = async () => packageHostResult(await invokeRaw(name, args), config);
|
|
908
1262
|
const kind = isMutatingTool(name, config, args, definitions.get(name)) ? "write" : "read";
|
|
1263
|
+
|
|
909
1264
|
return scheduler.schedule(kind, invoke, activeSignal);
|
|
910
1265
|
}
|
|
911
1266
|
|
|
912
1267
|
async function callMany(calls) {
|
|
913
1268
|
if (!Array.isArray(calls)) throw new TypeError("nova.callMany requires an array");
|
|
914
1269
|
const list = calls;
|
|
1270
|
+
|
|
915
1271
|
if (list.some(item => !isString(item?.name) || !item.name)) throw new TypeError("nova.callMany entries require a tool name");
|
|
1272
|
+
|
|
916
1273
|
const thunks = list.map((item) => {
|
|
917
1274
|
const n = item?.name;
|
|
918
1275
|
const a = item?.args;
|
|
1276
|
+
|
|
919
1277
|
return () => call(n, a);
|
|
920
1278
|
});
|
|
1279
|
+
|
|
921
1280
|
const names = list.map((item) => item?.name).filter((n) => isString(n));
|
|
922
1281
|
const wave = await runParallelWave(thunks, { names, calls: list, definitions: names.map(name => definitions.get(name)) }, { mode: "auto", config });
|
|
923
1282
|
// Return a results array that also carries .mode/.reason, and is directly
|
|
@@ -928,6 +1287,7 @@ export function createHostBridge({ pi, config, getCwd, registry, ledger: runLedg
|
|
|
928
1287
|
reason: { value: wave.reason, enumerable: false },
|
|
929
1288
|
results: { value: results, enumerable: false },
|
|
930
1289
|
});
|
|
1290
|
+
|
|
931
1291
|
return results;
|
|
932
1292
|
}
|
|
933
1293
|
|
|
@@ -955,12 +1315,13 @@ export function createHostBridge({ pi, config, getCwd, registry, ledger: runLedg
|
|
|
955
1315
|
},
|
|
956
1316
|
},
|
|
957
1317
|
fork(options) {
|
|
958
|
-
return createHostBridge({ pi, config, getCwd: options.getCwd, registry: sharedRegistry, ledger: ledger.fork() });
|
|
1318
|
+
return createHostBridge({ pi, config, getCwd: options.getCwd, registry: sharedRegistry, ledger: ledger.fork(), budget: options.budget });
|
|
959
1319
|
},
|
|
960
1320
|
close() { closed = true; vfs.closed = true; },
|
|
961
1321
|
bindCallContext,
|
|
962
1322
|
resetCallBudget,
|
|
963
1323
|
getTrace,
|
|
1324
|
+
getMutations: () => ({ ...vfs.mutations }),
|
|
964
1325
|
setCallListener,
|
|
965
1326
|
barrier: run => scheduler.schedule("write", run, activeSignal),
|
|
966
1327
|
beginSpeculation,
|