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