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