pi-supernova 0.4.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 +86 -24
- package/docs/CHANGELOG.md +70 -0
- package/docs/TOKEN_COSTS.md +64 -28
- package/index.js +64 -25
- package/package.json +2 -2
- package/src/bridge/catalog.js +14 -10
- package/src/bridge/host-bridge.js +919 -158
- 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 +190 -19
- package/src/context/search.js +70 -17
- package/src/context/snap.js +128 -40
- package/src/context/spans.js +39 -0
- package/src/context/surface.js +23 -15
- package/src/fs/check.js +10 -2
- package/src/fs/json-read.js +5 -1
- package/src/fs/patch.js +4 -2
- package/src/fs/vfs.js +133 -33
- package/src/fs/workspace.js +34 -4
- package/src/output/bottleneck.js +53 -15
- package/src/output/format.js +63 -29
- package/src/runtime/guest-worker.js +172 -61
- 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 +15 -22
- package/src/runtime/runtime.js +17 -8
- package/src/shared/decode.js +26 -3
- package/src/ui/omp-frame.js +11 -4
- package/src/ui/render.js +2 -2
|
@@ -3,12 +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 {
|
|
6
|
+
import { truncateChars } from "../output/format.js";
|
|
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";
|
|
11
|
+
import { pickSpan } from "../context/spans.js";
|
|
10
12
|
import { buildEditDiff, buildMultiEditDiff, buildPatchDiff, buildWriteDiff } from "../fs/diff.js";
|
|
11
|
-
import { executeSnap } from "../context/snap.js";
|
|
13
|
+
import { executeSnap, tokenizeQuery, stem } from "../context/snap.js";
|
|
12
14
|
import { selectEvidence } from "../context/evidence.js";
|
|
13
15
|
import { WorkspaceIndex } from "../context/repo-index.js";
|
|
14
16
|
import { outlineFile } from "../context/outline.js";
|
|
@@ -18,7 +20,7 @@ import { declaredName } from "../context/repo-index.js";
|
|
|
18
20
|
import { CausalVfs } from "../fs/vfs.js";
|
|
19
21
|
import { MAX_JSON_BYTES, jsonProjector, sessionJsonArgs, validateJsonRead } from "../fs/json-read.js";
|
|
20
22
|
import { applyPatchToText } from "../fs/patch.js";
|
|
21
|
-
import { resolveWorkspacePath, runCommand, clearPathCache, relativeSlash } from "../fs/workspace.js";
|
|
23
|
+
import { resolveWorkspacePath, runCommand, clearPathCache, relativeSlash, assertFilesystemPath } from "../fs/workspace.js";
|
|
22
24
|
import { fuzzyFind, grepIndexed, listIndexed, listWithTools, rgGrepArgs, referencesForNames } from "../context/search.js";
|
|
23
25
|
|
|
24
26
|
function textResult(text, details) {
|
|
@@ -28,6 +30,20 @@ function textResult(text, details) {
|
|
|
28
30
|
};
|
|
29
31
|
}
|
|
30
32
|
|
|
33
|
+
function resultDiff(response) {
|
|
34
|
+
let details = response?.details;
|
|
35
|
+
|
|
36
|
+
if (isString(details)) {
|
|
37
|
+
try {
|
|
38
|
+
details = JSON.parse(details);
|
|
39
|
+
} catch {
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return isObject(details) ? details.diff : undefined;
|
|
45
|
+
}
|
|
46
|
+
|
|
31
47
|
/** Unwrap a single matching quote pair around the whole string (`'git status'`). */
|
|
32
48
|
function unwrapIfFullyQuoted(s) {
|
|
33
49
|
if (s.length < 2) return s;
|
|
@@ -43,28 +59,63 @@ function unwrapIfFullyQuoted(s) {
|
|
|
43
59
|
return inner;
|
|
44
60
|
}
|
|
45
61
|
|
|
46
|
-
function
|
|
47
|
-
|
|
48
|
-
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
|
+
|
|
49
70
|
const startIndex = (isNumber(offset) ? Math.max(1, Math.floor(offset)) : 1) - 1;
|
|
50
|
-
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"));
|
|
51
84
|
|
|
52
|
-
|
|
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 };
|
|
53
88
|
}
|
|
54
89
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
);
|
|
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");
|
|
109
|
+
|
|
110
|
+
if (offset !== undefined) normalized.offset = Math.max(1, offset);
|
|
111
|
+
if (limit !== undefined) normalized.limit = Math.max(0, limit);
|
|
112
|
+
|
|
113
|
+
return normalized;
|
|
63
114
|
}
|
|
64
115
|
|
|
65
116
|
function resolveReadPath(cwd, target) {
|
|
66
117
|
if (!isString(target) || !target.trim()) throw new Error("read requires path");
|
|
67
|
-
const input = target
|
|
118
|
+
const input = assertFilesystemPath(target, "read");
|
|
68
119
|
|
|
69
120
|
return path.resolve(cwd, input === "~" ? homedir() : input.startsWith("~/") ? path.join(homedir(), input.slice(2)) : input);
|
|
70
121
|
}
|
|
@@ -72,12 +123,16 @@ function resolveReadPath(cwd, target) {
|
|
|
72
123
|
async function probeExistingPath(cwd, targetParam, vfs) {
|
|
73
124
|
const targetPath = resolveReadPath(cwd, targetParam);
|
|
74
125
|
|
|
75
|
-
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
|
+
}
|
|
76
131
|
|
|
77
132
|
try {
|
|
78
133
|
const st = await fs.stat(targetPath);
|
|
79
134
|
|
|
80
|
-
return { path: targetPath, directory: st.isDirectory() };
|
|
135
|
+
return { path: targetPath, directory: st.isDirectory(), size: st.size };
|
|
81
136
|
} catch (err) {
|
|
82
137
|
if (err?.code !== "ENOENT" && err?.code !== "ENOTDIR") throw err;
|
|
83
138
|
|
|
@@ -87,6 +142,40 @@ async function probeExistingPath(cwd, targetParam, vfs) {
|
|
|
87
142
|
}
|
|
88
143
|
}
|
|
89
144
|
|
|
145
|
+
const EDIT_PREVIEW_LINES = 16;
|
|
146
|
+
|
|
147
|
+
const MAX_DIRECTORY_ENTRIES = 10000;
|
|
148
|
+
|
|
149
|
+
function sourceLines(content) {
|
|
150
|
+
const raw = content.split("\n");
|
|
151
|
+
|
|
152
|
+
if (raw.at(-1) === "") raw.pop();
|
|
153
|
+
|
|
154
|
+
return raw;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function lineNumberAt(content, index) {
|
|
158
|
+
let line = 1;
|
|
159
|
+
|
|
160
|
+
for (let i = 0; i < index; i++) if (content.charCodeAt(i) === 10) line++;
|
|
161
|
+
|
|
162
|
+
return line;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function formatNumberedLine(n, text) {
|
|
166
|
+
return String(n).padStart(5) + " " + text;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function numberedPreview(content, cap = EDIT_PREVIEW_LINES) {
|
|
170
|
+
const { count, preview } = contentLineInfo(content, cap);
|
|
171
|
+
|
|
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";
|
|
175
|
+
|
|
176
|
+
return body + "\n" + suffix;
|
|
177
|
+
}
|
|
178
|
+
|
|
90
179
|
function applyReplacements(target, content, requestedEdits) {
|
|
91
180
|
if (requestedEdits.length === 0) throw new Error("edit requires at least one replacement");
|
|
92
181
|
|
|
@@ -96,17 +185,28 @@ function applyReplacements(target, content, requestedEdits) {
|
|
|
96
185
|
}
|
|
97
186
|
|
|
98
187
|
if (!isString(replacement?.newText)) throw new Error("edit requires newText");
|
|
99
|
-
const
|
|
188
|
+
const oldText = String(replacement.oldText);
|
|
189
|
+
const newText = String(replacement.newText);
|
|
190
|
+
const index = content.indexOf(oldText);
|
|
100
191
|
|
|
101
192
|
if (index < 0) {
|
|
102
|
-
throw new Error(
|
|
193
|
+
throw new Error("edit target not found in " + target + ": oldText must match the file byte-for-byte\n" + numberedPreview(content));
|
|
103
194
|
}
|
|
195
|
+
const second = content.indexOf(oldText, index + 1);
|
|
196
|
+
|
|
197
|
+
if (second >= 0) {
|
|
198
|
+
const a = lineNumberAt(content, index);
|
|
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
|
+
};
|
|
104
205
|
|
|
105
|
-
|
|
106
|
-
throw new Error(`edit target is not unique in ${target}: include more surrounding lines in oldText, or pass edits:[{oldText,newText},…]`);
|
|
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)));
|
|
107
207
|
}
|
|
108
208
|
|
|
109
|
-
return { ...replacement, index, end: index +
|
|
209
|
+
return { ...replacement, oldText, newText, index, end: index + oldText.length };
|
|
110
210
|
});
|
|
111
211
|
|
|
112
212
|
matches.sort((a, b) => a.index - b.index);
|
|
@@ -125,6 +225,162 @@ function applyReplacements(target, content, requestedEdits) {
|
|
|
125
225
|
return { updated, matches };
|
|
126
226
|
}
|
|
127
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
|
+
|
|
273
|
+
function applyViewReplace(target, content, start, end, oldText, newText) {
|
|
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);
|
|
279
|
+
|
|
280
|
+
if (current !== oldText) {
|
|
281
|
+
const shown = current.length ? current : content;
|
|
282
|
+
|
|
283
|
+
throw new Error("edit view is stale in " + target + ": lines " + start + "-" + end + " changed\n" + numberedPreview(shown));
|
|
284
|
+
}
|
|
285
|
+
|
|
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;
|
|
292
|
+
|
|
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);
|
|
298
|
+
|
|
299
|
+
return { updated, oldText, newText };
|
|
300
|
+
}
|
|
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
|
+
|
|
128
384
|
function formatDirectoryEntry(name, type, size = 0) {
|
|
129
385
|
const sizeSuffix = size ? `, ${size} bytes` : "";
|
|
130
386
|
|
|
@@ -151,6 +407,7 @@ function createNativeAdapters(getCwd, vfs, config, index, ledger, hooks) {
|
|
|
151
407
|
const reads = createNativeScheduler();
|
|
152
408
|
|
|
153
409
|
async function sourceRead(query, searchDir, signal, params = {}) {
|
|
410
|
+
params = { ...params, resolve: params.resolve !== false };
|
|
154
411
|
const cwd = getCwd();
|
|
155
412
|
|
|
156
413
|
const includeHidden = path.relative(cwd, searchDir).split(path.sep)
|
|
@@ -160,20 +417,33 @@ function createNativeAdapters(getCwd, vfs, config, index, ledger, hooks) {
|
|
|
160
417
|
pathContext: { frecency: index.frecency, currentFile: index.lastTouched },
|
|
161
418
|
overlayText: p => vfs.getOverlay(p), pendingPaths: vfs.getOverlayPaths(), signal });
|
|
162
419
|
|
|
163
|
-
return openSource(result, params, signal);
|
|
420
|
+
return openSource(result, params, signal, undefined, query);
|
|
164
421
|
}
|
|
165
422
|
|
|
166
|
-
async function openSource(result, params, signal, resolvedPath) {
|
|
423
|
+
async function openSource(result, params, signal, resolvedPath, query) {
|
|
167
424
|
const cwd = getCwd();
|
|
168
425
|
|
|
169
426
|
if (result.status !== "found") return textResult(JSON.stringify(result), { isSnap: true });
|
|
170
427
|
signal?.throwIfAborted();
|
|
171
|
-
const
|
|
172
|
-
|
|
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);
|
|
173
433
|
|
|
174
|
-
|
|
175
|
-
|
|
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
|
+
}
|
|
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];
|
|
176
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 });
|
|
177
447
|
const source = { status: "found", path: result.path, line: result.line, lines: [firstLine, lastLine],
|
|
178
448
|
text: block.text.slice(0, sourceChars), complete, nextOffset };
|
|
179
449
|
|
|
@@ -184,8 +454,10 @@ function createNativeAdapters(getCwd, vfs, config, index, ledger, hooks) {
|
|
|
184
454
|
async function readDirectory(dirPath, signal) {
|
|
185
455
|
signal?.throwIfAborted();
|
|
186
456
|
const rows = new Map();
|
|
457
|
+
let truncated = false;
|
|
187
458
|
|
|
188
459
|
for (const file of vfs.getOverlayPaths()) {
|
|
460
|
+
if (rows.size >= MAX_DIRECTORY_ENTRIES) { truncated = true; break; }
|
|
189
461
|
const relative = path.relative(dirPath, file);
|
|
190
462
|
|
|
191
463
|
if (!relative || relative === ".." || relative.startsWith(".." + path.sep) || path.isAbsolute(relative)) continue;
|
|
@@ -202,16 +474,135 @@ function createNativeAdapters(getCwd, vfs, config, index, ledger, hooks) {
|
|
|
202
474
|
entries = [];
|
|
203
475
|
}
|
|
204
476
|
|
|
205
|
-
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
|
+
}
|
|
206
482
|
|
|
207
|
-
|
|
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
|
+
}
|
|
522
|
+
|
|
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
|
+
}
|
|
208
595
|
}
|
|
209
596
|
|
|
210
597
|
async function readAdapter(params, signal) {
|
|
211
598
|
signal?.throwIfAborted();
|
|
599
|
+
params = normalizeReadWindow(params);
|
|
600
|
+
if (!isObject(params)) throw new Error("read requires an options object");
|
|
212
601
|
const cwd = getCwd();
|
|
213
602
|
const targetParam = params?.path ?? params?.target;
|
|
214
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
|
+
|
|
215
606
|
if (Array.isArray(targetParam)) {
|
|
216
607
|
if (targetParam.length > 64) throw new Error("read accepts at most 64 paths per batch");
|
|
217
608
|
|
|
@@ -219,9 +610,13 @@ function createNativeAdapters(getCwd, vfs, config, index, ledger, hooks) {
|
|
|
219
610
|
|
|
220
611
|
const results = await Promise.all(targetParam.map(async p => {
|
|
221
612
|
try {
|
|
222
|
-
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 ?? "";
|
|
223
618
|
|
|
224
|
-
return { text:
|
|
619
|
+
return { text: item };
|
|
225
620
|
} catch (error) {
|
|
226
621
|
signal?.throwIfAborted();
|
|
227
622
|
|
|
@@ -283,14 +678,33 @@ function createNativeAdapters(getCwd, vfs, config, index, ledger, hooks) {
|
|
|
283
678
|
async function readSingle(params, cwd, targetParam, signal) {
|
|
284
679
|
params = sessionJsonArgs({ ...params, path: targetParam });
|
|
285
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");
|
|
286
692
|
targetParam = params.path;
|
|
287
693
|
|
|
288
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");
|
|
289
696
|
const target = await resolveSessionResource(targetParam, signal);
|
|
290
697
|
|
|
291
698
|
return params.resolve
|
|
292
699
|
? openSource({status:"found",path:targetParam,line:params.offset ?? 1}, params, signal, target)
|
|
293
|
-
: 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);
|
|
294
708
|
}
|
|
295
709
|
|
|
296
710
|
if (isString(params?.query)) {
|
|
@@ -299,47 +713,169 @@ function createNativeAdapters(getCwd, vfs, config, index, ledger, hooks) {
|
|
|
299
713
|
return sourceRead(params.query, scope, signal, params);
|
|
300
714
|
}
|
|
301
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
|
+
|
|
302
724
|
const existing = await probeExistingPath(cwd, targetParam, vfs);
|
|
303
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
|
+
|
|
304
730
|
if (existing) {
|
|
305
|
-
if (!existing.directory)
|
|
306
|
-
|
|
307
|
-
|
|
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
|
+
}
|
|
308
741
|
|
|
309
742
|
if (params.json !== undefined) throw new Error("JSON read requires a file, not a directory");
|
|
310
743
|
|
|
311
744
|
return isString(params?.about) ? sourceRead(params.about, existing.path, signal, params) : readDirectory(existing.path, signal);
|
|
312
745
|
}
|
|
313
746
|
|
|
314
|
-
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
|
+
}
|
|
315
755
|
const targetPath = resolveReadPath(cwd, targetParam);
|
|
316
756
|
|
|
317
|
-
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 });
|
|
318
837
|
}
|
|
319
838
|
|
|
320
839
|
/** Plain text, a line window, or (with `about`) a relevance-folded outline of the whole file. */
|
|
321
|
-
async function readFile(targetPath, params, sourceLine, displayPath) {
|
|
840
|
+
async function readFile(targetPath, params, sourceLine, displayPath, query, signal) {
|
|
322
841
|
const cwd = getCwd();
|
|
323
842
|
const rel = displayPath ?? relativeSlash(cwd, targetPath);
|
|
324
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
|
+
|
|
325
848
|
if (params.json !== undefined) {
|
|
326
849
|
const project = jsonProjector(params.json);
|
|
327
|
-
const text = await vfs.read(targetPath, { maxBytes: MAX_JSON_BYTES });
|
|
850
|
+
const text = await vfs.read(targetPath, { maxBytes: MAX_JSON_BYTES, label: "JSON input" });
|
|
328
851
|
let document;
|
|
329
852
|
|
|
330
853
|
try { document = JSON.parse(text); }
|
|
331
854
|
catch { throw new Error("invalid JSON in " + rel + "; the entire document must parse before projection"); }
|
|
332
855
|
|
|
333
856
|
const many = Array.isArray(params.json);
|
|
334
|
-
|
|
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;
|
|
335
860
|
const parts = [];
|
|
336
861
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
862
|
+
try {
|
|
863
|
+
let index = 0;
|
|
864
|
+
|
|
865
|
+
for (const value of project(document)) {
|
|
866
|
+
const encoded = JSON.stringify(value);
|
|
340
867
|
|
|
341
|
-
|
|
342
|
-
|
|
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)));
|
|
343
879
|
}
|
|
344
880
|
|
|
345
881
|
return textResult(many ? "[" + parts.join(",") + "]" : parts[0], { path: targetPath, json: true, complete: true });
|
|
@@ -348,14 +884,64 @@ function createNativeAdapters(getCwd, vfs, config, index, ledger, hooks) {
|
|
|
348
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()];
|
|
349
885
|
|
|
350
886
|
if (mime) {
|
|
351
|
-
if ((await fs.stat(targetPath)).size > 20 * 1024 * 1024) throw new Error("image exceeds 20 MiB; resize it before reading");
|
|
352
887
|
const staged = vfs.getOverlay(targetPath);
|
|
353
|
-
|
|
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");
|
|
354
920
|
|
|
355
921
|
return { content: [{ type: "image", mimeType: mime, data: bytes.toString("base64") }], details: { path: targetPath } };
|
|
356
922
|
}
|
|
357
923
|
|
|
358
|
-
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
|
+
|
|
359
945
|
index.touch(rel);
|
|
360
946
|
|
|
361
947
|
if (isString(params?.about)) {
|
|
@@ -369,17 +955,31 @@ function createNativeAdapters(getCwd, vfs, config, index, ledger, hooks) {
|
|
|
369
955
|
}
|
|
370
956
|
}
|
|
371
957
|
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
958
|
+
let offset = params?.offset;
|
|
959
|
+
let limit = params?.limit;
|
|
960
|
+
let viewComplete;
|
|
961
|
+
|
|
962
|
+
if (params.resolve && isString(query)) {
|
|
963
|
+
const spans = WorkspaceIndex.spansOf(WorkspaceIndex.fromText(targetPath, text));
|
|
964
|
+
const span = pickSpan(spans, { line: sourceLine, name: query });
|
|
965
|
+
|
|
966
|
+
if (span) {
|
|
967
|
+
const spanLines = span.end - span.start + 1;
|
|
968
|
+
offset = span.start;
|
|
969
|
+
limit = isNumber(params.limit) ? Math.min(params.limit, spanLines) : spanLines;
|
|
970
|
+
viewComplete = limit >= spanLines;
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
offset ??= sourceLine && text.length > budget ? Math.max(1, sourceLine - 2) : 1;
|
|
375
975
|
const firstLine = isNumber(offset) ? Math.max(1, Math.floor(offset)) : 1;
|
|
376
|
-
const sliced =
|
|
976
|
+
const sliced = windowed ? text : sliceLinesRaw(text, offset, limit);
|
|
377
977
|
|
|
378
978
|
if (params.complete === true && (sliced !== text || sliced.length > budget || (params.resolve && JSON.stringify(sliced).length > budget))) {
|
|
379
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`);
|
|
380
980
|
}
|
|
381
981
|
|
|
382
|
-
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)) {
|
|
383
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");
|
|
384
984
|
let cap = budget - 160;
|
|
385
985
|
|
|
@@ -402,13 +1002,17 @@ function createNativeAdapters(getCwd, vfs, config, index, ledger, hooks) {
|
|
|
402
1002
|
if (end < 0) throw new Error(`line ${firstLine} exceeds the read budget; use bash to inspect a bounded substring`);
|
|
403
1003
|
const body = sliced.slice(0, end + 1);
|
|
404
1004
|
const next = firstLine + body.split("\n").length - 1;
|
|
1005
|
+
ledger.recordOrigin(rel, firstLine, sourceLines(body), explicit);
|
|
405
1006
|
|
|
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 });
|
|
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 });
|
|
407
1008
|
}
|
|
408
1009
|
|
|
409
|
-
|
|
1010
|
+
const slicedLines = sliced.length <= 512 * 1024 ? sourceLines(sliced) : null;
|
|
1011
|
+
const slicedLineCount = slicedLines?.length ?? contentLineInfo(sliced).count;
|
|
1012
|
+
|
|
1013
|
+
if (slicedLines) ledger.recordOrigin(rel, firstLine, slicedLines, explicit);
|
|
410
1014
|
|
|
411
|
-
return textResult(sliced, { path: targetPath, firstLine, lastLine: firstLine +
|
|
1015
|
+
return textResult(sliced, { path: targetPath, firstLine, lastLine: firstLine + slicedLineCount - 1, sourceChars: sliced.length, complete: windowed ? windowWhole : sliced === text, viewComplete });
|
|
412
1016
|
}
|
|
413
1017
|
|
|
414
1018
|
/**
|
|
@@ -416,19 +1020,33 @@ function createNativeAdapters(getCwd, vfs, config, index, ledger, hooks) {
|
|
|
416
1020
|
* lines with numbers, a quick structural check, and bounded lexical reference hints.
|
|
417
1021
|
* These do not replace tests or semantic caller resolution.
|
|
418
1022
|
*/
|
|
419
|
-
async function editSummary(cwd, target, original, updated, diff, signal) {
|
|
1023
|
+
async function editSummary(cwd, target, original, updated, diff, signal, span) {
|
|
420
1024
|
const rel = relativeSlash(cwd, target);
|
|
421
|
-
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
|
+
};
|
|
422
1034
|
const ranges = [];
|
|
423
1035
|
|
|
424
|
-
|
|
425
|
-
|
|
1036
|
+
if (span && Number.isInteger(span.start) && Number.isInteger(span.end) && span.start >= 1 && span.end >= span.start) {
|
|
1037
|
+
const end = Math.min(lineCount, span.end);
|
|
1038
|
+
|
|
1039
|
+
if (span.start <= end) ranges.push({ start: span.start, end });
|
|
1040
|
+
} else {
|
|
1041
|
+
const positions = diff.lines.filter(row => row.type !== "context")
|
|
1042
|
+
.map(row => Math.min(lineCount, row.newLineNum ?? row.lineNum)).sort((a, b) => a - b);
|
|
426
1043
|
|
|
427
|
-
|
|
428
|
-
|
|
1044
|
+
for (const line of positions) {
|
|
1045
|
+
const start = Math.max(1, line - 2), end = Math.min(lineCount, line + 2);
|
|
429
1046
|
|
|
430
|
-
|
|
431
|
-
|
|
1047
|
+
if (ranges.length && start <= ranges.at(-1).end + 1) ranges.at(-1).end = Math.max(ranges.at(-1).end, end);
|
|
1048
|
+
else ranges.push({ start, end });
|
|
1049
|
+
}
|
|
432
1050
|
}
|
|
433
1051
|
|
|
434
1052
|
const blocks = [];
|
|
@@ -436,7 +1054,7 @@ function createNativeAdapters(getCwd, vfs, config, index, ledger, hooks) {
|
|
|
436
1054
|
|
|
437
1055
|
for (const { start, end } of ranges) {
|
|
438
1056
|
const last = Math.min(end, start + perRange - 1);
|
|
439
|
-
const lines =
|
|
1057
|
+
const lines = Array.from({ length: Math.max(0, last - start + 1) }, (_, i) => lineAt(start + i));
|
|
440
1058
|
ledger.recordOrigin(rel, start, lines);
|
|
441
1059
|
blocks.push("edited " + rel + ":" + start + "-" + last + "\n" + lines.map((line, i) => String(start + i).padStart(5) + " " + line).join("\n"));
|
|
442
1060
|
|
|
@@ -444,7 +1062,7 @@ function createNativeAdapters(getCwd, vfs, config, index, ledger, hooks) {
|
|
|
444
1062
|
}
|
|
445
1063
|
|
|
446
1064
|
let out = blocks.join("\n");
|
|
447
|
-
const check = quickCheck(updated, path.extname(target));
|
|
1065
|
+
const check = updated.length <= QUICK_CHECK_MAX_CHARS ? quickCheck(updated, path.extname(target)) : null;
|
|
448
1066
|
|
|
449
1067
|
if (check && !check.ok) out += `\ncheck: ${check.message}`;
|
|
450
1068
|
const refs = await changedDeclarationRefs(cwd, target, original, updated, diff, signal);
|
|
@@ -456,18 +1074,26 @@ function createNativeAdapters(getCwd, vfs, config, index, ledger, hooks) {
|
|
|
456
1074
|
|
|
457
1075
|
async function changedDeclarationRefs(cwd, target, original, updated, diff, signal) {
|
|
458
1076
|
// Diff rows carry the replaced fragments; declarations live on whole file lines.
|
|
459
|
-
|
|
460
|
-
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
|
+
};
|
|
461
1087
|
const names = new Set();
|
|
462
1088
|
const spans = new Map();
|
|
463
1089
|
|
|
464
1090
|
for (const l of diff.lines) {
|
|
465
1091
|
if (l.type === "context") continue;
|
|
466
1092
|
const number = l.type === "remove" ? l.lineNum : l.newLineNum ?? l.lineNum;
|
|
467
|
-
const name = declaredName((l.type === "remove" ?
|
|
1093
|
+
const name = declaredName(changedLine(l.type === "remove" ? original : updated, l.type === "remove" ? oldLines : newLines, number));
|
|
468
1094
|
|
|
469
1095
|
if (name) names.add(name);
|
|
470
|
-
else {
|
|
1096
|
+
else if (canMapOwners) {
|
|
471
1097
|
if (!spans.has(l.type)) spans.set(l.type, WorkspaceIndex.spansOf(WorkspaceIndex.fromText(target, l.type === "remove" ? original : updated)));
|
|
472
1098
|
const owner = spans.get(l.type).find(span => span.start <= number && number <= span.end);
|
|
473
1099
|
|
|
@@ -502,18 +1128,36 @@ function createNativeAdapters(getCwd, vfs, config, index, ledger, hooks) {
|
|
|
502
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;
|
|
503
1129
|
|
|
504
1130
|
/** Fresh bounded source window for a diagnostic; no index warmup or stale cached bodies. */
|
|
505
|
-
async function sourceWindow(cwd, commandCwd, file, lineNo) {
|
|
1131
|
+
async function sourceWindow(cwd, commandCwd, file, lineNo, signal) {
|
|
506
1132
|
const candidate = path.resolve(commandCwd, file);
|
|
507
1133
|
let text, rel;
|
|
508
1134
|
|
|
509
1135
|
try {
|
|
510
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;
|
|
511
1139
|
|
|
512
|
-
if (!candidate.startsWith(
|
|
1140
|
+
if (!candidate.startsWith(cwdPrefix) && !candidate.startsWith(rootPrefix)) return null;
|
|
513
1141
|
const real = await fs.realpath(candidate);
|
|
1142
|
+
const handle = await fs.open(real, fs.constants.O_RDONLY | (fs.constants.O_NONBLOCK ?? 0));
|
|
514
1143
|
|
|
515
|
-
|
|
516
|
-
|
|
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(); }
|
|
517
1161
|
rel = relativeSlash(root, real);
|
|
518
1162
|
} catch { return null; }
|
|
519
1163
|
|
|
@@ -530,7 +1174,7 @@ function createNativeAdapters(getCwd, vfs, config, index, ledger, hooks) {
|
|
|
530
1174
|
}
|
|
531
1175
|
|
|
532
1176
|
/** A failing command names path:line; the model wants those lines next. Attach them (≤4 sites). */
|
|
533
|
-
async function sourceForReferences(cwd, commandCwd, output) {
|
|
1177
|
+
async function sourceForReferences(cwd, commandCwd, output, signal) {
|
|
534
1178
|
const seen = new Set();
|
|
535
1179
|
const blocks = [];
|
|
536
1180
|
|
|
@@ -541,7 +1185,7 @@ function createNativeAdapters(getCwd, vfs, config, index, ledger, hooks) {
|
|
|
541
1185
|
|
|
542
1186
|
if (seen.size >= 4) break;
|
|
543
1187
|
seen.add(key);
|
|
544
|
-
const block = await sourceWindow(cwd, commandCwd, m[1], Number(m[2]));
|
|
1188
|
+
const block = await sourceWindow(cwd, commandCwd, m[1], Number(m[2]), signal);
|
|
545
1189
|
|
|
546
1190
|
if (block) blocks.push(block);
|
|
547
1191
|
}
|
|
@@ -552,7 +1196,7 @@ function createNativeAdapters(getCwd, vfs, config, index, ledger, hooks) {
|
|
|
552
1196
|
function outlineOptions(params, references) {
|
|
553
1197
|
const options = { references };
|
|
554
1198
|
|
|
555
|
-
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);
|
|
556
1200
|
|
|
557
1201
|
return options;
|
|
558
1202
|
}
|
|
@@ -568,7 +1212,10 @@ function createNativeAdapters(getCwd, vfs, config, index, ledger, hooks) {
|
|
|
568
1212
|
|
|
569
1213
|
/** Where else a name appears (declaration line excluded), for outlines and edit results. */
|
|
570
1214
|
async function referenceFinder(cwd, targetPath) {
|
|
571
|
-
|
|
1215
|
+
let files;
|
|
1216
|
+
|
|
1217
|
+
try { files = [...new Set([...await index.files(cwd), ...vfs.getOverlayPaths()])]; }
|
|
1218
|
+
catch { return () => []; }
|
|
572
1219
|
|
|
573
1220
|
if (!index.canScan(files)) return () => [];
|
|
574
1221
|
|
|
@@ -586,7 +1233,7 @@ function createNativeAdapters(getCwd, vfs, config, index, ledger, hooks) {
|
|
|
586
1233
|
|
|
587
1234
|
hooks.summarizeEdit = editSummary;
|
|
588
1235
|
|
|
589
|
-
|
|
1236
|
+
const adapters = {
|
|
590
1237
|
read: readAdapter,
|
|
591
1238
|
async write(params, signal) {
|
|
592
1239
|
const cwd = getCwd();
|
|
@@ -597,24 +1244,51 @@ function createNativeAdapters(getCwd, vfs, config, index, ledger, hooks) {
|
|
|
597
1244
|
if (!isString(params?.content)) throw new Error("write requires string content");
|
|
598
1245
|
|
|
599
1246
|
if (params.append !== undefined && params.append !== true && params.append !== false) throw new Error("write append must be a boolean");
|
|
600
|
-
|
|
1247
|
+
if (params.allowReadArtifacts !== undefined && typeof params.allowReadArtifacts !== "boolean") throw new Error("write allowReadArtifacts must be a boolean");
|
|
1248
|
+
let content = String(params.content);
|
|
601
1249
|
|
|
602
|
-
if (params.allowReadArtifacts !== true && /\[read truncated;|…\[
|
|
1250
|
+
if (params.allowReadArtifacts !== true && /\[read truncated;|…\[[^\]\n]*truncated[^\]\n]*\]…/u.test(content)) {
|
|
603
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");
|
|
604
1252
|
}
|
|
605
1253
|
|
|
606
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
|
+
}
|
|
607
1284
|
|
|
608
|
-
try {
|
|
609
|
-
prevText = await vfs.read(target, { preserveRead: true });
|
|
610
|
-
} catch (error) { if (error.code !== "ENOENT") throw error; }
|
|
611
|
-
|
|
612
|
-
if (params.append === true) content = prevText + content;
|
|
613
1285
|
const { speculative } = await vfs.write(target, content);
|
|
614
1286
|
index.touch(relativeSlash(cwd, target));
|
|
615
|
-
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);
|
|
616
1290
|
const tag = speculative ? " (speculative)" : "";
|
|
617
|
-
const check = quickCheck(content, path.extname(target));
|
|
1291
|
+
const check = content.length <= QUICK_CHECK_MAX_CHARS ? quickCheck(content, path.extname(target)) : null;
|
|
618
1292
|
const warning = check && !check.ok ? "\ncheck: " + check.message : "";
|
|
619
1293
|
|
|
620
1294
|
return textResult(`wrote ${target}${tag}${warning}`, { path: target, speculative, diff });
|
|
@@ -625,17 +1299,38 @@ function createNativeAdapters(getCwd, vfs, config, index, ledger, hooks) {
|
|
|
625
1299
|
|
|
626
1300
|
if (signal?.aborted) throw new Error("aborted");
|
|
627
1301
|
|
|
1302
|
+
const content = await vfs.read(target, { maxBytes: 64 * 1024 * 1024 });
|
|
1303
|
+
|
|
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);
|
|
1307
|
+
const windowNext = isString(params.oldText)
|
|
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);
|
|
1311
|
+
const { speculative } = await vfs.write(target, updated);
|
|
1312
|
+
index.touch(relativeSlash(cwd, target));
|
|
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);
|
|
1317
|
+
const inserted = sourceLines(windowNext);
|
|
1318
|
+
const spanEnd = params.viewStart + Math.max(inserted.length, 1) - 1;
|
|
1319
|
+
const summary = await editSummary(cwd, target, content, updated, diff, signal, { start: params.viewStart, end: spanEnd });
|
|
1320
|
+
|
|
1321
|
+
return textResult(summary, { path: target, speculative, diff });
|
|
1322
|
+
}
|
|
1323
|
+
|
|
628
1324
|
const requestedEdits = Array.isArray(params?.edits)
|
|
629
1325
|
? params.edits
|
|
630
1326
|
: [{ oldText: params?.oldText, newText: params?.newText }];
|
|
631
|
-
|
|
632
|
-
const content = await vfs.read(target);
|
|
633
1327
|
const { updated, matches } = applyReplacements(target, content, requestedEdits);
|
|
634
1328
|
const { speculative } = await vfs.write(target, updated);
|
|
635
1329
|
index.touch(relativeSlash(cwd, target));
|
|
636
1330
|
|
|
637
|
-
const diff =
|
|
638
|
-
|
|
1331
|
+
const diff = content.length > 512 * 1024 || updated.length > 512 * 1024
|
|
1332
|
+
? boundedEditDiff(target, content, matches)
|
|
1333
|
+
: matches.length === 1
|
|
639
1334
|
? buildEditDiff(target, content, matches[0].oldText, matches[0].newText)
|
|
640
1335
|
: buildMultiEditDiff(target, content, matches);
|
|
641
1336
|
|
|
@@ -648,9 +1343,11 @@ function createNativeAdapters(getCwd, vfs, config, index, ledger, hooks) {
|
|
|
648
1343
|
let inputPath = params?.path;
|
|
649
1344
|
|
|
650
1345
|
if (!inputPath && isString(params?.patch)) {
|
|
651
|
-
const
|
|
1346
|
+
for (const match of params.patch.matchAll(/^(?:---|\+\+\+)\s+([^\t\n]+)/gm)) {
|
|
1347
|
+
const candidate = match[1].trim().replace(/^[ab]\//, "");
|
|
652
1348
|
|
|
653
|
-
|
|
1349
|
+
if (candidate !== "/dev/null") { inputPath = candidate; break; }
|
|
1350
|
+
}
|
|
654
1351
|
}
|
|
655
1352
|
|
|
656
1353
|
const target = await resolveWorkspacePath(cwd, inputPath, "apply_patch", false);
|
|
@@ -661,7 +1358,15 @@ function createNativeAdapters(getCwd, vfs, config, index, ledger, hooks) {
|
|
|
661
1358
|
|
|
662
1359
|
if (signal?.aborted) throw new Error("aborted");
|
|
663
1360
|
|
|
664
|
-
|
|
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");
|
|
665
1370
|
const { resultText, hunkCount } = applyPatchToText(original, params.patch);
|
|
666
1371
|
const { speculative } = await vfs.write(target, resultText);
|
|
667
1372
|
const diff = buildPatchDiff(target, params.patch);
|
|
@@ -706,14 +1411,15 @@ function createNativeAdapters(getCwd, vfs, config, index, ledger, hooks) {
|
|
|
706
1411
|
const cwd = getCwd();
|
|
707
1412
|
|
|
708
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");
|
|
709
1415
|
|
|
710
1416
|
if (signal?.aborted) throw new Error("aborted");
|
|
711
1417
|
const searchDir = params?.path ? await resolveWorkspacePath(cwd, params.path, "evidence", true) : cwd;
|
|
712
1418
|
const options = {};
|
|
713
1419
|
|
|
714
|
-
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);
|
|
715
1421
|
|
|
716
|
-
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);
|
|
717
1423
|
const res = await selectEvidence({ query: params.query, root: cwd, searchDir, index, overlayText: (p) => vfs.getOverlay(p), pendingPaths: vfs.getOverlayPaths(), options });
|
|
718
1424
|
|
|
719
1425
|
for (const span of res.spans) ledger.recordOrigin(span.path, span.lines[0], span.text.split("\n"));
|
|
@@ -725,7 +1431,7 @@ function createNativeAdapters(getCwd, vfs, config, index, ledger, hooks) {
|
|
|
725
1431
|
const target = await resolveWorkspacePath(cwd, params?.path, "surface", false);
|
|
726
1432
|
|
|
727
1433
|
if (signal?.aborted) throw new Error("aborted");
|
|
728
|
-
const text = await vfs.read(target);
|
|
1434
|
+
const text = await vfs.read(target, { maxBytes: 2 * 1024 * 1024 });
|
|
729
1435
|
const ext = path.extname(target);
|
|
730
1436
|
const outline = extractStructuralSurface(text, ext);
|
|
731
1437
|
|
|
@@ -733,8 +1439,9 @@ function createNativeAdapters(getCwd, vfs, config, index, ledger, hooks) {
|
|
|
733
1439
|
},
|
|
734
1440
|
async bash(params, signal) {
|
|
735
1441
|
const cwd = getCwd();
|
|
736
|
-
const literal = params?.
|
|
1442
|
+
const literal = Array.isArray(params?.args) && process.platform !== "win32" && params.args.length === Object.keys(params.args).length && params.args.every(isString);
|
|
737
1443
|
|
|
1444
|
+
if (params?.command !== undefined && !isString(params.command)) throw new Error("bash command must be a string");
|
|
738
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");
|
|
739
1446
|
const command = literal ? String(params.command) : unwrapIfFullyQuoted(String(params?.command ?? "").trim());
|
|
740
1447
|
|
|
@@ -757,7 +1464,7 @@ function createNativeAdapters(getCwd, vfs, config, index, ledger, hooks) {
|
|
|
757
1464
|
maxOutputChars: config.maxCallResultChars,
|
|
758
1465
|
});
|
|
759
1466
|
} catch (error) {
|
|
760
|
-
if (!signal?.aborted) error.message += await sourceForReferences(cwd, targetCwd, error.message);
|
|
1467
|
+
if (!signal?.aborted) error.message += await sourceForReferences(cwd, targetCwd, error.message, signal);
|
|
761
1468
|
throw error;
|
|
762
1469
|
} finally {
|
|
763
1470
|
vfs.invalidateCache();
|
|
@@ -769,7 +1476,7 @@ function createNativeAdapters(getCwd, vfs, config, index, ledger, hooks) {
|
|
|
769
1476
|
const { stdout, stderr } = res;
|
|
770
1477
|
let text = stdout && stderr ? stdout + (stdout.endsWith("\n") ? "" : "\n") + stderr : stdout || stderr;
|
|
771
1478
|
|
|
772
|
-
if (res.exitCode !== 0) text += await sourceForReferences(cwd, targetCwd, text);
|
|
1479
|
+
if (res.exitCode !== 0) text += await sourceForReferences(cwd, targetCwd, text, signal);
|
|
773
1480
|
|
|
774
1481
|
return {
|
|
775
1482
|
content: [{ type: "text", text }],
|
|
@@ -783,7 +1490,7 @@ function createNativeAdapters(getCwd, vfs, config, index, ledger, hooks) {
|
|
|
783
1490
|
|
|
784
1491
|
if (!pattern) throw new Error("grep requires pattern");
|
|
785
1492
|
const searchPath = params?.path ? await resolveWorkspacePath(cwd, params.path, "grep", true) : cwd;
|
|
786
|
-
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());
|
|
787
1494
|
|
|
788
1495
|
if (indexed !== null) return textResult(indexed, { exitCode: indexed ? 0 : 1, via: "index" });
|
|
789
1496
|
// Large tree: real rg keeps its own output format.
|
|
@@ -800,30 +1507,14 @@ function createNativeAdapters(getCwd, vfs, config, index, ledger, hooks) {
|
|
|
800
1507
|
const pattern = String(params?.pattern || "");
|
|
801
1508
|
|
|
802
1509
|
if (!pattern) throw new Error("glob requires pattern");
|
|
803
|
-
const fuzzy = await fuzzyFind(index, cwd, cwd, pattern);
|
|
1510
|
+
const fuzzy = await fuzzyFind(index, cwd, cwd, pattern, 20, vfs.getOverlayPaths());
|
|
804
1511
|
|
|
805
1512
|
if (fuzzy !== null) return textResult(fuzzy, { via: "fuzzy" });
|
|
806
|
-
const indexed = await listIndexed(index, cwd, cwd, pattern);
|
|
1513
|
+
const indexed = await listIndexed(index, cwd, cwd, pattern, vfs.getOverlayPaths());
|
|
807
1514
|
|
|
808
1515
|
if (indexed !== null) return textResult(indexed, { via: "index" });
|
|
809
1516
|
|
|
810
|
-
|
|
811
|
-
() => null,
|
|
812
|
-
);
|
|
813
|
-
|
|
814
|
-
if (rg && (rg.exitCode === 0 || rg.exitCode === 1)) {
|
|
815
|
-
return textResult(rg.stdout, { via: "rg" });
|
|
816
|
-
}
|
|
817
|
-
|
|
818
|
-
const findPattern = pattern.startsWith("./") ? pattern : `./${pattern}`;
|
|
819
|
-
|
|
820
|
-
const fallback = await runCommand(["find", ".", "-type", "f", "-path", findPattern], {
|
|
821
|
-
cwd,
|
|
822
|
-
timeoutMs: 30_000,
|
|
823
|
-
signal,
|
|
824
|
-
});
|
|
825
|
-
|
|
826
|
-
return textResult(fallback.stdout, { via: "find" });
|
|
1517
|
+
return listWithTools(cwd, pattern, cwd, signal, vfs.getOverlayPaths());
|
|
827
1518
|
},
|
|
828
1519
|
async find(params, signal) {
|
|
829
1520
|
const cwd = getCwd();
|
|
@@ -832,24 +1523,45 @@ function createNativeAdapters(getCwd, vfs, config, index, ledger, hooks) {
|
|
|
832
1523
|
|
|
833
1524
|
if (signal?.aborted) throw new Error("aborted");
|
|
834
1525
|
const globPattern = pattern ? String(pattern) : null;
|
|
835
|
-
const fuzzy = await fuzzyFind(index, searchDir, cwd, globPattern);
|
|
1526
|
+
const fuzzy = await fuzzyFind(index, searchDir, cwd, globPattern, 20, vfs.getOverlayPaths());
|
|
836
1527
|
|
|
837
1528
|
if (fuzzy !== null) return textResult(fuzzy, { via: "fuzzy" });
|
|
838
|
-
const indexed = await listIndexed(index, searchDir, cwd, globPattern);
|
|
1529
|
+
const indexed = await listIndexed(index, searchDir, cwd, globPattern, vfs.getOverlayPaths());
|
|
839
1530
|
|
|
840
1531
|
if (indexed !== null) return textResult(indexed, { via: "index" });
|
|
841
1532
|
|
|
842
|
-
return listWithTools(searchDir, globPattern, cwd, signal);
|
|
1533
|
+
return listWithTools(searchDir, globPattern, cwd, signal, vfs.getOverlayPaths());
|
|
843
1534
|
},
|
|
844
1535
|
async ls(params, signal) {
|
|
845
1536
|
const cwd = getCwd();
|
|
846
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
|
+
}
|
|
847
1552
|
|
|
848
1553
|
return readDirectory(dirPath, signal);
|
|
849
1554
|
},
|
|
850
1555
|
};
|
|
1556
|
+
|
|
1557
|
+
return adapters;
|
|
851
1558
|
}
|
|
852
1559
|
|
|
1560
|
+
/**
|
|
1561
|
+
* Fused INVOKE kernel. Guest RPC is the only caller; fuel is cwd + vfs + signal.
|
|
1562
|
+
* BIND stays downward (see tests/contracts/layers.test.mjs). Do not split this
|
|
1563
|
+
* closure into pass-through files that re-import each other.
|
|
1564
|
+
*/
|
|
853
1565
|
export function createHostBridge({ pi, config, getCwd, registry, ledger: runLedger, budget }) {
|
|
854
1566
|
const index = registry?.index ?? new WorkspaceIndex((argv, opts) => runCommand(argv, opts));
|
|
855
1567
|
const ledger = runLedger ?? new SeenLedger({ window: config.seenWindow ?? 0 });
|
|
@@ -932,13 +1644,22 @@ export function createHostBridge({ pi, config, getCwd, registry, ledger: runLedg
|
|
|
932
1644
|
const sessionId = ctx?.sessionManager?.getSessionId?.();
|
|
933
1645
|
boundSessionId = sessionId;
|
|
934
1646
|
const registry = pi?.pi?.AgentRegistry?.global?.();
|
|
935
|
-
|
|
936
|
-
|
|
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
|
|
937
1653
|
: null;
|
|
938
1654
|
activeSignal = signal;
|
|
939
1655
|
vfs.signal = signal;
|
|
940
1656
|
}
|
|
941
1657
|
|
|
1658
|
+
function evalToolNames() {
|
|
1659
|
+
try { return hostSession?.getEvalBridgeToolNames?.() ?? []; }
|
|
1660
|
+
catch { return []; }
|
|
1661
|
+
}
|
|
1662
|
+
|
|
942
1663
|
function hostTool(name) {
|
|
943
1664
|
if (!hostSession) return undefined;
|
|
944
1665
|
const metadata = definitions.get(name);
|
|
@@ -946,13 +1667,14 @@ export function createHostBridge({ pi, config, getCwd, registry, ledger: runLedg
|
|
|
946
1667
|
// Keep Supernova's transactional adapters for ordinary built-ins. Respect overrides.
|
|
947
1668
|
if (Object.hasOwn(natives, name) && metadata?.sourceInfo?.source === "builtin") return undefined;
|
|
948
1669
|
|
|
949
|
-
return hostSession.getToolForEvalBridge?.(name);
|
|
1670
|
+
try { return hostSession.getToolForEvalBridge?.(name); }
|
|
1671
|
+
catch { return undefined; }
|
|
950
1672
|
}
|
|
951
1673
|
|
|
952
1674
|
function isCallable(name) {
|
|
953
1675
|
if (name === "supernova" || (config.excludeTools ?? []).includes(name)) return false;
|
|
954
1676
|
|
|
955
|
-
if (hostSession && (hostSession.isDisposed || hostSession.sessionManager
|
|
1677
|
+
if (hostSession && (hostSession.isDisposed || hostSession.sessionManager?.getSessionId?.() !== boundSessionId)) return false;
|
|
956
1678
|
|
|
957
1679
|
// An internal adapter belongs to Supernova, not the host's visible tool list.
|
|
958
1680
|
const nativeOwned = Object.hasOwn(natives, name) && !executors.has(name)
|
|
@@ -961,18 +1683,27 @@ export function createHostBridge({ pi, config, getCwd, registry, ledger: runLedg
|
|
|
961
1683
|
if (nativeOwned) return true;
|
|
962
1684
|
|
|
963
1685
|
if (hostSession) {
|
|
964
|
-
|
|
1686
|
+
const evalNames = evalToolNames();
|
|
1687
|
+
|
|
1688
|
+
if (!evalNames.includes(name) && definitions.has(name)) return false;
|
|
965
1689
|
|
|
966
1690
|
return !!hostTool(name) || (Object.hasOwn(natives, name) && (!definitions.has(name) || definitions.get(name).sourceInfo?.source === "builtin"));
|
|
967
1691
|
}
|
|
968
1692
|
|
|
969
|
-
|
|
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;
|
|
970
1698
|
|
|
971
1699
|
return executors.has(name) || Object.hasOwn(natives, name);
|
|
972
1700
|
}
|
|
973
1701
|
|
|
974
1702
|
function refreshTools() {
|
|
975
|
-
|
|
1703
|
+
let listed = [];
|
|
1704
|
+
|
|
1705
|
+
try { listed = pi?.getAllTools?.() ?? []; } catch {}
|
|
1706
|
+
const tools = Array.isArray(listed) ? listed : [];
|
|
976
1707
|
|
|
977
1708
|
for (const tool of tools) {
|
|
978
1709
|
if (!isString(tool?.name)) continue;
|
|
@@ -1018,20 +1749,6 @@ export function createHostBridge({ pi, config, getCwd, registry, ledger: runLedg
|
|
|
1018
1749
|
return vfs.rollback();
|
|
1019
1750
|
}
|
|
1020
1751
|
|
|
1021
|
-
function resultDiff(response) {
|
|
1022
|
-
let details = response?.details;
|
|
1023
|
-
|
|
1024
|
-
if (isString(details)) {
|
|
1025
|
-
try {
|
|
1026
|
-
details = JSON.parse(details);
|
|
1027
|
-
} catch {
|
|
1028
|
-
return undefined;
|
|
1029
|
-
}
|
|
1030
|
-
}
|
|
1031
|
-
|
|
1032
|
-
return isObject(details) ? details.diff : undefined;
|
|
1033
|
-
}
|
|
1034
|
-
|
|
1035
1752
|
function notifyCall(record) {
|
|
1036
1753
|
if (!callListener) return;
|
|
1037
1754
|
|
|
@@ -1073,14 +1790,33 @@ export function createHostBridge({ pi, config, getCwd, registry, ledger: runLedg
|
|
|
1073
1790
|
if (name !== "write" || !isString(args?.path) || !isString(args?.content)) return undefined;
|
|
1074
1791
|
const target = await resolveWorkspacePath(getCwd(), args.path, "write", false);
|
|
1075
1792
|
let previous = "";
|
|
1793
|
+
let removedLines;
|
|
1794
|
+
let stat;
|
|
1076
1795
|
|
|
1077
|
-
try {
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
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
|
+
}
|
|
1081
1817
|
}
|
|
1082
1818
|
|
|
1083
|
-
return buildWriteDiff(target, previous, args.content);
|
|
1819
|
+
return removedLines === undefined ? buildWriteDiff(target, previous, args.content) : boundedWriteDiff(target, args.content, removedLines);
|
|
1084
1820
|
}
|
|
1085
1821
|
|
|
1086
1822
|
function completeRecord(record, res, fallbackDiff) {
|
|
@@ -1091,6 +1827,24 @@ export function createHostBridge({ pi, config, getCwd, registry, ledger: runLedg
|
|
|
1091
1827
|
notifyCall(record);
|
|
1092
1828
|
}
|
|
1093
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
|
+
|
|
1094
1848
|
async function invokeRaw(name, args) {
|
|
1095
1849
|
checkCallBudget(name);
|
|
1096
1850
|
const callId = ++sharedRegistry.callSeq;
|
|
@@ -1099,15 +1853,17 @@ export function createHostBridge({ pi, config, getCwd, registry, ledger: runLedg
|
|
|
1099
1853
|
if (!isCallable(name)) throw new Error(unknownToolMessage(name, [...definitions.keys(), ...Object.keys(natives)].filter(isCallable)));
|
|
1100
1854
|
|
|
1101
1855
|
const command = { apply_patch: "edit", surface: "read", evidence: "read", snap: "read" }[name] ?? name;
|
|
1102
|
-
const record = { name: command, adapter: name, args: args
|
|
1856
|
+
const record = { name: command, adapter: name, args: traceArgs(args), time: Date.now() };
|
|
1103
1857
|
trace.push(record);
|
|
1104
1858
|
notifyCall(record);
|
|
1105
1859
|
|
|
1106
1860
|
try {
|
|
1107
1861
|
const delegated = hostTool(name);
|
|
1108
1862
|
const exec = delegated ? delegated.execute.bind(delegated) : hostSession ? undefined : executors.get(name);
|
|
1109
|
-
|
|
1110
|
-
|
|
1863
|
+
const argvOwned = name === "bash" && Array.isArray(args?.args) && process.platform !== "win32" && args.args.length === Object.keys(args.args).length && args.args.every(isString);
|
|
1864
|
+
// Explicit overrides own all reads. Options, even false-valued ones,
|
|
1865
|
+
// must not silently bypass the host executor.
|
|
1866
|
+
if (exec && !argvOwned) {
|
|
1111
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");
|
|
1112
1868
|
|
|
1113
1869
|
if (name === "write" && args?.append === true) throw new Error("append requires the Supernova-owned write adapter, not an external override");
|
|
@@ -1122,7 +1878,7 @@ export function createHostBridge({ pi, config, getCwd, registry, ledger: runLedg
|
|
|
1122
1878
|
|
|
1123
1879
|
try {
|
|
1124
1880
|
const res = await exec(`supernova:${name}:${callId}`, args || {}, activeSignal, undefined, delegated
|
|
1125
|
-
? { ...activeCtx, settings: hostSession.settings, toolNames:
|
|
1881
|
+
? { ...activeCtx, settings: hostSession.settings, toolNames: evalToolNames(), autoApprove: false }
|
|
1126
1882
|
: activeCtx);
|
|
1127
1883
|
|
|
1128
1884
|
completeRecord(record, res, fallbackDiff);
|
|
@@ -1136,7 +1892,7 @@ export function createHostBridge({ pi, config, getCwd, registry, ledger: runLedg
|
|
|
1136
1892
|
const native = Object.hasOwn(natives, name) ? natives[name] : undefined;
|
|
1137
1893
|
|
|
1138
1894
|
if (native) {
|
|
1139
|
-
const res = await native(args || {}, activeSignal);
|
|
1895
|
+
const res = await native(argvOwned ? { ...(args || {}), args: args.args.map(String) } : args || {}, activeSignal);
|
|
1140
1896
|
completeRecord(record, res);
|
|
1141
1897
|
|
|
1142
1898
|
return res;
|
|
@@ -1158,6 +1914,11 @@ export function createHostBridge({ pi, config, getCwd, registry, ledger: runLedg
|
|
|
1158
1914
|
const exitCode = isObject(res?.details) ? res.details.exitCode : undefined;
|
|
1159
1915
|
|
|
1160
1916
|
if (Number.isInteger(exitCode) && exitCode !== 0) record.exitCode = exitCode;
|
|
1917
|
+
const text = isObject(res) && Array.isArray(res.content)
|
|
1918
|
+
? res.content.filter(part => part?.type === "text" && isString(part.text)).map(part => part.text).join("\n")
|
|
1919
|
+
: undefined;
|
|
1920
|
+
|
|
1921
|
+
if (text) record.resultText = truncateChars(text, 4096, "trace").text;
|
|
1161
1922
|
}
|
|
1162
1923
|
|
|
1163
1924
|
async function call(name, args) {
|