pi-supernova 0.5.0 → 0.6.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 +73 -11
- package/docs/CHANGELOG.md +46 -0
- package/docs/TOKEN_COSTS.md +63 -29
- package/index.js +7 -4
- package/package.json +2 -2
- package/src/bridge/catalog.js +14 -10
- package/src/bridge/host-bridge.js +810 -145
- package/src/bridge/native-tools.js +16 -6
- package/src/context/evidence.js +13 -5
- package/src/context/fuzzy.js +34 -13
- package/src/context/outline.js +11 -2
- package/src/context/repo-index.js +150 -15
- package/src/context/search.js +70 -17
- package/src/context/snap.js +98 -46
- package/src/context/surface.js +15 -9
- package/src/fs/check.js +10 -2
- package/src/fs/patch.js +4 -2
- package/src/fs/vfs.js +133 -36
- package/src/fs/workspace.js +7 -1
- package/src/output/bottleneck.js +36 -11
- package/src/output/format.js +44 -29
- package/src/runtime/guest-worker.js +135 -39
- package/src/runtime/parallel.js +4 -1
- package/src/runtime/program-batch.js +17 -6
- package/src/runtime/program-file.js +6 -3
- package/src/runtime/reference.js +5 -5
- package/src/runtime/runtime.js +17 -6
- package/src/shared/decode.js +15 -3
- package/src/ui/omp-frame.js +11 -4
- package/src/ui/render.js +2 -2
|
@@ -3,13 +3,14 @@ import * as fs from "node:fs/promises";
|
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
5
|
import { packageHostResult, hostResultFailed } from "../output/bottleneck.js";
|
|
6
|
+
import { truncateChars } from "../output/format.js";
|
|
6
7
|
import { isString, isNumber, isFunction, isObject, looksLikePath } from "../shared/decode.js";
|
|
7
8
|
import { isMutatingTool, runParallelWave, createNativeScheduler } from "../runtime/parallel.js";
|
|
8
9
|
import { unknownToolMessage } from "./catalog.js";
|
|
9
10
|
import { extractStructuralSurface } from "../context/surface.js";
|
|
10
11
|
import { pickSpan } from "../context/spans.js";
|
|
11
12
|
import { buildEditDiff, buildMultiEditDiff, buildPatchDiff, buildWriteDiff } from "../fs/diff.js";
|
|
12
|
-
import { executeSnap } from "../context/snap.js";
|
|
13
|
+
import { executeSnap, tokenizeQuery, stem } from "../context/snap.js";
|
|
13
14
|
import { selectEvidence } from "../context/evidence.js";
|
|
14
15
|
import { WorkspaceIndex } from "../context/repo-index.js";
|
|
15
16
|
import { outlineFile } from "../context/outline.js";
|
|
@@ -58,13 +59,58 @@ function unwrapIfFullyQuoted(s) {
|
|
|
58
59
|
return inner;
|
|
59
60
|
}
|
|
60
61
|
|
|
61
|
-
function
|
|
62
|
-
|
|
63
|
-
const
|
|
62
|
+
function sliceLinesRawInfo(text, offset, limit) {
|
|
63
|
+
const logical = contentLineInfo(text).count;
|
|
64
|
+
const totalLines = text === "" ? 1 : logical + (text.endsWith("\n") ? 1 : 0);
|
|
65
|
+
|
|
66
|
+
if (!isNumber(offset) && !isNumber(limit)) {
|
|
67
|
+
return { text, end: totalLines, total: totalLines, count: totalLines, eof: true, whole: true };
|
|
68
|
+
}
|
|
69
|
+
|
|
64
70
|
const startIndex = (isNumber(offset) ? Math.max(1, Math.floor(offset)) : 1) - 1;
|
|
65
|
-
const count = isNumber(limit) ? Math.max(0, Math.floor(limit)) :
|
|
71
|
+
const count = isNumber(limit) ? Math.max(0, Math.floor(limit)) : totalLines;
|
|
72
|
+
|
|
73
|
+
if (count === 0 || startIndex >= totalLines) {
|
|
74
|
+
const emptyFile = totalLines === 1 && text === "";
|
|
75
|
+
|
|
76
|
+
return { text: "", end: totalLines, total: totalLines, count: 0, eof: true, whole: emptyFile };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const endExclusive = Math.min(totalLines, startIndex + count);
|
|
80
|
+
const start = lineStartIndex(text, startIndex + 1);
|
|
81
|
+
const end = lineEndIndex(text, start, endExclusive - startIndex);
|
|
82
|
+
let selected = text.slice(start, end);
|
|
83
|
+
const eof = endExclusive >= totalLines || (endExclusive === totalLines - 1 && text.endsWith("\n"));
|
|
84
|
+
|
|
85
|
+
if (endExclusive < totalLines && !selected.endsWith("\n")) selected += "\n";
|
|
86
|
+
|
|
87
|
+
return { text: selected, end: endExclusive, total: totalLines, count: endExclusive - startIndex, eof, whole: startIndex === 0 && eof };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Read-window slicing preserves the selected lines' own line ending. */
|
|
91
|
+
function sliceLinesRaw(text, offset, limit) {
|
|
92
|
+
return sliceLinesRawInfo(text, offset, limit).text;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function readLineParam(value, name) {
|
|
96
|
+
if (value === undefined) return undefined;
|
|
97
|
+
const number = isNumber(value) ? value : isString(value) && value.trim() !== "" ? Number(value) : NaN;
|
|
98
|
+
|
|
99
|
+
if (!Number.isFinite(number)) throw new Error("read " + name + " must be a finite number");
|
|
100
|
+
|
|
101
|
+
return Math.floor(number);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function normalizeReadWindow(params) {
|
|
105
|
+
if (!isObject(params)) return params;
|
|
106
|
+
const normalized = { ...params };
|
|
107
|
+
const offset = readLineParam(params.offset, "offset");
|
|
108
|
+
const limit = readLineParam(params.limit, "limit");
|
|
66
109
|
|
|
67
|
-
|
|
110
|
+
if (offset !== undefined) normalized.offset = Math.max(1, offset);
|
|
111
|
+
if (limit !== undefined) normalized.limit = Math.max(0, limit);
|
|
112
|
+
|
|
113
|
+
return normalized;
|
|
68
114
|
}
|
|
69
115
|
|
|
70
116
|
function resolveReadPath(cwd, target) {
|
|
@@ -77,12 +123,16 @@ function resolveReadPath(cwd, target) {
|
|
|
77
123
|
async function probeExistingPath(cwd, targetParam, vfs) {
|
|
78
124
|
const targetPath = resolveReadPath(cwd, targetParam);
|
|
79
125
|
|
|
80
|
-
if (vfs.getOverlay(targetPath) !== undefined)
|
|
126
|
+
if (vfs.getOverlay(targetPath) !== undefined) {
|
|
127
|
+
const overlay = vfs.getOverlay(targetPath);
|
|
128
|
+
|
|
129
|
+
return { path: targetPath, directory: false, size: Buffer.byteLength(overlay, "utf8"), overlay };
|
|
130
|
+
}
|
|
81
131
|
|
|
82
132
|
try {
|
|
83
133
|
const st = await fs.stat(targetPath);
|
|
84
134
|
|
|
85
|
-
return { path: targetPath, directory: st.isDirectory() };
|
|
135
|
+
return { path: targetPath, directory: st.isDirectory(), size: st.size };
|
|
86
136
|
} catch (err) {
|
|
87
137
|
if (err?.code !== "ENOENT" && err?.code !== "ENOTDIR") throw err;
|
|
88
138
|
|
|
@@ -94,6 +144,8 @@ async function probeExistingPath(cwd, targetParam, vfs) {
|
|
|
94
144
|
|
|
95
145
|
const EDIT_PREVIEW_LINES = 16;
|
|
96
146
|
|
|
147
|
+
const MAX_DIRECTORY_ENTRIES = 10000;
|
|
148
|
+
|
|
97
149
|
function sourceLines(content) {
|
|
98
150
|
const raw = content.split("\n");
|
|
99
151
|
|
|
@@ -115,12 +167,11 @@ function formatNumberedLine(n, text) {
|
|
|
115
167
|
}
|
|
116
168
|
|
|
117
169
|
function numberedPreview(content, cap = EDIT_PREVIEW_LINES) {
|
|
118
|
-
const
|
|
170
|
+
const { count, preview } = contentLineInfo(content, cap);
|
|
119
171
|
|
|
120
|
-
if (
|
|
121
|
-
const
|
|
122
|
-
const
|
|
123
|
-
const suffix = lines.length > cap ? lines.length + " lines total" : lines.length + " lines";
|
|
172
|
+
if (count === 0) return "0 lines";
|
|
173
|
+
const body = preview.map((line, i) => formatNumberedLine(i + 1, line)).join("\n");
|
|
174
|
+
const suffix = count + " lines total";
|
|
124
175
|
|
|
125
176
|
return body + "\n" + suffix;
|
|
126
177
|
}
|
|
@@ -134,22 +185,28 @@ function applyReplacements(target, content, requestedEdits) {
|
|
|
134
185
|
}
|
|
135
186
|
|
|
136
187
|
if (!isString(replacement?.newText)) throw new Error("edit requires newText");
|
|
137
|
-
const
|
|
188
|
+
const oldText = String(replacement.oldText);
|
|
189
|
+
const newText = String(replacement.newText);
|
|
190
|
+
const index = content.indexOf(oldText);
|
|
138
191
|
|
|
139
192
|
if (index < 0) {
|
|
140
193
|
throw new Error("edit target not found in " + target + ": oldText must match the file byte-for-byte\n" + numberedPreview(content));
|
|
141
194
|
}
|
|
142
|
-
const second = content.indexOf(
|
|
195
|
+
const second = content.indexOf(oldText, index + 1);
|
|
143
196
|
|
|
144
197
|
if (second >= 0) {
|
|
145
|
-
const lines = sourceLines(content);
|
|
146
198
|
const a = lineNumberAt(content, index);
|
|
147
199
|
const b = lineNumberAt(content, second);
|
|
200
|
+
const lineText = n => {
|
|
201
|
+
const range = lineTextRange(content, n);
|
|
202
|
+
|
|
203
|
+
return content.slice(range.start, range.end).replace(/\r?\n$/, "");
|
|
204
|
+
};
|
|
148
205
|
|
|
149
|
-
throw new Error("edit target is not unique in " + target + ": lines " + a + " and " + b + "; include more surrounding lines in oldText, or pass edits:[{oldText,newText},…]\n" + formatNumberedLine(a,
|
|
206
|
+
throw new Error("edit target is not unique in " + target + ": lines " + a + " and " + b + "; include more surrounding lines in oldText, or pass edits:[{oldText,newText},…]\n" + formatNumberedLine(a, lineText(a)) + "\n" + formatNumberedLine(b, lineText(b)));
|
|
150
207
|
}
|
|
151
208
|
|
|
152
|
-
return { ...replacement, index, end: index +
|
|
209
|
+
return { ...replacement, oldText, newText, index, end: index + oldText.length };
|
|
153
210
|
});
|
|
154
211
|
|
|
155
212
|
matches.sort((a, b) => a.index - b.index);
|
|
@@ -168,8 +225,57 @@ function applyReplacements(target, content, requestedEdits) {
|
|
|
168
225
|
return { updated, matches };
|
|
169
226
|
}
|
|
170
227
|
|
|
228
|
+
function lineStartIndex(content, line) {
|
|
229
|
+
let index = 0;
|
|
230
|
+
|
|
231
|
+
for (let current = 1; current < line; current++) {
|
|
232
|
+
const next = content.indexOf("\n", index);
|
|
233
|
+
|
|
234
|
+
if (next < 0) return content.length;
|
|
235
|
+
index = next + 1;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return Math.min(index, content.length);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function lineEndIndex(content, startIndex, lineCount) {
|
|
242
|
+
let index = startIndex;
|
|
243
|
+
|
|
244
|
+
for (let i = 0; i < lineCount; i++) {
|
|
245
|
+
const next = content.indexOf("\n", index);
|
|
246
|
+
|
|
247
|
+
if (next < 0) return content.length;
|
|
248
|
+
index = next + 1;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return index;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function lineTextRange(content, line) {
|
|
255
|
+
const start = lineStartIndex(content, line);
|
|
256
|
+
|
|
257
|
+
return { start, end: lineEndIndex(content, start, 1) };
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function shiftDiffLines(diff, delta) {
|
|
261
|
+
if (!diff || delta === 0) return diff;
|
|
262
|
+
|
|
263
|
+
return {
|
|
264
|
+
...diff,
|
|
265
|
+
lines: diff.lines.map(line => ({
|
|
266
|
+
...line,
|
|
267
|
+
lineNum: line.lineNum + delta,
|
|
268
|
+
newLineNum: line.newLineNum === undefined ? undefined : line.newLineNum + delta,
|
|
269
|
+
})),
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
|
|
171
273
|
function applyViewReplace(target, content, start, end, oldText, newText) {
|
|
172
|
-
|
|
274
|
+
if (!Number.isInteger(start) || !Number.isInteger(end) || start < 1 || end < start) {
|
|
275
|
+
throw new Error("edit requires a valid view range in " + target);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const current = sliceLinesRaw(content, start, end - start + 1);
|
|
173
279
|
|
|
174
280
|
if (current !== oldText) {
|
|
175
281
|
const shown = current.length ? current : content;
|
|
@@ -177,18 +283,104 @@ function applyViewReplace(target, content, start, end, oldText, newText) {
|
|
|
177
283
|
throw new Error("edit view is stale in " + target + ": lines " + start + "-" + end + " changed\n" + numberedPreview(shown));
|
|
178
284
|
}
|
|
179
285
|
|
|
180
|
-
const
|
|
181
|
-
const
|
|
286
|
+
const startIndex = lineStartIndex(content, start);
|
|
287
|
+
const endIndex = lineEndIndex(content, startIndex, Math.max(0, end - start + 1));
|
|
288
|
+
const hasSuffix = endIndex < content.length;
|
|
289
|
+
const separator = hasSuffix && content.slice(Math.max(0, endIndex - 2), endIndex) === "\r\n" ? "\r\n" : "\n";
|
|
290
|
+
const eofNewline = content.endsWith("\r\n") ? "\r\n" : "\n";
|
|
291
|
+
let insert = newText;
|
|
182
292
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
if (
|
|
187
|
-
const updated =
|
|
293
|
+
// A view replaces whole source lines. Preserve the separator before following
|
|
294
|
+
// lines, but let an explicit trailing newline change a no-trailing-newline EOF.
|
|
295
|
+
if (hasSuffix && insert !== "" && !insert.endsWith("\n")) insert += separator;
|
|
296
|
+
if (!hasSuffix && content.endsWith("\n") && insert !== "" && !insert.endsWith("\n")) insert += eofNewline;
|
|
297
|
+
const updated = content.slice(0, startIndex) + insert + content.slice(endIndex);
|
|
188
298
|
|
|
189
299
|
return { updated, oldText, newText };
|
|
190
300
|
}
|
|
191
301
|
|
|
302
|
+
const WRITE_DIFF_MAX_READ_BYTES = 512 * 1024;
|
|
303
|
+
const WRITE_APPEND_MAX_READ_BYTES = 64 * 1024 * 1024;
|
|
304
|
+
const QUICK_CHECK_MAX_CHARS = 2 * 1024 * 1024;
|
|
305
|
+
|
|
306
|
+
async function countContentLines(target, signal) {
|
|
307
|
+
const file = await fs.open(target, fs.constants.O_RDONLY | (fs.constants.O_NONBLOCK ?? 0));
|
|
308
|
+
|
|
309
|
+
try {
|
|
310
|
+
const stat = await file.stat();
|
|
311
|
+
|
|
312
|
+
if (!stat.isFile()) return null;
|
|
313
|
+
let newlines = 0;
|
|
314
|
+
let last = -1;
|
|
315
|
+
let total = 0;
|
|
316
|
+
|
|
317
|
+
for await (const chunk of file.createReadStream({ autoClose: false, signal })) {
|
|
318
|
+
for (let i = 0; i < chunk.length; i++) if (chunk[i] === 10) newlines++;
|
|
319
|
+
last = chunk.at(-1);
|
|
320
|
+
total += chunk.length;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
return total === 0 ? 0 : newlines + (last === 10 ? 0 : 1);
|
|
324
|
+
} finally { await file.close(); }
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function contentLineInfo(text, previewLimit = 0) {
|
|
328
|
+
if (text === "") return { count: 0, preview: [], newlines: 0 };
|
|
329
|
+
const preview = [];
|
|
330
|
+
let count = 0;
|
|
331
|
+
let start = 0;
|
|
332
|
+
|
|
333
|
+
while (start <= text.length) {
|
|
334
|
+
const newline = text.indexOf("\n", start);
|
|
335
|
+
const end = newline < 0 ? text.length : newline;
|
|
336
|
+
|
|
337
|
+
if (end === text.length && end === start && text.endsWith("\n")) break;
|
|
338
|
+
if (preview.length < previewLimit) preview.push(text.slice(start, end).replace(/\r$/, ""));
|
|
339
|
+
count++;
|
|
340
|
+
if (newline < 0) break;
|
|
341
|
+
start = newline + 1;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
return { count, preview, newlines: count - Number(!text.endsWith("\n")) };
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function boundedEditDiff(target, original, matches) {
|
|
348
|
+
const lines = [];
|
|
349
|
+
let shift = 0;
|
|
350
|
+
let added = 0;
|
|
351
|
+
let removed = 0;
|
|
352
|
+
|
|
353
|
+
for (const match of matches) {
|
|
354
|
+
const oldInfo = contentLineInfo(match.oldText, 32);
|
|
355
|
+
const newInfo = contentLineInfo(match.newText, 32);
|
|
356
|
+
const start = lineNumberAt(original, match.index);
|
|
357
|
+
const nextStart = start + shift;
|
|
358
|
+
|
|
359
|
+
removed += oldInfo.count;
|
|
360
|
+
added += newInfo.count;
|
|
361
|
+
|
|
362
|
+
for (let i = 0; i < oldInfo.preview.length; i++) lines.push({ type: "remove", lineNum: start + i, newLineNum: nextStart + i, text: oldInfo.preview[i] });
|
|
363
|
+
for (let i = 0; i < newInfo.preview.length; i++) lines.push({ type: "add", lineNum: nextStart + i, newLineNum: nextStart + i, text: newInfo.preview[i] });
|
|
364
|
+
|
|
365
|
+
shift += newInfo.newlines - oldInfo.newlines;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
return { path: target, op: "edit", added, removed, lines };
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function boundedWriteDiff(target, content, removed) {
|
|
372
|
+
const added = contentLineInfo(content, 64);
|
|
373
|
+
|
|
374
|
+
return {
|
|
375
|
+
path: target,
|
|
376
|
+
op: "write",
|
|
377
|
+
added: added.count,
|
|
378
|
+
removed: removed ?? 0,
|
|
379
|
+
displayLineCount: (removed ?? 0) + added.count,
|
|
380
|
+
lines: added.preview.map((text, i) => ({ type: "add", lineNum: i + 1, text })),
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
|
|
192
384
|
function formatDirectoryEntry(name, type, size = 0) {
|
|
193
385
|
const sizeSuffix = size ? `, ${size} bytes` : "";
|
|
194
386
|
|
|
@@ -233,12 +425,25 @@ function createNativeAdapters(getCwd, vfs, config, index, ledger, hooks) {
|
|
|
233
425
|
|
|
234
426
|
if (result.status !== "found") return textResult(JSON.stringify(result), { isSnap: true });
|
|
235
427
|
signal?.throwIfAborted();
|
|
236
|
-
const
|
|
237
|
-
|
|
428
|
+
const target = resolvedPath ?? path.resolve(cwd, result.path);
|
|
429
|
+
let bounded = isString(query) && params.complete !== true;
|
|
430
|
+
|
|
431
|
+
if (bounded) {
|
|
432
|
+
const overlay = vfs.getOverlay(target);
|
|
238
433
|
|
|
239
|
-
|
|
240
|
-
|
|
434
|
+
if (overlay !== undefined) bounded = Buffer.byteLength(overlay, "utf8") > 512 * 1024;
|
|
435
|
+
else try { bounded = (await fs.stat(target)).size > 512 * 1024; } catch { bounded = false; }
|
|
436
|
+
}
|
|
241
437
|
|
|
438
|
+
const opened = await readFile(target, bounded
|
|
439
|
+
? { ...params, about: undefined, offset: Math.max(1, result.line - 4), limit: params.limit ?? 120 }
|
|
440
|
+
: { ...params, about: undefined }, result.line, result.path, bounded ? undefined : query, signal);
|
|
441
|
+
const block = opened.content?.[0];
|
|
442
|
+
|
|
443
|
+
if (block?.type !== "text") throw new Error("source resolution requires a text file; read the image path directly");
|
|
444
|
+
const { firstLine, lastLine, sourceChars, nextOffset, complete, viewComplete } = opened.details;
|
|
445
|
+
|
|
446
|
+
if (lastLine < firstLine) return textResult(JSON.stringify({ status: "incomplete", path: result.path, line: result.line, signature: result.signature ?? "", confidence: result.confidence ?? 0, context: result.context ?? [], message: "offset is beyond the end of " + result.path }), { ...opened.details, isSnap: true });
|
|
242
447
|
const source = { status: "found", path: result.path, line: result.line, lines: [firstLine, lastLine],
|
|
243
448
|
text: block.text.slice(0, sourceChars), complete, nextOffset };
|
|
244
449
|
|
|
@@ -249,8 +454,10 @@ function createNativeAdapters(getCwd, vfs, config, index, ledger, hooks) {
|
|
|
249
454
|
async function readDirectory(dirPath, signal) {
|
|
250
455
|
signal?.throwIfAborted();
|
|
251
456
|
const rows = new Map();
|
|
457
|
+
let truncated = false;
|
|
252
458
|
|
|
253
459
|
for (const file of vfs.getOverlayPaths()) {
|
|
460
|
+
if (rows.size >= MAX_DIRECTORY_ENTRIES) { truncated = true; break; }
|
|
254
461
|
const relative = path.relative(dirPath, file);
|
|
255
462
|
|
|
256
463
|
if (!relative || relative === ".." || relative.startsWith(".." + path.sep) || path.isAbsolute(relative)) continue;
|
|
@@ -267,16 +474,135 @@ function createNativeAdapters(getCwd, vfs, config, index, ledger, hooks) {
|
|
|
267
474
|
entries = [];
|
|
268
475
|
}
|
|
269
476
|
|
|
270
|
-
for (
|
|
477
|
+
for (let i = 0; i < entries.length; i++) {
|
|
478
|
+
if ((i & 127) === 0) signal?.throwIfAborted();
|
|
479
|
+
if (rows.size >= MAX_DIRECTORY_ENTRIES) { truncated = true; break; }
|
|
480
|
+
if (!rows.has(entries[i].name)) rows.set(entries[i].name, await formatLsEntry(dirPath, entries[i]));
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
const values = [...rows.values()];
|
|
484
|
+
const text = values.join("\n") + (truncated ? "\n[directory listing truncated at " + MAX_DIRECTORY_ENTRIES + " entries]" : "");
|
|
485
|
+
|
|
486
|
+
return textResult(text, { path: dirPath, directory: true, count: rows.size, entries: values, outputTruncated: truncated });
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/** Read an explicit line window without materializing the whole file when possible. */
|
|
490
|
+
async function readWindow(targetPath, startLine, lineCount, maxBytes, signal) {
|
|
491
|
+
const overlay = vfs.getOverlay(targetPath);
|
|
492
|
+
|
|
493
|
+
if (overlay !== undefined) {
|
|
494
|
+
const window = sliceLinesRawInfo(overlay, startLine, lineCount);
|
|
495
|
+
const satisfied = lineCount === undefined || lineCount === 0 || window.text === "" || window.count >= lineCount || window.eof;
|
|
496
|
+
|
|
497
|
+
return { text: window.text, satisfied, whole: window.whole };
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
let file;
|
|
501
|
+
|
|
502
|
+
try { file = await fs.open(targetPath, fs.constants.O_RDONLY | (fs.constants.O_NONBLOCK ?? 0)); }
|
|
503
|
+
catch (error) {
|
|
504
|
+
if (error.code === "ENOENT") {
|
|
505
|
+
const missing = new Error("no such file: " + targetPath + " (locate it with read using a directory path or source question)");
|
|
506
|
+
missing.code = "ENOENT";
|
|
507
|
+
throw missing;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
throw error;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
try {
|
|
514
|
+
const stat = await file.stat();
|
|
515
|
+
|
|
516
|
+
if (!stat.isFile()) throw new Error("read requires a regular file: " + targetPath);
|
|
517
|
+
if (lineCount === 0) {
|
|
518
|
+
if (stat.size === 0) vfs.setCache(targetPath, "");
|
|
519
|
+
|
|
520
|
+
return { text: "", satisfied: true, whole: stat.size === 0 };
|
|
521
|
+
}
|
|
271
522
|
|
|
272
|
-
|
|
523
|
+
const wantedLines = lineCount === undefined ? Infinity : startLine + lineCount - 1;
|
|
524
|
+
const scan = Buffer.alloc(64 * 1024);
|
|
525
|
+
const parts = [];
|
|
526
|
+
let position = 0;
|
|
527
|
+
let linesSeen = 0;
|
|
528
|
+
let started = startLine === 1;
|
|
529
|
+
let startByte = started ? 0 : -1;
|
|
530
|
+
let done = false;
|
|
531
|
+
let doneByte = -1;
|
|
532
|
+
let collected = 0;
|
|
533
|
+
|
|
534
|
+
while (position < stat.size && !done && collected <= maxBytes) {
|
|
535
|
+
signal?.throwIfAborted();
|
|
536
|
+
const { bytesRead } = await file.read(scan, 0, scan.length, position);
|
|
537
|
+
|
|
538
|
+
if (bytesRead <= 0) break;
|
|
539
|
+
let begin = 0;
|
|
540
|
+
|
|
541
|
+
if (!started) {
|
|
542
|
+
while (begin < bytesRead && linesSeen < startLine - 1) if (scan[begin++] === 10) linesSeen++;
|
|
543
|
+
if (linesSeen < startLine - 1) {
|
|
544
|
+
position += bytesRead;
|
|
545
|
+
continue;
|
|
546
|
+
}
|
|
547
|
+
started = true;
|
|
548
|
+
startByte = position + begin;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
let end = bytesRead;
|
|
552
|
+
|
|
553
|
+
if (wantedLines !== Infinity) {
|
|
554
|
+
for (let i = begin; i < bytesRead; i++) {
|
|
555
|
+
if (scan[i] !== 10) continue;
|
|
556
|
+
linesSeen++;
|
|
557
|
+
if (linesSeen === wantedLines) {
|
|
558
|
+
end = i + 1;
|
|
559
|
+
done = true;
|
|
560
|
+
doneByte = position + end;
|
|
561
|
+
break;
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
const takeBegin = position === startByte ? begin : Math.max(0, startByte - position);
|
|
567
|
+
const takeEnd = Math.min(end, takeBegin + Math.max(0, maxBytes + 1 - collected));
|
|
568
|
+
|
|
569
|
+
if (takeEnd > takeBegin) {
|
|
570
|
+
parts.push(Buffer.from(scan.subarray(takeBegin, takeEnd)));
|
|
571
|
+
collected += takeEnd - takeBegin;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
position += bytesRead;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
if (!started) {
|
|
578
|
+
if (stat.size === 0) vfs.setCache(targetPath, "");
|
|
579
|
+
|
|
580
|
+
return { text: "", satisfied: true, whole: stat.size === 0 };
|
|
581
|
+
}
|
|
582
|
+
const text = Buffer.concat(parts, collected).toString("utf8");
|
|
583
|
+
const satisfied = (done && startByte + collected >= doneByte) || startByte + collected >= stat.size;
|
|
584
|
+
const whole = startLine === 1 && startByte === 0 && startByte + collected >= stat.size;
|
|
585
|
+
|
|
586
|
+
// Even a bounded window owns a full byte snapshot. The observed stat
|
|
587
|
+
// ties signing to the file version that supplied these window bytes.
|
|
588
|
+
await vfs.recordExpected(targetPath, stat);
|
|
589
|
+
if (whole) vfs.setCache(targetPath, text);
|
|
590
|
+
|
|
591
|
+
return { text, satisfied, whole };
|
|
592
|
+
} finally {
|
|
593
|
+
await file.close();
|
|
594
|
+
}
|
|
273
595
|
}
|
|
274
596
|
|
|
275
597
|
async function readAdapter(params, signal) {
|
|
276
598
|
signal?.throwIfAborted();
|
|
599
|
+
params = normalizeReadWindow(params);
|
|
600
|
+
if (!isObject(params)) throw new Error("read requires an options object");
|
|
277
601
|
const cwd = getCwd();
|
|
278
602
|
const targetParam = params?.path ?? params?.target;
|
|
279
603
|
|
|
604
|
+
if (params?.path !== undefined && params?.target !== undefined && params.path !== params.target) throw new Error("read accepts either path or target, not both");
|
|
605
|
+
|
|
280
606
|
if (Array.isArray(targetParam)) {
|
|
281
607
|
if (targetParam.length > 64) throw new Error("read accepts at most 64 paths per batch");
|
|
282
608
|
|
|
@@ -284,9 +610,13 @@ function createNativeAdapters(getCwd, vfs, config, index, ledger, hooks) {
|
|
|
284
610
|
|
|
285
611
|
const results = await Promise.all(targetParam.map(async p => {
|
|
286
612
|
try {
|
|
287
|
-
const
|
|
613
|
+
const res = await readAdapter({ ...params, path: p, target: undefined }, signal);
|
|
614
|
+
const block = res.content?.[0];
|
|
615
|
+
const item = block?.type === "image" ? block
|
|
616
|
+
: res.details?.directory === true && Array.isArray(res.details.entries) ? res.details.entries
|
|
617
|
+
: block?.text ?? "";
|
|
288
618
|
|
|
289
|
-
return { text:
|
|
619
|
+
return { text: item };
|
|
290
620
|
} catch (error) {
|
|
291
621
|
signal?.throwIfAborted();
|
|
292
622
|
|
|
@@ -348,14 +678,33 @@ function createNativeAdapters(getCwd, vfs, config, index, ledger, hooks) {
|
|
|
348
678
|
async function readSingle(params, cwd, targetParam, signal) {
|
|
349
679
|
params = sessionJsonArgs({ ...params, path: targetParam });
|
|
350
680
|
validateJsonRead(params);
|
|
681
|
+
for (const [key, value] of [["resolve", params.resolve], ["complete", params.complete], ["outline", params.outline], ["evidence", params.evidence]]) {
|
|
682
|
+
if (value !== undefined && typeof value !== "boolean") throw new Error("read " + key + " must be a boolean");
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
if (params.about !== undefined && !isString(params.about)) throw new Error("read about must be a string");
|
|
686
|
+
if (params.query !== undefined && !isString(params.query)) throw new Error("read query must be a string");
|
|
687
|
+
const focusModes = [params.about !== undefined, params.query !== undefined, params.outline === true].filter(Boolean).length;
|
|
688
|
+
|
|
689
|
+
if (focusModes > 1 || (params.outline === true && params.evidence === true)) throw new Error("read accepts only one of about, query, outline, or evidence");
|
|
690
|
+
if (params.resolve === true && params.complete === true) throw new Error("read accepts either resolve or complete, not both");
|
|
691
|
+
if ((focusModes === 1 || params.evidence === true) && params.complete === true) throw new Error("complete:true requires a raw file read, not a source view");
|
|
351
692
|
targetParam = params.path;
|
|
352
693
|
|
|
353
694
|
if (isString(targetParam) && /^(?:agent|artifact):\/\//i.test(targetParam)) {
|
|
695
|
+
if (focusModes || params.evidence === true) throw new Error("session resources do not support about/query/outline/evidence views");
|
|
354
696
|
const target = await resolveSessionResource(targetParam, signal);
|
|
355
697
|
|
|
356
698
|
return params.resolve
|
|
357
699
|
? openSource({status:"found",path:targetParam,line:params.offset ?? 1}, params, signal, target)
|
|
358
|
-
: readFile(target, params, undefined, targetParam);
|
|
700
|
+
: readFile(target, params, undefined, targetParam, undefined, signal);
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
if (params.evidence === true) {
|
|
704
|
+
const query = params.about ?? params.query ?? targetParam;
|
|
705
|
+
const scope = targetParam !== query || looksLikePath(targetParam) ? targetParam : undefined;
|
|
706
|
+
|
|
707
|
+
return adapters.evidence({ ...params, query, path: scope }, signal);
|
|
359
708
|
}
|
|
360
709
|
|
|
361
710
|
if (isString(params?.query)) {
|
|
@@ -364,47 +713,169 @@ function createNativeAdapters(getCwd, vfs, config, index, ledger, hooks) {
|
|
|
364
713
|
return sourceRead(params.query, scope, signal, params);
|
|
365
714
|
}
|
|
366
715
|
|
|
716
|
+
if (params.outline === true) {
|
|
717
|
+
const targetPath = resolveReadPath(cwd, targetParam);
|
|
718
|
+
const text = await vfs.read(targetPath, { maxBytes: 2 * 1024 * 1024 });
|
|
719
|
+
const outline = extractStructuralSurface(text, path.extname(targetPath));
|
|
720
|
+
|
|
721
|
+
return textResult(JSON.stringify(outline, null, 2), { path: targetPath, count: outline.items.length });
|
|
722
|
+
}
|
|
723
|
+
|
|
367
724
|
const existing = await probeExistingPath(cwd, targetParam, vfs);
|
|
368
725
|
|
|
726
|
+
if (params.resolve === true && isString(params.about) && existing && !existing.directory) {
|
|
727
|
+
throw new Error("resolve:true cannot combine with about on a file; use about for a focused outline or resolve for source text");
|
|
728
|
+
}
|
|
729
|
+
|
|
369
730
|
if (existing) {
|
|
370
|
-
if (!existing.directory)
|
|
371
|
-
|
|
372
|
-
|
|
731
|
+
if (!existing.directory) {
|
|
732
|
+
if (isString(params.about) && existing.size > 512 * 1024) {
|
|
733
|
+
if (existing.overlay !== undefined) return focusedOverlayText(existing.path, relativeSlash(cwd, existing.path), existing.overlay, params.about, signal);
|
|
734
|
+
return focusedLargeFile(existing.path, relativeSlash(cwd, existing.path), params.about, signal);
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
return params.resolve
|
|
738
|
+
? openSource({ status: "found", path: relativeSlash(cwd, existing.path), line: params.offset ?? 1 }, params, signal)
|
|
739
|
+
: readFile(existing.path, params, undefined, undefined, undefined, signal);
|
|
740
|
+
}
|
|
373
741
|
|
|
374
742
|
if (params.json !== undefined) throw new Error("JSON read requires a file, not a directory");
|
|
375
743
|
|
|
376
744
|
return isString(params?.about) ? sourceRead(params.about, existing.path, signal, params) : readDirectory(existing.path, signal);
|
|
377
745
|
}
|
|
378
746
|
|
|
379
|
-
if (params.json === undefined && !looksLikePath(targetParam))
|
|
747
|
+
if (params.json === undefined && !looksLikePath(targetParam)) {
|
|
748
|
+
const scope = isString(params.about) ? resolveReadPath(cwd, targetParam) : cwd;
|
|
749
|
+
|
|
750
|
+
return sourceRead(isString(params.about) ? params.about : targetParam, scope, signal, params);
|
|
751
|
+
}
|
|
752
|
+
if (params.resolve === true && params.complete !== true) {
|
|
753
|
+
return textResult(JSON.stringify({ status: "not_found", path: null, line: null, signature: "", confidence: 0, context: [] }), { isSnap: true });
|
|
754
|
+
}
|
|
380
755
|
const targetPath = resolveReadPath(cwd, targetParam);
|
|
381
756
|
|
|
382
|
-
return readFile(targetPath, params);
|
|
757
|
+
return readFile(targetPath, params, undefined, undefined, undefined, signal);
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
/** Focus a huge file through rg instead of materializing it just to fold declarations. */
|
|
761
|
+
async function focusedLargeFile(targetPath, rel, about, signal) {
|
|
762
|
+
const tokens = tokenizeQuery(about).tokens;
|
|
763
|
+
const stems = [...new Set(tokens.map(token => stem(token).slice(0, 128)))];
|
|
764
|
+
|
|
765
|
+
if (tokens.length > 16) throw new Error("about is too broad; use at most 16 keywords");
|
|
766
|
+
if (!stems.length) throw new Error("about needs at least one searchable keyword");
|
|
767
|
+
const budget = Math.max(1, Math.min(config.maxCallResultChars ?? 65536, config.maxReturnChars ?? 32000) - 256);
|
|
768
|
+
const args = ["rg", "--fixed-strings", "--ignore-case", "--line-number", "--before-context", "3", "--after-context", "3"];
|
|
769
|
+
|
|
770
|
+
for (const token of stems) args.push("-e", token);
|
|
771
|
+
args.push("--", targetPath);
|
|
772
|
+
const observed = await fs.stat(targetPath);
|
|
773
|
+
const res = await runCommand(args, { cwd: path.dirname(targetPath), timeoutMs: 15000, maxOutputChars: budget, signal });
|
|
774
|
+
|
|
775
|
+
if (res.exitCode === 0 || res.exitCode === 1) await vfs.recordExpected(targetPath, observed);
|
|
776
|
+
if (res.exitCode === 1) return textResult("// " + rel + " · no matching text\n", { path: targetPath, outputTruncated: false, complete: false });
|
|
777
|
+
if (res.exitCode !== 0) throw new Error(res.stderr.trim() || `rg exited ${res.exitCode}`);
|
|
778
|
+
const marker = res.outputTruncated ? "\n[focused read truncated; narrow about or use read(path, line, count)]" : "";
|
|
779
|
+
|
|
780
|
+
return textResult("// " + rel + " · focused text windows (not a complete file); read(path, line, count) for raw text\n" + res.stdout + marker,
|
|
781
|
+
{ path: targetPath, outputTruncated: res.outputTruncated, complete: false });
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
/** Focus a large staged file without materializing another bounded window of its whole text. */
|
|
785
|
+
function focusedOverlayText(targetPath, rel, overlay, about, signal) {
|
|
786
|
+
const { tokens } = tokenizeQuery(about);
|
|
787
|
+
|
|
788
|
+
if (tokens.length > 16) throw new Error("about is too broad; use at most 16 keywords");
|
|
789
|
+
const stems = tokens.map(token => stem(token).slice(0, 128));
|
|
790
|
+
const hits = [];
|
|
791
|
+
let line = 1;
|
|
792
|
+
let start = 0;
|
|
793
|
+
|
|
794
|
+
while (start <= overlay.length) {
|
|
795
|
+
signal?.throwIfAborted();
|
|
796
|
+
const newline = overlay.indexOf("\n", start);
|
|
797
|
+
const end = newline < 0 ? overlay.length : newline + 1;
|
|
798
|
+
const row = overlay.slice(start, newline < 0 ? end : newline).replace(/\r$/, "");
|
|
799
|
+
|
|
800
|
+
if (stems.some(st => row.toLowerCase().includes(st))) {
|
|
801
|
+
hits.push(line);
|
|
802
|
+
if (hits.length >= 200) break;
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
if (end === overlay.length) break;
|
|
806
|
+
start = end;
|
|
807
|
+
line++;
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
const out = [];
|
|
811
|
+
let cursor = 1;
|
|
812
|
+
let used = 0;
|
|
813
|
+
const budget = Math.max(1, Math.min(config.maxCallResultChars ?? 65536, config.maxReturnChars ?? 32000) - 256);
|
|
814
|
+
let truncated = hits.length >= 200;
|
|
815
|
+
|
|
816
|
+
for (const hit of hits) {
|
|
817
|
+
const from = Math.max(cursor, hit - 3);
|
|
818
|
+
const to = hit + 3;
|
|
819
|
+
const first = lineStartIndex(overlay, from);
|
|
820
|
+
const last = lineTextRange(overlay, Math.min(to, line)).end;
|
|
821
|
+
const body = overlay.slice(first, last);
|
|
822
|
+
|
|
823
|
+
if (used + body.length > budget) { truncated = true; break; }
|
|
824
|
+
if (out.length && from > cursor) out.push("...");
|
|
825
|
+
out.push(`// ${rel}:${from}\n${body}`);
|
|
826
|
+
used += body.length;
|
|
827
|
+
cursor = Math.max(cursor, to + 1);
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
if (!out.length) return textResult("// " + rel + (hits.length
|
|
831
|
+
? " · matching text exceeds view budget; first match at line " + hits[0] + "; use read(path, line, count)\n"
|
|
832
|
+
: " · no matching staged text\n"), { path: targetPath, outputTruncated: truncated, complete: false });
|
|
833
|
+
|
|
834
|
+
const marker = truncated ? "\n[focused read truncated; narrow about or use read(path, line, count)]" : "";
|
|
835
|
+
|
|
836
|
+
return textResult("// " + rel + " · focused staged text windows (not a complete file)\n" + out.join("\n") + marker, { path: targetPath, outputTruncated: truncated, complete: false });
|
|
383
837
|
}
|
|
384
838
|
|
|
385
839
|
/** Plain text, a line window, or (with `about`) a relevance-folded outline of the whole file. */
|
|
386
|
-
async function readFile(targetPath, params, sourceLine, displayPath, query) {
|
|
840
|
+
async function readFile(targetPath, params, sourceLine, displayPath, query, signal) {
|
|
387
841
|
const cwd = getCwd();
|
|
388
842
|
const rel = displayPath ?? relativeSlash(cwd, targetPath);
|
|
389
843
|
|
|
844
|
+
if (isString(params?.about) && tokenizeQuery(params.about).tokens.length > 16) {
|
|
845
|
+
throw new Error("about is too broad; use at most 16 keywords");
|
|
846
|
+
}
|
|
847
|
+
|
|
390
848
|
if (params.json !== undefined) {
|
|
391
849
|
const project = jsonProjector(params.json);
|
|
392
|
-
const text = await vfs.read(targetPath, { maxBytes: MAX_JSON_BYTES });
|
|
850
|
+
const text = await vfs.read(targetPath, { maxBytes: MAX_JSON_BYTES, label: "JSON input" });
|
|
393
851
|
let document;
|
|
394
852
|
|
|
395
853
|
try { document = JSON.parse(text); }
|
|
396
854
|
catch { throw new Error("invalid JSON in " + rel + "; the entire document must parse before projection"); }
|
|
397
855
|
|
|
398
856
|
const many = Array.isArray(params.json);
|
|
399
|
-
|
|
857
|
+
const selectors = many ? params.json.map(selector => String(selector)) : [params.json === true ? "." : String(params.json)];
|
|
858
|
+
const budget = Math.max(1, Math.min(config.maxCallResultChars ?? 65536, config.maxReturnChars ?? 32000) - 256) - (many ? params.json.length + 1 : 0);
|
|
859
|
+
let remaining = budget;
|
|
400
860
|
const parts = [];
|
|
401
861
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
862
|
+
try {
|
|
863
|
+
let index = 0;
|
|
864
|
+
|
|
865
|
+
for (const value of project(document)) {
|
|
866
|
+
const encoded = JSON.stringify(value);
|
|
405
867
|
|
|
406
|
-
|
|
407
|
-
|
|
868
|
+
if (encoded.length > remaining) {
|
|
869
|
+
throw new Error("JSON selection exceeds the read budget for " + rel + " (" + (selectors[index] ?? "selector") + ": " + encoded.length + " chars, " + remaining + " remaining of " + budget + "); select narrower fields or an array slice such as .items[0:10]");
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
remaining -= encoded.length;
|
|
873
|
+
parts.push(encoded);
|
|
874
|
+
index++;
|
|
875
|
+
}
|
|
876
|
+
} catch (error) {
|
|
877
|
+
if (error instanceof Error && error.message.startsWith("JSON selection exceeds the read budget")) throw error;
|
|
878
|
+
throw new Error("JSON selection failed for " + rel + " (" + selectors.join(", ") + "): " + (error instanceof Error ? error.message : String(error)));
|
|
408
879
|
}
|
|
409
880
|
|
|
410
881
|
return textResult(many ? "[" + parts.join(",") + "]" : parts[0], { path: targetPath, json: true, complete: true });
|
|
@@ -413,14 +884,64 @@ function createNativeAdapters(getCwd, vfs, config, index, ledger, hooks) {
|
|
|
413
884
|
const mime = { ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".gif": "image/gif", ".webp": "image/webp", ".bmp": "image/bmp" }[path.extname(targetPath).toLowerCase()];
|
|
414
885
|
|
|
415
886
|
if (mime) {
|
|
416
|
-
if ((await fs.stat(targetPath)).size > 20 * 1024 * 1024) throw new Error("image exceeds 20 MiB; resize it before reading");
|
|
417
887
|
const staged = vfs.getOverlay(targetPath);
|
|
418
|
-
|
|
888
|
+
let bytes;
|
|
889
|
+
|
|
890
|
+
if (staged !== undefined) {
|
|
891
|
+
const size = Buffer.byteLength(staged, "utf8");
|
|
892
|
+
|
|
893
|
+
if (size > 20 * 1024 * 1024) throw new Error("image " + rel + " is " + size + " bytes (" + (size / 1024 / 1024).toFixed(1) + " MiB); the image read limit is 20971520 bytes (20 MiB); resize or select fewer/smaller images");
|
|
894
|
+
bytes = Buffer.from(staged);
|
|
895
|
+
} else {
|
|
896
|
+
let file;
|
|
897
|
+
|
|
898
|
+
try { file = await fs.open(targetPath, fs.constants.O_RDONLY | (fs.constants.O_NONBLOCK ?? 0)); }
|
|
899
|
+
catch (error) {
|
|
900
|
+
if (error.code === "ENOENT") {
|
|
901
|
+
const missing = new Error("no such file: " + targetPath + " (locate it with read using a directory path or source question)");
|
|
902
|
+
missing.code = "ENOENT";
|
|
903
|
+
throw missing;
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
throw error;
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
try {
|
|
910
|
+
const stat = await file.stat();
|
|
911
|
+
|
|
912
|
+
if (!stat.isFile()) throw new Error("image read requires a regular file: " + targetPath);
|
|
913
|
+
if (stat.size > 20 * 1024 * 1024) throw new Error("image " + rel + " is " + stat.size + " bytes (" + (stat.size / 1024 / 1024).toFixed(1) + " MiB); the image read limit is 20971520 bytes (20 MiB); resize or select fewer/smaller images");
|
|
914
|
+
bytes = await file.readFile({ signal });
|
|
915
|
+
await vfs.recordExpected(targetPath, stat);
|
|
916
|
+
} finally { await file.close(); }
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
if (bytes.length > 20 * 1024 * 1024) throw new Error("image " + rel + " is " + bytes.length + " bytes (" + (bytes.length / 1024 / 1024).toFixed(1) + " MiB); the image read limit is 20971520 bytes (20 MiB); resize or select fewer/smaller images");
|
|
419
920
|
|
|
420
921
|
return { content: [{ type: "image", mimeType: mime, data: bytes.toString("base64") }], details: { path: targetPath } };
|
|
421
922
|
}
|
|
422
923
|
|
|
423
|
-
const
|
|
924
|
+
const explicit = isNumber(params?.offset) || isNumber(params?.limit);
|
|
925
|
+
const budget = Math.max(1, Math.min(config.maxCallResultChars ?? 65536, config.maxReturnChars ?? 32000) - (params.resolve ? 1024 : 256));
|
|
926
|
+
let text;
|
|
927
|
+
let windowed = false;
|
|
928
|
+
let windowSatisfied = true;
|
|
929
|
+
let windowWhole = false;
|
|
930
|
+
|
|
931
|
+
const canWindow = params.complete !== true && !isString(params?.about) && !isString(query);
|
|
932
|
+
|
|
933
|
+
if (canWindow) {
|
|
934
|
+
const startLine = isNumber(params?.offset) ? Math.max(1, Math.floor(params.offset)) : 1;
|
|
935
|
+
const lineCount = isNumber(params?.limit) ? Math.max(0, Math.floor(params.limit)) : undefined;
|
|
936
|
+
const window = await readWindow(targetPath, startLine, lineCount, budget * 4 + 1024, signal);
|
|
937
|
+
text = window.text;
|
|
938
|
+
windowed = true;
|
|
939
|
+
windowSatisfied = window.satisfied;
|
|
940
|
+
windowWhole = window.whole === true;
|
|
941
|
+
} else {
|
|
942
|
+
text = await vfs.read(targetPath, { maxBytes: 64 * 1024 * 1024 });
|
|
943
|
+
}
|
|
944
|
+
|
|
424
945
|
index.touch(rel);
|
|
425
946
|
|
|
426
947
|
if (isString(params?.about)) {
|
|
@@ -434,30 +955,31 @@ function createNativeAdapters(getCwd, vfs, config, index, ledger, hooks) {
|
|
|
434
955
|
}
|
|
435
956
|
}
|
|
436
957
|
|
|
437
|
-
const explicit = isNumber(params?.offset) || isNumber(params?.limit);
|
|
438
|
-
const budget = Math.max(1, Math.min(config.maxCallResultChars ?? 65536, config.maxReturnChars ?? 32000) - (params.resolve ? 1024 : 256));
|
|
439
958
|
let offset = params?.offset;
|
|
440
959
|
let limit = params?.limit;
|
|
960
|
+
let viewComplete;
|
|
441
961
|
|
|
442
|
-
if (
|
|
962
|
+
if (params.resolve && isString(query)) {
|
|
443
963
|
const spans = WorkspaceIndex.spansOf(WorkspaceIndex.fromText(targetPath, text));
|
|
444
964
|
const span = pickSpan(spans, { line: sourceLine, name: query });
|
|
445
965
|
|
|
446
966
|
if (span) {
|
|
967
|
+
const spanLines = span.end - span.start + 1;
|
|
447
968
|
offset = span.start;
|
|
448
|
-
limit =
|
|
969
|
+
limit = isNumber(params.limit) ? Math.min(params.limit, spanLines) : spanLines;
|
|
970
|
+
viewComplete = limit >= spanLines;
|
|
449
971
|
}
|
|
450
972
|
}
|
|
451
973
|
|
|
452
974
|
offset ??= sourceLine && text.length > budget ? Math.max(1, sourceLine - 2) : 1;
|
|
453
975
|
const firstLine = isNumber(offset) ? Math.max(1, Math.floor(offset)) : 1;
|
|
454
|
-
const sliced =
|
|
976
|
+
const sliced = windowed ? text : sliceLinesRaw(text, offset, limit);
|
|
455
977
|
|
|
456
978
|
if (params.complete === true && (sliced !== text || sliced.length > budget || (params.resolve && JSON.stringify(sliced).length > budget))) {
|
|
457
979
|
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`);
|
|
458
980
|
}
|
|
459
981
|
|
|
460
|
-
if (sliced.length > budget || (params.resolve && JSON.stringify(sliced).length > budget)) {
|
|
982
|
+
if (sliced.length > budget || (params.resolve && JSON.stringify(sliced).length > budget) || (windowed && !windowSatisfied)) {
|
|
461
983
|
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");
|
|
462
984
|
let cap = budget - 160;
|
|
463
985
|
|
|
@@ -480,13 +1002,17 @@ function createNativeAdapters(getCwd, vfs, config, index, ledger, hooks) {
|
|
|
480
1002
|
if (end < 0) throw new Error(`line ${firstLine} exceeds the read budget; use bash to inspect a bounded substring`);
|
|
481
1003
|
const body = sliced.slice(0, end + 1);
|
|
482
1004
|
const next = firstLine + body.split("\n").length - 1;
|
|
1005
|
+
ledger.recordOrigin(rel, firstLine, sourceLines(body), explicit);
|
|
483
1006
|
|
|
484
|
-
return textResult(body + `\n[read truncated; continue with read({path:${JSON.stringify(rel)}, offset:${next}})]`, { path: targetPath, outputTruncated: true, nextOffset: next, firstLine, lastLine: next - 1, sourceChars: body.length, complete: false });
|
|
1007
|
+
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, viewComplete: false });
|
|
485
1008
|
}
|
|
486
1009
|
|
|
487
|
-
|
|
1010
|
+
const slicedLines = sliced.length <= 512 * 1024 ? sourceLines(sliced) : null;
|
|
1011
|
+
const slicedLineCount = slicedLines?.length ?? contentLineInfo(sliced).count;
|
|
488
1012
|
|
|
489
|
-
|
|
1013
|
+
if (slicedLines) ledger.recordOrigin(rel, firstLine, slicedLines, explicit);
|
|
1014
|
+
|
|
1015
|
+
return textResult(sliced, { path: targetPath, firstLine, lastLine: firstLine + slicedLineCount - 1, sourceChars: sliced.length, complete: windowed ? windowWhole : sliced === text, viewComplete });
|
|
490
1016
|
}
|
|
491
1017
|
|
|
492
1018
|
/**
|
|
@@ -496,17 +1022,27 @@ function createNativeAdapters(getCwd, vfs, config, index, ledger, hooks) {
|
|
|
496
1022
|
*/
|
|
497
1023
|
async function editSummary(cwd, target, original, updated, diff, signal, span) {
|
|
498
1024
|
const rel = relativeSlash(cwd, target);
|
|
499
|
-
const
|
|
1025
|
+
const smallUpdated = updated.length <= 512 * 1024;
|
|
1026
|
+
const newLines = smallUpdated ? updated.split("\n") : null;
|
|
1027
|
+
const lineCount = newLines ? newLines.length : contentLineInfo(updated).count;
|
|
1028
|
+
const lineAt = n => {
|
|
1029
|
+
if (newLines) return newLines[n - 1] ?? "";
|
|
1030
|
+
const { start, end } = lineTextRange(updated, n);
|
|
1031
|
+
|
|
1032
|
+
return updated.slice(start, end).replace(/\r?\n$/, "");
|
|
1033
|
+
};
|
|
500
1034
|
const ranges = [];
|
|
501
1035
|
|
|
502
1036
|
if (span && Number.isInteger(span.start) && Number.isInteger(span.end) && span.start >= 1 && span.end >= span.start) {
|
|
503
|
-
|
|
1037
|
+
const end = Math.min(lineCount, span.end);
|
|
1038
|
+
|
|
1039
|
+
if (span.start <= end) ranges.push({ start: span.start, end });
|
|
504
1040
|
} else {
|
|
505
1041
|
const positions = diff.lines.filter(row => row.type !== "context")
|
|
506
|
-
.map(row => Math.min(
|
|
1042
|
+
.map(row => Math.min(lineCount, row.newLineNum ?? row.lineNum)).sort((a, b) => a - b);
|
|
507
1043
|
|
|
508
1044
|
for (const line of positions) {
|
|
509
|
-
const start = Math.max(1, line - 2), end = Math.min(
|
|
1045
|
+
const start = Math.max(1, line - 2), end = Math.min(lineCount, line + 2);
|
|
510
1046
|
|
|
511
1047
|
if (ranges.length && start <= ranges.at(-1).end + 1) ranges.at(-1).end = Math.max(ranges.at(-1).end, end);
|
|
512
1048
|
else ranges.push({ start, end });
|
|
@@ -518,7 +1054,7 @@ function createNativeAdapters(getCwd, vfs, config, index, ledger, hooks) {
|
|
|
518
1054
|
|
|
519
1055
|
for (const { start, end } of ranges) {
|
|
520
1056
|
const last = Math.min(end, start + perRange - 1);
|
|
521
|
-
const lines =
|
|
1057
|
+
const lines = Array.from({ length: Math.max(0, last - start + 1) }, (_, i) => lineAt(start + i));
|
|
522
1058
|
ledger.recordOrigin(rel, start, lines);
|
|
523
1059
|
blocks.push("edited " + rel + ":" + start + "-" + last + "\n" + lines.map((line, i) => String(start + i).padStart(5) + " " + line).join("\n"));
|
|
524
1060
|
|
|
@@ -526,7 +1062,7 @@ function createNativeAdapters(getCwd, vfs, config, index, ledger, hooks) {
|
|
|
526
1062
|
}
|
|
527
1063
|
|
|
528
1064
|
let out = blocks.join("\n");
|
|
529
|
-
const check = quickCheck(updated, path.extname(target));
|
|
1065
|
+
const check = updated.length <= QUICK_CHECK_MAX_CHARS ? quickCheck(updated, path.extname(target)) : null;
|
|
530
1066
|
|
|
531
1067
|
if (check && !check.ok) out += `\ncheck: ${check.message}`;
|
|
532
1068
|
const refs = await changedDeclarationRefs(cwd, target, original, updated, diff, signal);
|
|
@@ -538,18 +1074,26 @@ function createNativeAdapters(getCwd, vfs, config, index, ledger, hooks) {
|
|
|
538
1074
|
|
|
539
1075
|
async function changedDeclarationRefs(cwd, target, original, updated, diff, signal) {
|
|
540
1076
|
// Diff rows carry the replaced fragments; declarations live on whole file lines.
|
|
541
|
-
|
|
542
|
-
const
|
|
1077
|
+
// Large-file edits skip owner-span mapping: it duplicates source bodies and can exhaust the guest heap.
|
|
1078
|
+
const canMapOwners = original.length <= 512 * 1024 && updated.length <= 512 * 1024;
|
|
1079
|
+
const oldLines = canMapOwners ? original.split("\n") : null;
|
|
1080
|
+
const newLines = canMapOwners ? updated.split("\n") : null;
|
|
1081
|
+
const changedLine = (text, lines, number) => {
|
|
1082
|
+
if (lines) return lines[number - 1] ?? "";
|
|
1083
|
+
const { start, end } = lineTextRange(text, number);
|
|
1084
|
+
|
|
1085
|
+
return text.slice(start, end).replace(/\r?\n$/, "");
|
|
1086
|
+
};
|
|
543
1087
|
const names = new Set();
|
|
544
1088
|
const spans = new Map();
|
|
545
1089
|
|
|
546
1090
|
for (const l of diff.lines) {
|
|
547
1091
|
if (l.type === "context") continue;
|
|
548
1092
|
const number = l.type === "remove" ? l.lineNum : l.newLineNum ?? l.lineNum;
|
|
549
|
-
const name = declaredName((l.type === "remove" ?
|
|
1093
|
+
const name = declaredName(changedLine(l.type === "remove" ? original : updated, l.type === "remove" ? oldLines : newLines, number));
|
|
550
1094
|
|
|
551
1095
|
if (name) names.add(name);
|
|
552
|
-
else {
|
|
1096
|
+
else if (canMapOwners) {
|
|
553
1097
|
if (!spans.has(l.type)) spans.set(l.type, WorkspaceIndex.spansOf(WorkspaceIndex.fromText(target, l.type === "remove" ? original : updated)));
|
|
554
1098
|
const owner = spans.get(l.type).find(span => span.start <= number && number <= span.end);
|
|
555
1099
|
|
|
@@ -584,18 +1128,36 @@ function createNativeAdapters(getCwd, vfs, config, index, ledger, hooks) {
|
|
|
584
1128
|
const SOURCE_REF = /((?:\/|[A-Za-z]:[\\/])?(?:[\w.@-]+[\\/])*[\w.@-]+\.(?:m?[jt]sx?|c[jt]s|py|rs|go|java|kt|rb|php|c|cc|cpp|h|hpp|cs|swift|json|ya?ml|toml))(?::|\()(\d+)/g;
|
|
585
1129
|
|
|
586
1130
|
/** Fresh bounded source window for a diagnostic; no index warmup or stale cached bodies. */
|
|
587
|
-
async function sourceWindow(cwd, commandCwd, file, lineNo) {
|
|
1131
|
+
async function sourceWindow(cwd, commandCwd, file, lineNo, signal) {
|
|
588
1132
|
const candidate = path.resolve(commandCwd, file);
|
|
589
1133
|
let text, rel;
|
|
590
1134
|
|
|
591
1135
|
try {
|
|
592
1136
|
const root = await fs.realpath(cwd);
|
|
1137
|
+
const cwdPrefix = path.resolve(cwd).endsWith(path.sep) ? path.resolve(cwd) : path.resolve(cwd) + path.sep;
|
|
1138
|
+
const rootPrefix = root.endsWith(path.sep) ? root : root + path.sep;
|
|
593
1139
|
|
|
594
|
-
if (!candidate.startsWith(
|
|
1140
|
+
if (!candidate.startsWith(cwdPrefix) && !candidate.startsWith(rootPrefix)) return null;
|
|
595
1141
|
const real = await fs.realpath(candidate);
|
|
1142
|
+
const handle = await fs.open(real, fs.constants.O_RDONLY | (fs.constants.O_NONBLOCK ?? 0));
|
|
596
1143
|
|
|
597
|
-
|
|
598
|
-
|
|
1144
|
+
try {
|
|
1145
|
+
const stat = await handle.stat();
|
|
1146
|
+
|
|
1147
|
+
if (!real.startsWith(rootPrefix) || !stat.isFile() || stat.size > 1024 * 1024) return null;
|
|
1148
|
+
const buffer = Buffer.alloc(Math.min(stat.size, 1024 * 1024));
|
|
1149
|
+
let offset = 0;
|
|
1150
|
+
|
|
1151
|
+
while (offset < buffer.length) {
|
|
1152
|
+
const { bytesRead } = await handle.read(buffer, offset, buffer.length - offset, offset);
|
|
1153
|
+
|
|
1154
|
+
if (bytesRead <= 0) break;
|
|
1155
|
+
offset += bytesRead;
|
|
1156
|
+
signal?.throwIfAborted();
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
text = buffer.subarray(0, offset).toString("utf8");
|
|
1160
|
+
} finally { await handle.close(); }
|
|
599
1161
|
rel = relativeSlash(root, real);
|
|
600
1162
|
} catch { return null; }
|
|
601
1163
|
|
|
@@ -612,7 +1174,7 @@ function createNativeAdapters(getCwd, vfs, config, index, ledger, hooks) {
|
|
|
612
1174
|
}
|
|
613
1175
|
|
|
614
1176
|
/** A failing command names path:line; the model wants those lines next. Attach them (≤4 sites). */
|
|
615
|
-
async function sourceForReferences(cwd, commandCwd, output) {
|
|
1177
|
+
async function sourceForReferences(cwd, commandCwd, output, signal) {
|
|
616
1178
|
const seen = new Set();
|
|
617
1179
|
const blocks = [];
|
|
618
1180
|
|
|
@@ -623,7 +1185,7 @@ function createNativeAdapters(getCwd, vfs, config, index, ledger, hooks) {
|
|
|
623
1185
|
|
|
624
1186
|
if (seen.size >= 4) break;
|
|
625
1187
|
seen.add(key);
|
|
626
|
-
const block = await sourceWindow(cwd, commandCwd, m[1], Number(m[2]));
|
|
1188
|
+
const block = await sourceWindow(cwd, commandCwd, m[1], Number(m[2]), signal);
|
|
627
1189
|
|
|
628
1190
|
if (block) blocks.push(block);
|
|
629
1191
|
}
|
|
@@ -634,7 +1196,7 @@ function createNativeAdapters(getCwd, vfs, config, index, ledger, hooks) {
|
|
|
634
1196
|
function outlineOptions(params, references) {
|
|
635
1197
|
const options = { references };
|
|
636
1198
|
|
|
637
|
-
if (params?.maxChars) options.maxChars = params.maxChars;
|
|
1199
|
+
if (Number.isInteger(params?.maxChars) && params.maxChars > 0) options.maxChars = Math.min(params.maxChars, config.maxCallResultChars ?? 65536);
|
|
638
1200
|
|
|
639
1201
|
return options;
|
|
640
1202
|
}
|
|
@@ -650,7 +1212,10 @@ function createNativeAdapters(getCwd, vfs, config, index, ledger, hooks) {
|
|
|
650
1212
|
|
|
651
1213
|
/** Where else a name appears (declaration line excluded), for outlines and edit results. */
|
|
652
1214
|
async function referenceFinder(cwd, targetPath) {
|
|
653
|
-
|
|
1215
|
+
let files;
|
|
1216
|
+
|
|
1217
|
+
try { files = [...new Set([...await index.files(cwd), ...vfs.getOverlayPaths()])]; }
|
|
1218
|
+
catch { return () => []; }
|
|
654
1219
|
|
|
655
1220
|
if (!index.canScan(files)) return () => [];
|
|
656
1221
|
|
|
@@ -668,7 +1233,7 @@ function createNativeAdapters(getCwd, vfs, config, index, ledger, hooks) {
|
|
|
668
1233
|
|
|
669
1234
|
hooks.summarizeEdit = editSummary;
|
|
670
1235
|
|
|
671
|
-
|
|
1236
|
+
const adapters = {
|
|
672
1237
|
read: readAdapter,
|
|
673
1238
|
async write(params, signal) {
|
|
674
1239
|
const cwd = getCwd();
|
|
@@ -679,24 +1244,51 @@ function createNativeAdapters(getCwd, vfs, config, index, ledger, hooks) {
|
|
|
679
1244
|
if (!isString(params?.content)) throw new Error("write requires string content");
|
|
680
1245
|
|
|
681
1246
|
if (params.append !== undefined && params.append !== true && params.append !== false) throw new Error("write append must be a boolean");
|
|
682
|
-
|
|
1247
|
+
if (params.allowReadArtifacts !== undefined && typeof params.allowReadArtifacts !== "boolean") throw new Error("write allowReadArtifacts must be a boolean");
|
|
1248
|
+
let content = String(params.content);
|
|
683
1249
|
|
|
684
|
-
if (params.allowReadArtifacts !== true && /\[read truncated;|…\[
|
|
1250
|
+
if (params.allowReadArtifacts !== true && /\[read truncated;|…\[[^\]\n]*truncated[^\]\n]*\]…/u.test(content)) {
|
|
685
1251
|
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");
|
|
686
1252
|
}
|
|
687
1253
|
|
|
688
1254
|
let prevText = "";
|
|
1255
|
+
let removedLines;
|
|
1256
|
+
let stat;
|
|
1257
|
+
|
|
1258
|
+
try { stat = await fs.stat(target); }
|
|
1259
|
+
catch (error) { if (error.code !== "ENOENT") throw error; }
|
|
1260
|
+
|
|
1261
|
+
const overlay = vfs.getOverlay(target);
|
|
1262
|
+
const existingBytes = overlay !== undefined ? Buffer.byteLength(overlay, "utf8") : stat?.size;
|
|
1263
|
+
|
|
1264
|
+
if (params.append === true) {
|
|
1265
|
+
if (existingBytes > WRITE_APPEND_MAX_READ_BYTES) throw new Error("append input exceeds " + WRITE_APPEND_MAX_READ_BYTES + " bytes; stream it with bash redirection instead");
|
|
1266
|
+
|
|
1267
|
+
try { prevText = overlay !== undefined ? overlay : await vfs.read(target, { maxBytes: WRITE_APPEND_MAX_READ_BYTES, preserveRead: true }); }
|
|
1268
|
+
catch (error) { if (error.code !== "ENOENT") throw error; }
|
|
1269
|
+
content = prevText + content;
|
|
1270
|
+
} else if (existingBytes !== undefined && existingBytes > WRITE_DIFF_MAX_READ_BYTES) {
|
|
1271
|
+
if (overlay === undefined) {
|
|
1272
|
+
// Keep the CAS baseline without materializing a huge old body just to draw a receipt.
|
|
1273
|
+
await vfs.captureExpected(target);
|
|
1274
|
+
|
|
1275
|
+
try { removedLines = await countContentLines(target, signal); }
|
|
1276
|
+
catch (error) { if (error.code !== "ENOENT") throw error; else removedLines = 0; }
|
|
1277
|
+
} else {
|
|
1278
|
+
removedLines = contentLineInfo(overlay).count;
|
|
1279
|
+
}
|
|
1280
|
+
} else {
|
|
1281
|
+
try { prevText = overlay !== undefined ? overlay : await vfs.read(target, { maxBytes: WRITE_DIFF_MAX_READ_BYTES, preserveRead: true }); }
|
|
1282
|
+
catch (error) { if (error.code !== "ENOENT") throw error; }
|
|
1283
|
+
}
|
|
689
1284
|
|
|
690
|
-
try {
|
|
691
|
-
prevText = await vfs.read(target, { preserveRead: true });
|
|
692
|
-
} catch (error) { if (error.code !== "ENOENT") throw error; }
|
|
693
|
-
|
|
694
|
-
if (params.append === true) content = prevText + content;
|
|
695
1285
|
const { speculative } = await vfs.write(target, content);
|
|
696
1286
|
index.touch(relativeSlash(cwd, target));
|
|
697
|
-
const diff =
|
|
1287
|
+
const diff = removedLines === undefined && content.length <= WRITE_DIFF_MAX_READ_BYTES
|
|
1288
|
+
? buildWriteDiff(target, prevText, content)
|
|
1289
|
+
: boundedWriteDiff(target, content, removedLines ?? contentLineInfo(prevText).count);
|
|
698
1290
|
const tag = speculative ? " (speculative)" : "";
|
|
699
|
-
const check = quickCheck(content, path.extname(target));
|
|
1291
|
+
const check = content.length <= QUICK_CHECK_MAX_CHARS ? quickCheck(content, path.extname(target)) : null;
|
|
700
1292
|
const warning = check && !check.ok ? "\ncheck: " + check.message : "";
|
|
701
1293
|
|
|
702
1294
|
return textResult(`wrote ${target}${tag}${warning}`, { path: target, speculative, diff });
|
|
@@ -707,18 +1299,21 @@ function createNativeAdapters(getCwd, vfs, config, index, ledger, hooks) {
|
|
|
707
1299
|
|
|
708
1300
|
if (signal?.aborted) throw new Error("aborted");
|
|
709
1301
|
|
|
710
|
-
const content = await vfs.read(target);
|
|
1302
|
+
const content = await vfs.read(target, { maxBytes: 64 * 1024 * 1024 });
|
|
711
1303
|
|
|
712
1304
|
if (isNumber(params?.viewStart) && isNumber(params?.viewEnd) && isString(params?.viewText) && isString(params?.newText)) {
|
|
1305
|
+
const viewText = String(params.viewText);
|
|
1306
|
+
const nextText = String(params.newText);
|
|
713
1307
|
const windowNext = isString(params.oldText)
|
|
714
|
-
? applyReplacements(target,
|
|
715
|
-
:
|
|
716
|
-
const { updated } = applyViewReplace(target, content, params.viewStart, params.viewEnd,
|
|
1308
|
+
? applyReplacements(target, viewText, [{ oldText: String(params.oldText), newText: nextText }]).updated
|
|
1309
|
+
: nextText;
|
|
1310
|
+
const { updated } = applyViewReplace(target, content, params.viewStart, params.viewEnd, viewText, windowNext);
|
|
717
1311
|
const { speculative } = await vfs.write(target, updated);
|
|
718
1312
|
index.touch(relativeSlash(cwd, target));
|
|
719
|
-
const diffFrom = isString(params.oldText) ? params.oldText :
|
|
720
|
-
const diffTo = isString(params.oldText) ?
|
|
721
|
-
const
|
|
1313
|
+
const diffFrom = isString(params.oldText) ? String(params.oldText) : viewText;
|
|
1314
|
+
const diffTo = isString(params.oldText) ? nextText : windowNext;
|
|
1315
|
+
const localDiff = buildEditDiff(target, viewText, diffFrom, diffTo);
|
|
1316
|
+
const diff = shiftDiffLines(localDiff, params.viewStart - 1);
|
|
722
1317
|
const inserted = sourceLines(windowNext);
|
|
723
1318
|
const spanEnd = params.viewStart + Math.max(inserted.length, 1) - 1;
|
|
724
1319
|
const summary = await editSummary(cwd, target, content, updated, diff, signal, { start: params.viewStart, end: spanEnd });
|
|
@@ -733,8 +1328,9 @@ function createNativeAdapters(getCwd, vfs, config, index, ledger, hooks) {
|
|
|
733
1328
|
const { speculative } = await vfs.write(target, updated);
|
|
734
1329
|
index.touch(relativeSlash(cwd, target));
|
|
735
1330
|
|
|
736
|
-
const diff =
|
|
737
|
-
|
|
1331
|
+
const diff = content.length > 512 * 1024 || updated.length > 512 * 1024
|
|
1332
|
+
? boundedEditDiff(target, content, matches)
|
|
1333
|
+
: matches.length === 1
|
|
738
1334
|
? buildEditDiff(target, content, matches[0].oldText, matches[0].newText)
|
|
739
1335
|
: buildMultiEditDiff(target, content, matches);
|
|
740
1336
|
|
|
@@ -747,9 +1343,11 @@ function createNativeAdapters(getCwd, vfs, config, index, ledger, hooks) {
|
|
|
747
1343
|
let inputPath = params?.path;
|
|
748
1344
|
|
|
749
1345
|
if (!inputPath && isString(params?.patch)) {
|
|
750
|
-
const
|
|
1346
|
+
for (const match of params.patch.matchAll(/^(?:---|\+\+\+)\s+([^\t\n]+)/gm)) {
|
|
1347
|
+
const candidate = match[1].trim().replace(/^[ab]\//, "");
|
|
751
1348
|
|
|
752
|
-
|
|
1349
|
+
if (candidate !== "/dev/null") { inputPath = candidate; break; }
|
|
1350
|
+
}
|
|
753
1351
|
}
|
|
754
1352
|
|
|
755
1353
|
const target = await resolveWorkspacePath(cwd, inputPath, "apply_patch", false);
|
|
@@ -760,7 +1358,15 @@ function createNativeAdapters(getCwd, vfs, config, index, ledger, hooks) {
|
|
|
760
1358
|
|
|
761
1359
|
if (signal?.aborted) throw new Error("aborted");
|
|
762
1360
|
|
|
763
|
-
|
|
1361
|
+
let original;
|
|
1362
|
+
|
|
1363
|
+
try {
|
|
1364
|
+
original = await vfs.read(target, { maxBytes: 64 * 1024 * 1024, preserveRead: true });
|
|
1365
|
+
} catch (error) {
|
|
1366
|
+
if (error?.code !== "ENOENT") throw error;
|
|
1367
|
+
original = "";
|
|
1368
|
+
}
|
|
1369
|
+
if (original.length > 2 * 1024 * 1024) throw new Error("apply_patch input exceeds 2 MiB; use edit() for targeted replacements");
|
|
764
1370
|
const { resultText, hunkCount } = applyPatchToText(original, params.patch);
|
|
765
1371
|
const { speculative } = await vfs.write(target, resultText);
|
|
766
1372
|
const diff = buildPatchDiff(target, params.patch);
|
|
@@ -805,14 +1411,15 @@ function createNativeAdapters(getCwd, vfs, config, index, ledger, hooks) {
|
|
|
805
1411
|
const cwd = getCwd();
|
|
806
1412
|
|
|
807
1413
|
if (!isString(params?.query) || !params.query.trim()) throw new Error("evidence requires query");
|
|
1414
|
+
if (tokenizeQuery(params.query).tokens.length > 16) throw new Error("evidence query is too broad; use at most 16 keywords");
|
|
808
1415
|
|
|
809
1416
|
if (signal?.aborted) throw new Error("aborted");
|
|
810
1417
|
const searchDir = params?.path ? await resolveWorkspacePath(cwd, params.path, "evidence", true) : cwd;
|
|
811
1418
|
const options = {};
|
|
812
1419
|
|
|
813
|
-
if (Number.isInteger(params?.k) && params.k > 0) options.k = params.k;
|
|
1420
|
+
if (Number.isInteger(params?.k) && params.k > 0) options.k = Math.min(params.k, 20);
|
|
814
1421
|
|
|
815
|
-
if (Number.isInteger(params?.maxChars) && params.maxChars > 0) options.maxChars = params.maxChars;
|
|
1422
|
+
if (Number.isInteger(params?.maxChars) && params.maxChars > 0) options.maxChars = Math.min(params.maxChars, config.maxCallResultChars ?? 65536);
|
|
816
1423
|
const res = await selectEvidence({ query: params.query, root: cwd, searchDir, index, overlayText: (p) => vfs.getOverlay(p), pendingPaths: vfs.getOverlayPaths(), options });
|
|
817
1424
|
|
|
818
1425
|
for (const span of res.spans) ledger.recordOrigin(span.path, span.lines[0], span.text.split("\n"));
|
|
@@ -824,7 +1431,7 @@ function createNativeAdapters(getCwd, vfs, config, index, ledger, hooks) {
|
|
|
824
1431
|
const target = await resolveWorkspacePath(cwd, params?.path, "surface", false);
|
|
825
1432
|
|
|
826
1433
|
if (signal?.aborted) throw new Error("aborted");
|
|
827
|
-
const text = await vfs.read(target);
|
|
1434
|
+
const text = await vfs.read(target, { maxBytes: 2 * 1024 * 1024 });
|
|
828
1435
|
const ext = path.extname(target);
|
|
829
1436
|
const outline = extractStructuralSurface(text, ext);
|
|
830
1437
|
|
|
@@ -834,6 +1441,7 @@ function createNativeAdapters(getCwd, vfs, config, index, ledger, hooks) {
|
|
|
834
1441
|
const cwd = getCwd();
|
|
835
1442
|
const literal = Array.isArray(params?.args) && process.platform !== "win32" && params.args.length === Object.keys(params.args).length && params.args.every(isString);
|
|
836
1443
|
|
|
1444
|
+
if (params?.command !== undefined && !isString(params.command)) throw new Error("bash command must be a string");
|
|
837
1445
|
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");
|
|
838
1446
|
const command = literal ? String(params.command) : unwrapIfFullyQuoted(String(params?.command ?? "").trim());
|
|
839
1447
|
|
|
@@ -856,7 +1464,7 @@ function createNativeAdapters(getCwd, vfs, config, index, ledger, hooks) {
|
|
|
856
1464
|
maxOutputChars: config.maxCallResultChars,
|
|
857
1465
|
});
|
|
858
1466
|
} catch (error) {
|
|
859
|
-
if (!signal?.aborted) error.message += await sourceForReferences(cwd, targetCwd, error.message);
|
|
1467
|
+
if (!signal?.aborted) error.message += await sourceForReferences(cwd, targetCwd, error.message, signal);
|
|
860
1468
|
throw error;
|
|
861
1469
|
} finally {
|
|
862
1470
|
vfs.invalidateCache();
|
|
@@ -868,7 +1476,7 @@ function createNativeAdapters(getCwd, vfs, config, index, ledger, hooks) {
|
|
|
868
1476
|
const { stdout, stderr } = res;
|
|
869
1477
|
let text = stdout && stderr ? stdout + (stdout.endsWith("\n") ? "" : "\n") + stderr : stdout || stderr;
|
|
870
1478
|
|
|
871
|
-
if (res.exitCode !== 0) text += await sourceForReferences(cwd, targetCwd, text);
|
|
1479
|
+
if (res.exitCode !== 0) text += await sourceForReferences(cwd, targetCwd, text, signal);
|
|
872
1480
|
|
|
873
1481
|
return {
|
|
874
1482
|
content: [{ type: "text", text }],
|
|
@@ -882,7 +1490,7 @@ function createNativeAdapters(getCwd, vfs, config, index, ledger, hooks) {
|
|
|
882
1490
|
|
|
883
1491
|
if (!pattern) throw new Error("grep requires pattern");
|
|
884
1492
|
const searchPath = params?.path ? await resolveWorkspacePath(cwd, params.path, "grep", true) : cwd;
|
|
885
|
-
const indexed = await grepIndexed(index, pattern, params, searchPath, cwd);
|
|
1493
|
+
const indexed = await grepIndexed(index, pattern, params, searchPath, cwd, file => vfs.getOverlay(file), vfs.getOverlayPaths());
|
|
886
1494
|
|
|
887
1495
|
if (indexed !== null) return textResult(indexed, { exitCode: indexed ? 0 : 1, via: "index" });
|
|
888
1496
|
// Large tree: real rg keeps its own output format.
|
|
@@ -899,30 +1507,14 @@ function createNativeAdapters(getCwd, vfs, config, index, ledger, hooks) {
|
|
|
899
1507
|
const pattern = String(params?.pattern || "");
|
|
900
1508
|
|
|
901
1509
|
if (!pattern) throw new Error("glob requires pattern");
|
|
902
|
-
const fuzzy = await fuzzyFind(index, cwd, cwd, pattern);
|
|
1510
|
+
const fuzzy = await fuzzyFind(index, cwd, cwd, pattern, 20, vfs.getOverlayPaths());
|
|
903
1511
|
|
|
904
1512
|
if (fuzzy !== null) return textResult(fuzzy, { via: "fuzzy" });
|
|
905
|
-
const indexed = await listIndexed(index, cwd, cwd, pattern);
|
|
1513
|
+
const indexed = await listIndexed(index, cwd, cwd, pattern, vfs.getOverlayPaths());
|
|
906
1514
|
|
|
907
1515
|
if (indexed !== null) return textResult(indexed, { via: "index" });
|
|
908
1516
|
|
|
909
|
-
|
|
910
|
-
() => null,
|
|
911
|
-
);
|
|
912
|
-
|
|
913
|
-
if (rg && (rg.exitCode === 0 || rg.exitCode === 1)) {
|
|
914
|
-
return textResult(rg.stdout, { via: "rg" });
|
|
915
|
-
}
|
|
916
|
-
|
|
917
|
-
const findPattern = pattern.startsWith("./") ? pattern : `./${pattern}`;
|
|
918
|
-
|
|
919
|
-
const fallback = await runCommand(["find", ".", "-type", "f", "-path", findPattern], {
|
|
920
|
-
cwd,
|
|
921
|
-
timeoutMs: 30_000,
|
|
922
|
-
signal,
|
|
923
|
-
});
|
|
924
|
-
|
|
925
|
-
return textResult(fallback.stdout, { via: "find" });
|
|
1517
|
+
return listWithTools(cwd, pattern, cwd, signal, vfs.getOverlayPaths());
|
|
926
1518
|
},
|
|
927
1519
|
async find(params, signal) {
|
|
928
1520
|
const cwd = getCwd();
|
|
@@ -931,22 +1523,38 @@ function createNativeAdapters(getCwd, vfs, config, index, ledger, hooks) {
|
|
|
931
1523
|
|
|
932
1524
|
if (signal?.aborted) throw new Error("aborted");
|
|
933
1525
|
const globPattern = pattern ? String(pattern) : null;
|
|
934
|
-
const fuzzy = await fuzzyFind(index, searchDir, cwd, globPattern);
|
|
1526
|
+
const fuzzy = await fuzzyFind(index, searchDir, cwd, globPattern, 20, vfs.getOverlayPaths());
|
|
935
1527
|
|
|
936
1528
|
if (fuzzy !== null) return textResult(fuzzy, { via: "fuzzy" });
|
|
937
|
-
const indexed = await listIndexed(index, searchDir, cwd, globPattern);
|
|
1529
|
+
const indexed = await listIndexed(index, searchDir, cwd, globPattern, vfs.getOverlayPaths());
|
|
938
1530
|
|
|
939
1531
|
if (indexed !== null) return textResult(indexed, { via: "index" });
|
|
940
1532
|
|
|
941
|
-
return listWithTools(searchDir, globPattern, cwd, signal);
|
|
1533
|
+
return listWithTools(searchDir, globPattern, cwd, signal, vfs.getOverlayPaths());
|
|
942
1534
|
},
|
|
943
1535
|
async ls(params, signal) {
|
|
944
1536
|
const cwd = getCwd();
|
|
945
1537
|
const dirPath = params?.path ? await resolveWorkspacePath(cwd, params.path, "ls", true) : cwd;
|
|
1538
|
+
const pending = vfs.getOverlay(dirPath);
|
|
1539
|
+
|
|
1540
|
+
if (pending !== undefined) {
|
|
1541
|
+
const entry = formatDirectoryEntry(path.basename(dirPath), "file", Buffer.byteLength(pending, "utf8"));
|
|
1542
|
+
|
|
1543
|
+
return textResult(entry, { path: dirPath, directory: false, count: 1, entries: [entry] });
|
|
1544
|
+
}
|
|
1545
|
+
const stat = await fs.stat(dirPath).catch(() => null);
|
|
1546
|
+
|
|
1547
|
+
if (stat?.isFile()) {
|
|
1548
|
+
const entry = formatDirectoryEntry(path.basename(dirPath), "file", stat.size);
|
|
1549
|
+
|
|
1550
|
+
return textResult(entry, { path: dirPath, directory: false, count: 1, entries: [entry] });
|
|
1551
|
+
}
|
|
946
1552
|
|
|
947
1553
|
return readDirectory(dirPath, signal);
|
|
948
1554
|
},
|
|
949
1555
|
};
|
|
1556
|
+
|
|
1557
|
+
return adapters;
|
|
950
1558
|
}
|
|
951
1559
|
|
|
952
1560
|
/**
|
|
@@ -1036,13 +1644,22 @@ export function createHostBridge({ pi, config, getCwd, registry, ledger: runLedg
|
|
|
1036
1644
|
const sessionId = ctx?.sessionManager?.getSessionId?.();
|
|
1037
1645
|
boundSessionId = sessionId;
|
|
1038
1646
|
const registry = pi?.pi?.AgentRegistry?.global?.();
|
|
1039
|
-
|
|
1040
|
-
|
|
1647
|
+
let sessions = [];
|
|
1648
|
+
|
|
1649
|
+
try { sessions = registry?.list?.() ?? []; } catch {}
|
|
1650
|
+
if (!Array.isArray(sessions)) sessions = [];
|
|
1651
|
+
hostSession = sessionId
|
|
1652
|
+
? sessions.map(ref => ref.session).find(session => !session?.isDisposed && session?.sessionManager?.getSessionId?.() === sessionId) ?? null
|
|
1041
1653
|
: null;
|
|
1042
1654
|
activeSignal = signal;
|
|
1043
1655
|
vfs.signal = signal;
|
|
1044
1656
|
}
|
|
1045
1657
|
|
|
1658
|
+
function evalToolNames() {
|
|
1659
|
+
try { return hostSession?.getEvalBridgeToolNames?.() ?? []; }
|
|
1660
|
+
catch { return []; }
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1046
1663
|
function hostTool(name) {
|
|
1047
1664
|
if (!hostSession) return undefined;
|
|
1048
1665
|
const metadata = definitions.get(name);
|
|
@@ -1050,13 +1667,14 @@ export function createHostBridge({ pi, config, getCwd, registry, ledger: runLedg
|
|
|
1050
1667
|
// Keep Supernova's transactional adapters for ordinary built-ins. Respect overrides.
|
|
1051
1668
|
if (Object.hasOwn(natives, name) && metadata?.sourceInfo?.source === "builtin") return undefined;
|
|
1052
1669
|
|
|
1053
|
-
return hostSession.getToolForEvalBridge?.(name);
|
|
1670
|
+
try { return hostSession.getToolForEvalBridge?.(name); }
|
|
1671
|
+
catch { return undefined; }
|
|
1054
1672
|
}
|
|
1055
1673
|
|
|
1056
1674
|
function isCallable(name) {
|
|
1057
1675
|
if (name === "supernova" || (config.excludeTools ?? []).includes(name)) return false;
|
|
1058
1676
|
|
|
1059
|
-
if (hostSession && (hostSession.isDisposed || hostSession.sessionManager
|
|
1677
|
+
if (hostSession && (hostSession.isDisposed || hostSession.sessionManager?.getSessionId?.() !== boundSessionId)) return false;
|
|
1060
1678
|
|
|
1061
1679
|
// An internal adapter belongs to Supernova, not the host's visible tool list.
|
|
1062
1680
|
const nativeOwned = Object.hasOwn(natives, name) && !executors.has(name)
|
|
@@ -1065,18 +1683,27 @@ export function createHostBridge({ pi, config, getCwd, registry, ledger: runLedg
|
|
|
1065
1683
|
if (nativeOwned) return true;
|
|
1066
1684
|
|
|
1067
1685
|
if (hostSession) {
|
|
1068
|
-
|
|
1686
|
+
const evalNames = evalToolNames();
|
|
1687
|
+
|
|
1688
|
+
if (!evalNames.includes(name) && definitions.has(name)) return false;
|
|
1069
1689
|
|
|
1070
1690
|
return !!hostTool(name) || (Object.hasOwn(natives, name) && (!definitions.has(name) || definitions.get(name).sourceInfo?.source === "builtin"));
|
|
1071
1691
|
}
|
|
1072
1692
|
|
|
1073
|
-
|
|
1693
|
+
let activeTools;
|
|
1694
|
+
|
|
1695
|
+
try { activeTools = isFunction(pi?.getActiveTools) ? pi.getActiveTools() : undefined; } catch {}
|
|
1696
|
+
|
|
1697
|
+
if (definitions.has(name) && Array.isArray(activeTools) && !activeTools.includes(name)) return false;
|
|
1074
1698
|
|
|
1075
1699
|
return executors.has(name) || Object.hasOwn(natives, name);
|
|
1076
1700
|
}
|
|
1077
1701
|
|
|
1078
1702
|
function refreshTools() {
|
|
1079
|
-
|
|
1703
|
+
let listed = [];
|
|
1704
|
+
|
|
1705
|
+
try { listed = pi?.getAllTools?.() ?? []; } catch {}
|
|
1706
|
+
const tools = Array.isArray(listed) ? listed : [];
|
|
1080
1707
|
|
|
1081
1708
|
for (const tool of tools) {
|
|
1082
1709
|
if (!isString(tool?.name)) continue;
|
|
@@ -1163,14 +1790,33 @@ export function createHostBridge({ pi, config, getCwd, registry, ledger: runLedg
|
|
|
1163
1790
|
if (name !== "write" || !isString(args?.path) || !isString(args?.content)) return undefined;
|
|
1164
1791
|
const target = await resolveWorkspacePath(getCwd(), args.path, "write", false);
|
|
1165
1792
|
let previous = "";
|
|
1793
|
+
let removedLines;
|
|
1794
|
+
let stat;
|
|
1166
1795
|
|
|
1167
|
-
try {
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1796
|
+
try { stat = await fs.stat(target); }
|
|
1797
|
+
catch (error) { if (error.code !== "ENOENT") throw error; }
|
|
1798
|
+
|
|
1799
|
+
const overlay = vfs.getOverlay(target);
|
|
1800
|
+
const existingBytes = overlay !== undefined ? Buffer.byteLength(overlay, "utf8") : stat?.size;
|
|
1801
|
+
|
|
1802
|
+
if (existingBytes !== undefined && existingBytes > WRITE_DIFF_MAX_READ_BYTES) {
|
|
1803
|
+
if (overlay === undefined) {
|
|
1804
|
+
await vfs.captureExpected(target);
|
|
1805
|
+
|
|
1806
|
+
try { removedLines = await countContentLines(target, activeSignal); }
|
|
1807
|
+
catch (error) { if (error.code !== "ENOENT") throw error; else removedLines = 0; }
|
|
1808
|
+
} else {
|
|
1809
|
+
removedLines = contentLineInfo(overlay).count;
|
|
1810
|
+
}
|
|
1811
|
+
} else {
|
|
1812
|
+
try {
|
|
1813
|
+
previous = overlay !== undefined ? overlay : await vfs.read(target, { maxBytes: WRITE_DIFF_MAX_READ_BYTES, preserveRead: true });
|
|
1814
|
+
} catch (error) {
|
|
1815
|
+
if (error?.code !== "ENOENT") throw error;
|
|
1816
|
+
}
|
|
1171
1817
|
}
|
|
1172
1818
|
|
|
1173
|
-
return buildWriteDiff(target, previous, args.content);
|
|
1819
|
+
return removedLines === undefined ? buildWriteDiff(target, previous, args.content) : boundedWriteDiff(target, args.content, removedLines);
|
|
1174
1820
|
}
|
|
1175
1821
|
|
|
1176
1822
|
function completeRecord(record, res, fallbackDiff) {
|
|
@@ -1181,6 +1827,24 @@ export function createHostBridge({ pi, config, getCwd, registry, ledger: runLedg
|
|
|
1181
1827
|
notifyCall(record);
|
|
1182
1828
|
}
|
|
1183
1829
|
|
|
1830
|
+
function traceArgs(args) {
|
|
1831
|
+
if (!isObject(args)) return {};
|
|
1832
|
+
const out = {};
|
|
1833
|
+
|
|
1834
|
+
for (const key of ["path", "target", "query", "pattern", "command", "cwd", "glob", "action", "op"]) {
|
|
1835
|
+
const value = args[key];
|
|
1836
|
+
|
|
1837
|
+
if (isString(value)) out[key] = truncateChars(value, 240, "trace").text;
|
|
1838
|
+
else if (Array.isArray(value)) out[key] = value.slice(0, 128).map(item => isString(item) ? truncateChars(item, 240, "trace").text : typeof item);
|
|
1839
|
+
}
|
|
1840
|
+
|
|
1841
|
+
if (isString(args.content)) out.content = args.content.length + " chars";
|
|
1842
|
+
if (Array.isArray(args.edits)) out.edits = args.edits.length + " edits";
|
|
1843
|
+
if (Array.isArray(args.args)) out.args = args.args.length + " argv";
|
|
1844
|
+
|
|
1845
|
+
return out;
|
|
1846
|
+
}
|
|
1847
|
+
|
|
1184
1848
|
async function invokeRaw(name, args) {
|
|
1185
1849
|
checkCallBudget(name);
|
|
1186
1850
|
const callId = ++sharedRegistry.callSeq;
|
|
@@ -1189,7 +1853,7 @@ export function createHostBridge({ pi, config, getCwd, registry, ledger: runLedg
|
|
|
1189
1853
|
if (!isCallable(name)) throw new Error(unknownToolMessage(name, [...definitions.keys(), ...Object.keys(natives)].filter(isCallable)));
|
|
1190
1854
|
|
|
1191
1855
|
const command = { apply_patch: "edit", surface: "read", evidence: "read", snap: "read" }[name] ?? name;
|
|
1192
|
-
const record = { name: command, adapter: name, args: args
|
|
1856
|
+
const record = { name: command, adapter: name, args: traceArgs(args), time: Date.now() };
|
|
1193
1857
|
trace.push(record);
|
|
1194
1858
|
notifyCall(record);
|
|
1195
1859
|
|
|
@@ -1197,7 +1861,8 @@ export function createHostBridge({ pi, config, getCwd, registry, ledger: runLedg
|
|
|
1197
1861
|
const delegated = hostTool(name);
|
|
1198
1862
|
const exec = delegated ? delegated.execute.bind(delegated) : hostSession ? undefined : executors.get(name);
|
|
1199
1863
|
const argvOwned = name === "bash" && Array.isArray(args?.args) && process.platform !== "win32" && args.args.length === Object.keys(args.args).length && args.args.every(isString);
|
|
1200
|
-
|
|
1864
|
+
// Explicit overrides own all reads. Options, even false-valued ones,
|
|
1865
|
+
// must not silently bypass the host executor.
|
|
1201
1866
|
if (exec && !argvOwned) {
|
|
1202
1867
|
if (name === "read" && (args?.json !== undefined || /^(agent|artifact):\/\/.*\?/i.test(String(args?.path)))) throw new Error("JSON projection requires the Supernova-owned read adapter, not an external override");
|
|
1203
1868
|
|
|
@@ -1213,7 +1878,7 @@ export function createHostBridge({ pi, config, getCwd, registry, ledger: runLedg
|
|
|
1213
1878
|
|
|
1214
1879
|
try {
|
|
1215
1880
|
const res = await exec(`supernova:${name}:${callId}`, args || {}, activeSignal, undefined, delegated
|
|
1216
|
-
? { ...activeCtx, settings: hostSession.settings, toolNames:
|
|
1881
|
+
? { ...activeCtx, settings: hostSession.settings, toolNames: evalToolNames(), autoApprove: false }
|
|
1217
1882
|
: activeCtx);
|
|
1218
1883
|
|
|
1219
1884
|
completeRecord(record, res, fallbackDiff);
|
|
@@ -1227,7 +1892,7 @@ export function createHostBridge({ pi, config, getCwd, registry, ledger: runLedg
|
|
|
1227
1892
|
const native = Object.hasOwn(natives, name) ? natives[name] : undefined;
|
|
1228
1893
|
|
|
1229
1894
|
if (native) {
|
|
1230
|
-
const res = await native(args || {}, activeSignal);
|
|
1895
|
+
const res = await native(argvOwned ? { ...(args || {}), args: args.args.map(String) } : args || {}, activeSignal);
|
|
1231
1896
|
completeRecord(record, res);
|
|
1232
1897
|
|
|
1233
1898
|
return res;
|
|
@@ -1253,7 +1918,7 @@ export function createHostBridge({ pi, config, getCwd, registry, ledger: runLedg
|
|
|
1253
1918
|
? res.content.filter(part => part?.type === "text" && isString(part.text)).map(part => part.text).join("\n")
|
|
1254
1919
|
: undefined;
|
|
1255
1920
|
|
|
1256
|
-
if (text) record.resultText = text;
|
|
1921
|
+
if (text) record.resultText = truncateChars(text, 4096, "trace").text;
|
|
1257
1922
|
}
|
|
1258
1923
|
|
|
1259
1924
|
async function call(name, args) {
|