pi-supernova 0.8.1 → 0.9.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 +244 -54
- package/docs/CHANGELOG.md +111 -1
- package/docs/TOKEN_COSTS.md +38 -0
- package/index.js +10 -175
- package/package.json +2 -1
- package/src/adapters/bash.js +23 -33
- package/src/adapters/errors.js +1 -9
- package/src/adapters/read-focus.js +98 -0
- package/src/adapters/read-image.js +51 -0
- package/src/adapters/read-json.js +42 -0
- package/src/adapters/read-text.js +71 -0
- package/src/adapters/read.js +66 -635
- package/src/bridge/catalog.js +3 -2
- package/src/bridge/host-bridge.js +35 -167
- package/src/bridge/tool-registry.js +104 -0
- package/src/bridge/trace.js +41 -0
- package/src/context/evidence-graph.js +249 -0
- package/src/context/evidence-rank.js +153 -0
- package/src/context/evidence.js +10 -424
- package/src/context/query.js +71 -0
- package/src/context/repo-index.js +8 -162
- package/src/context/search-files.js +19 -0
- package/src/context/search.js +2 -24
- package/src/context/snap-search.js +202 -0
- package/src/context/snap.js +5 -266
- package/src/context/source-entry.js +112 -0
- package/src/contract/bash.js +18 -2
- package/src/contract/program.js +36 -0
- package/src/contract/read.js +8 -53
- package/src/fs/check.js +1 -1
- package/src/fs/commit.js +161 -0
- package/src/fs/diff.js +11 -15
- package/src/fs/directory.js +79 -0
- package/src/fs/file-io.js +100 -0
- package/src/fs/glob.js +54 -0
- package/src/fs/json-size.js +54 -0
- package/src/fs/lines.js +117 -0
- package/src/fs/read-window.js +74 -0
- package/src/fs/session-resource.js +50 -0
- package/src/fs/text-ops.js +7 -242
- package/src/fs/vfs.js +5 -239
- package/src/fs/workspace.js +7 -2
- package/src/output/bottleneck.js +13 -67
- package/src/output/final.js +114 -0
- package/src/output/format.js +94 -5
- package/src/output/outcome.js +91 -0
- package/src/runtime/batch-input.js +68 -0
- package/src/runtime/guest-api.js +281 -0
- package/src/runtime/guest-worker.js +62 -333
- package/src/runtime/parallel.js +41 -39
- package/src/runtime/program-batch.js +35 -68
- package/src/runtime/program-file.js +3 -11
- package/src/runtime/program.js +141 -0
- package/src/runtime/reference.js +11 -10
- package/src/runtime/runtime.js +94 -261
- package/src/runtime/worker-pool.js +91 -0
- package/src/shared/decode.js +22 -8
- package/src/shared/image-worker.js +30 -0
- package/src/shared/image.js +78 -0
- package/src/shared/png.js +57 -0
- package/src/shared/result.js +77 -0
- package/src/shared/syntax-context.js +61 -3
- package/src/ui/host-render.js +104 -0
- package/src/ui/progress.js +51 -0
- package/src/ui/render.js +21 -421
- package/src/ui/trace.js +277 -0
package/src/adapters/read.js
CHANGED
|
@@ -1,30 +1,29 @@
|
|
|
1
1
|
import * as fs from "node:fs/promises";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
-
import { isString
|
|
3
|
+
import { isString } from "../shared/decode.js";
|
|
4
|
+
import { readResult, asReadResult, READ_VALUE, READ_BYTES, MAX_READ_VALUE_BYTES } from "../shared/result.js";
|
|
4
5
|
import { extractStructuralSurface } from "../context/surface.js";
|
|
5
|
-
import {
|
|
6
|
-
import { executeSnap, tokenizeQuery, stem } from "../context/snap.js";
|
|
6
|
+
import { executeSnap, tokenizeQuery } from "../context/snap.js";
|
|
7
7
|
import { selectEvidence } from "../context/evidence.js";
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
jsonStringLength, maxJsonStringPrefix,
|
|
20
|
-
} from "../fs/text-ops.js";
|
|
21
|
-
import { imageTooLarge, missingFile, IMAGE_MAX_BYTES, LARGE_FILE_BYTES, ABOUT_TOKEN_MAX, IMAGE_MIME, RAW_JSON_CHARS, RAW_SOURCE_CHARS, RAW_SOURCE_LINES, ROUTING_MAX_CHARS } from "./errors.js";
|
|
22
|
-
import { outlineOptions, recordOutlineOrigins, createReferenceFinder } from "./refs.js";
|
|
23
|
-
import { sourceContext, parsePosition } from "../shared/syntax-context.js";
|
|
8
|
+
import { normalizeRead, classifyRead, needsProbe } from "../contract/read.js";
|
|
9
|
+
import { resolveWorkspacePath, relativeSlash } from "../fs/workspace.js";
|
|
10
|
+
import { normalizeReadWindow, resolveReadPath, probeExistingPath } from "../fs/text-ops.js";
|
|
11
|
+
import { createDirectoryReader } from "../fs/directory.js";
|
|
12
|
+
import { createWindowReader } from "../fs/read-window.js";
|
|
13
|
+
import { resolveSessionResource } from "../fs/session-resource.js";
|
|
14
|
+
import { ABOUT_TOKEN_MAX } from "./errors.js";
|
|
15
|
+
import { projectJson } from "./read-json.js";
|
|
16
|
+
import { createImageReader } from "./read-image.js";
|
|
17
|
+
import { createTextReader } from "./read-text.js";
|
|
18
|
+
import { createFocusedReader } from "./read-focus.js";
|
|
24
19
|
|
|
25
20
|
export function createRead(ctx) {
|
|
26
21
|
const { getCwd, vfs, config, index, ledger, hooks, reads } = ctx;
|
|
27
|
-
const
|
|
22
|
+
const readDirectory = createDirectoryReader(vfs);
|
|
23
|
+
const readWindow = createWindowReader(vfs);
|
|
24
|
+
const readTextFile = createTextReader(ctx, readWindow);
|
|
25
|
+
const maybeImage = createImageReader(vfs);
|
|
26
|
+
const focusAbout = createFocusedReader(vfs, readBudget);
|
|
28
27
|
async function sourceRead(query, searchDir, signal, params = {}) {
|
|
29
28
|
params = { ...params, resolve: params.resolve !== false };
|
|
30
29
|
const cwd = getCwd();
|
|
@@ -50,7 +49,7 @@ export function createRead(ctx) {
|
|
|
50
49
|
async function openSource(result, params, signal, resolvedPath, query) {
|
|
51
50
|
const cwd = getCwd();
|
|
52
51
|
|
|
53
|
-
if (result.status !== "found") return
|
|
52
|
+
if (result.status !== "found") return readResult(result, { isSnap: true });
|
|
54
53
|
signal?.throwIfAborted();
|
|
55
54
|
const target = resolvedPath ?? path.resolve(cwd, result.path);
|
|
56
55
|
const bounded = await sourceIsBounded(target, query, params);
|
|
@@ -68,297 +67,67 @@ export function createRead(ctx) {
|
|
|
68
67
|
function openedSource(result, params, block, details) {
|
|
69
68
|
const { firstLine, lastLine, sourceChars, nextOffset, complete } = details;
|
|
70
69
|
|
|
71
|
-
if (lastLine < firstLine) return
|
|
70
|
+
if (lastLine < firstLine) return readResult({ 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 }, { ...details, isSnap: true });
|
|
72
71
|
|
|
73
72
|
return foundSource(result, params, block, details, firstLine, lastLine, sourceChars, nextOffset, complete);
|
|
74
73
|
}
|
|
75
74
|
|
|
76
75
|
function foundSource(result, params, block, details, firstLine, lastLine, sourceChars, nextOffset, complete) {
|
|
77
76
|
const source = { status: "found", path: result.path, line: result.line, lines: [firstLine, lastLine],
|
|
78
|
-
text: block.text.slice(0, sourceChars), complete
|
|
77
|
+
text: block.text.slice(0, sourceChars), complete };
|
|
78
|
+
if (nextOffset !== undefined) source.nextOffset = nextOffset;
|
|
79
79
|
|
|
80
|
-
return
|
|
80
|
+
return readResult(params.resolve ? source : "// " + result.path + ":" + firstLine + "-" + lastLine + "\n" + block.text,
|
|
81
81
|
{ ...details, isSnap: true });
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
-
function
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
const [name, child] = relative.split(path.sep);
|
|
93
|
-
rows.set(name, child === undefined
|
|
94
|
-
? formatDirectoryEntry(name, "file", Buffer.byteLength(vfs.getOverlay(file), "utf8"))
|
|
95
|
-
: formatDirectoryEntry(name, "dir"));
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
return truncated;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
async function diskDirRows(dirPath, rows, signal) {
|
|
102
|
-
let entries;
|
|
103
|
-
|
|
104
|
-
try { entries = await fs.readdir(dirPath, { withFileTypes: true }); } catch (error) {
|
|
105
|
-
if (error.code !== "ENOENT" || rows.size === 0) throw error;
|
|
106
|
-
entries = [];
|
|
84
|
+
async function addBatchItem(state, index, raw, onItem) {
|
|
85
|
+
const bytes = raw[READ_BYTES];
|
|
86
|
+
if (!onItem) {
|
|
87
|
+
if (state.bytes + bytes > MAX_READ_VALUE_BYTES) throw new Error("batched read exceeds " + MAX_READ_VALUE_BYTES + " bytes; use individual reads");
|
|
88
|
+
} else if (!state.streamed && state.bytes + bytes > 65536) {
|
|
89
|
+
state.streamed = true;
|
|
90
|
+
const retained = state.items.splice(0);
|
|
91
|
+
await Promise.all(retained.map(async (item,i) => { if (item) await onItem(i,item); }));
|
|
107
92
|
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
if ((i & 127) === 0) signal?.throwIfAborted();
|
|
111
|
-
if (rows.size >= MAX_DIRECTORY_ENTRIES) return true;
|
|
112
|
-
if (!rows.has(entries[i].name)) rows.set(entries[i].name, await formatLsEntry(dirPath, entries[i]));
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
return false;
|
|
93
|
+
if (state.streamed) await onItem(index,raw);
|
|
94
|
+
else { state.items[index] = raw; state.bytes += bytes; }
|
|
116
95
|
}
|
|
117
96
|
|
|
118
|
-
async function
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
let truncated = overlayDirRows(dirPath, rows);
|
|
122
|
-
truncated = await diskDirRows(dirPath, rows, signal) || truncated;
|
|
123
|
-
|
|
124
|
-
const values = [...rows.values()];
|
|
125
|
-
const text = values.join("\n") + (truncated ? "\n[directory listing truncated at " + MAX_DIRECTORY_ENTRIES + " entries]" : "");
|
|
126
|
-
|
|
127
|
-
return textResult(text, { path: dirPath, directory: true, count: rows.size, entries: values, outputTruncated: truncated });
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
function overlayWindow(overlay, startLine, lineCount) {
|
|
131
|
-
const window = sliceLinesRawInfo(overlay, startLine, lineCount);
|
|
132
|
-
const satisfied = lineCount === undefined || lineCount === 0 || window.text === "" || window.count >= lineCount || window.eof;
|
|
133
|
-
|
|
134
|
-
return { text: window.text, satisfied, whole: window.whole };
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
function skipToStart(scan, bytesRead, linesSeen, startLine) {
|
|
138
|
-
let begin = 0;
|
|
139
|
-
|
|
140
|
-
while (begin < bytesRead && linesSeen < startLine - 1) if (scan[begin++] === 10) linesSeen++;
|
|
141
|
-
|
|
142
|
-
return { begin, linesSeen, started: linesSeen >= startLine - 1 };
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
function clipWanted(scan, begin, bytesRead, linesSeen, wantedLines) {
|
|
146
|
-
let end = bytesRead;
|
|
147
|
-
let done = false;
|
|
148
|
-
let doneAt = -1;
|
|
149
|
-
|
|
150
|
-
if (wantedLines === Infinity) return { end, linesSeen, done, doneAt };
|
|
151
|
-
|
|
152
|
-
for (let i = begin; i < bytesRead; i++) {
|
|
153
|
-
if (scan[i] !== 10) continue;
|
|
154
|
-
linesSeen++;
|
|
155
|
-
if (linesSeen !== wantedLines) continue;
|
|
156
|
-
end = i + 1;
|
|
157
|
-
done = true;
|
|
158
|
-
doneAt = end;
|
|
159
|
-
break;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
return { end, linesSeen, done, doneAt };
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
function consumeScanChunk(scan, bytesRead, state) {
|
|
166
|
-
let { linesSeen, started, startByte, position, startLine, wantedLines, maxBytes, collected, parts } = state;
|
|
167
|
-
let begin = 0;
|
|
168
|
-
let done = false;
|
|
169
|
-
let doneByte = -1;
|
|
170
|
-
|
|
171
|
-
if (!started) {
|
|
172
|
-
const skip = skipToStart(scan, bytesRead, linesSeen, startLine);
|
|
173
|
-
linesSeen = skip.linesSeen;
|
|
174
|
-
if (!skip.started) return { ...state, linesSeen, skip: true, done: false, doneByte: -1 };
|
|
175
|
-
started = true;
|
|
176
|
-
begin = skip.begin;
|
|
177
|
-
startByte = position + begin;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
const clip = clipWanted(scan, begin, bytesRead, linesSeen, wantedLines);
|
|
181
|
-
linesSeen = clip.linesSeen;
|
|
182
|
-
if (clip.done) { done = true; doneByte = position + clip.doneAt; }
|
|
183
|
-
collected = takeScanSlice(scan, begin, clip, position, startByte, maxBytes, collected, parts);
|
|
184
|
-
|
|
185
|
-
return { linesSeen, started, startByte, done, doneByte, collected, parts, skip: false };
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
function takeScanSlice(scan, begin, clip, position, startByte, maxBytes, collected, parts) {
|
|
189
|
-
const takeBegin = position === startByte ? begin : Math.max(0, startByte - position);
|
|
190
|
-
const takeEnd = Math.min(clip.end, takeBegin + Math.max(0, maxBytes + 1 - collected));
|
|
191
|
-
|
|
192
|
-
if (takeEnd > takeBegin) {
|
|
193
|
-
parts.push(Buffer.from(scan.subarray(takeBegin, takeEnd)));
|
|
194
|
-
collected += takeEnd - takeBegin;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
return collected;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
async function scanFileWindow(file, stat, startLine, lineCount, maxBytes, signal) {
|
|
201
|
-
const wantedLines = lineCount === undefined ? Infinity : startLine + lineCount - 1;
|
|
202
|
-
const scan = Buffer.alloc(64 * 1024);
|
|
203
|
-
const parts = [];
|
|
204
|
-
let position = 0;
|
|
205
|
-
let linesSeen = 0;
|
|
206
|
-
let started = startLine === 1;
|
|
207
|
-
let startByte = started ? 0 : -1;
|
|
208
|
-
let done = false;
|
|
209
|
-
let doneByte = -1;
|
|
210
|
-
let collected = 0;
|
|
211
|
-
|
|
212
|
-
while (position < stat.size && !done && collected <= maxBytes) {
|
|
213
|
-
signal?.throwIfAborted();
|
|
214
|
-
const { bytesRead } = await file.read(scan, 0, scan.length, position);
|
|
215
|
-
|
|
216
|
-
if (bytesRead <= 0) break;
|
|
217
|
-
const chunk = consumeScanChunk(scan, bytesRead, { linesSeen, started, startByte, position, startLine, wantedLines, maxBytes, collected, parts });
|
|
218
|
-
linesSeen = chunk.linesSeen;
|
|
219
|
-
started = chunk.started;
|
|
220
|
-
startByte = chunk.startByte;
|
|
221
|
-
done = chunk.done;
|
|
222
|
-
doneByte = chunk.doneByte;
|
|
223
|
-
collected = chunk.collected;
|
|
224
|
-
position += bytesRead;
|
|
225
|
-
if (chunk.skip) continue;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
return { parts, collected, started, startByte, done, doneByte, position };
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
async function openReadFile(targetPath) {
|
|
232
|
-
try { return await fs.open(targetPath, fs.constants.O_RDONLY | (fs.constants.O_NONBLOCK ?? 0)); }
|
|
97
|
+
async function readBatchItem(params, target, index, signal, state, onItem) {
|
|
98
|
+
let raw;
|
|
99
|
+
try { raw = asReadResult(await readSingle({...params,path:target,target:undefined},getCwd(),target,signal)); }
|
|
233
100
|
catch (error) {
|
|
234
|
-
if (error.code === "ENOENT") throw missingFile(targetPath);
|
|
235
|
-
if (error.code === "ENOTDIR") throw new Error("cannot use path: a parent component of " + targetPath + " is a file, not a directory");
|
|
236
|
-
if (error.code === "EISDIR") throw new Error("path is a directory, not a file: " + targetPath);
|
|
237
|
-
if (error.code === "EACCES" || error.code === "EPERM") throw new Error("permission denied reading " + targetPath + ": check the file mode (for example bash chmod)");
|
|
238
|
-
throw error;
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
async function finishFileWindow(targetPath, stat, startLine, scan) {
|
|
243
|
-
// A truncated window can cut a multi-byte character; a window that reached
|
|
244
|
-
// EOF must decode strictly, so a binary file cannot masquerade as text.
|
|
245
|
-
const bytes = Buffer.concat(scan.parts, scan.collected);
|
|
246
|
-
const text = scan.startByte + scan.collected >= stat.size ? decodeUtf8Strict(bytes, targetPath) : decodeUtf8Window(bytes);
|
|
247
|
-
const satisfied = (scan.done && scan.startByte + scan.collected >= scan.doneByte) || scan.startByte + scan.collected >= stat.size;
|
|
248
|
-
const whole = startLine === 1 && scan.startByte === 0 && scan.startByte + scan.collected >= stat.size;
|
|
249
|
-
await vfs.recordExpected(targetPath, stat);
|
|
250
|
-
|
|
251
|
-
return { text, satisfied, whole };
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
function emptyWindow(targetPath, stat) {
|
|
255
|
-
return { text: "", satisfied: true, whole: stat.size === 0 };
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
async function readWindow(targetPath, startLine, lineCount, maxBytes, signal) {
|
|
259
|
-
const overlay = vfs.getOverlay(targetPath);
|
|
260
|
-
|
|
261
|
-
if (overlay !== undefined) return overlayWindow(overlay, startLine, lineCount);
|
|
262
|
-
const file = await openReadFile(targetPath);
|
|
263
|
-
|
|
264
|
-
try {
|
|
265
|
-
const stat = await file.stat();
|
|
266
|
-
|
|
267
|
-
if (!stat.isFile()) throw new Error("read requires a regular file: " + targetPath);
|
|
268
|
-
if (lineCount === 0) return emptyWindow(targetPath, stat);
|
|
269
|
-
const scan = await scanFileWindow(file, stat, startLine, lineCount, maxBytes, signal);
|
|
270
|
-
|
|
271
|
-
if (!scan.started) return emptyWindow(targetPath, stat);
|
|
272
|
-
|
|
273
|
-
// Await inside the try: returning the promise directly leaves its
|
|
274
|
-
// rejection unobserved while the finally awaits file.close().
|
|
275
|
-
const window = await finishFileWindow(targetPath, stat, startLine, scan);
|
|
276
|
-
|
|
277
|
-
return window;
|
|
278
|
-
} finally {
|
|
279
|
-
await file.close();
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
async function readAdapter(params, signal) {
|
|
284
|
-
signal?.throwIfAborted();
|
|
285
|
-
params = normalizeRead(normalizeReadWindow(params));
|
|
286
|
-
const cwd = getCwd();
|
|
287
|
-
const targetParam = params.path;
|
|
288
|
-
|
|
289
|
-
if (Array.isArray(targetParam)) {
|
|
290
|
-
|
|
291
|
-
const results = await Promise.all(targetParam.map(async p => {
|
|
292
|
-
try {
|
|
293
|
-
const res = await readAdapter({ ...params, path: p, target: undefined }, signal);
|
|
294
|
-
const block = res.content?.[0];
|
|
295
|
-
const item = block?.type === "image" ? block
|
|
296
|
-
: res.details?.directory === true && Array.isArray(res.details.entries) ? res.details.entries
|
|
297
|
-
: block?.text ?? "";
|
|
298
|
-
|
|
299
|
-
return { text: item };
|
|
300
|
-
} catch (error) {
|
|
301
|
-
signal?.throwIfAborted();
|
|
302
|
-
|
|
303
|
-
return { text: `[read error: ${p}] ${error.message}`, error: { path: p, message: error.message } };
|
|
304
|
-
}
|
|
305
|
-
}));
|
|
306
|
-
|
|
307
101
|
signal?.throwIfAborted();
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
return response;
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
return reads.schedule("read", () => readSingle(params, cwd, targetParam, signal), signal);
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
function sessionUriParts(uri) {
|
|
318
|
-
const match = /^(agent|artifact):\/\/([^/?#]+)$/i.exec(uri);
|
|
319
|
-
|
|
320
|
-
if (!match) throw new Error("session resource reads support bare agent://<id> and artifact://<number>; use offset/limit for pagination");
|
|
321
|
-
const kind = match[1].toLowerCase();
|
|
322
|
-
const id = decodeURIComponent(match[2]);
|
|
323
|
-
|
|
324
|
-
if (!id || id === "." || id === ".." || (/[/\\]/u.test(id) || Array.from(id).some(char => char.charCodeAt(0) < 32)) || (kind === "artifact" && !/^\d+$/.test(id))) throw new Error("invalid session resource ID");
|
|
325
|
-
|
|
326
|
-
return { kind, id };
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
async function findArtifactFile(root, id, uri, signal) {
|
|
330
|
-
const matches = [];
|
|
331
|
-
let count = 0;
|
|
332
|
-
|
|
333
|
-
for await (const entry of await fs.opendir(root)) {
|
|
334
|
-
signal?.throwIfAborted();
|
|
335
|
-
|
|
336
|
-
if (++count > 4096) throw new Error("session artifact lookup exceeded its directory budget");
|
|
337
|
-
|
|
338
|
-
if (entry.name.startsWith(id + ".") && !entry.isDirectory()) matches.push(entry.name);
|
|
102
|
+
state.errors[index] = error.message;
|
|
103
|
+
raw = readResult(error.message,{path:target});
|
|
104
|
+
raw.isError = true;
|
|
339
105
|
}
|
|
340
|
-
|
|
341
|
-
if (matches.length !== 1) throw new Error(matches.length ? "ambiguous session artifact: " + uri : "session artifact not found: " + uri);
|
|
342
|
-
|
|
343
|
-
return matches[0];
|
|
106
|
+
await addBatchItem(state,index,raw,onItem);
|
|
344
107
|
}
|
|
345
108
|
|
|
346
|
-
async function
|
|
347
|
-
const
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
109
|
+
async function readBatch(params, signal, onItem) {
|
|
110
|
+
const state = {items:[],errors:Array(params.path.length).fill(null),bytes:0,streamed:false};
|
|
111
|
+
// Delivery/acknowledgement stays inside the same eight-operation scheduler
|
|
112
|
+
// slot as I/O. A busy guest cannot cause unbounded host/message-queue buffering.
|
|
113
|
+
const settled = await Promise.allSettled(params.path.map((target,index) =>
|
|
114
|
+
reads.schedule("read",()=>readBatchItem(params,target,index,signal,state,onItem),signal)));
|
|
115
|
+
const failed = settled.find(result=>result.status === "rejected");
|
|
116
|
+
if (failed) throw failed.reason;
|
|
351
117
|
signal?.throwIfAborted();
|
|
352
|
-
const
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
118
|
+
const response = readResult("",{count:params.path.length,batch:true,independent:params._independent===true,
|
|
119
|
+
jsonMany:Array.isArray(params.json),streamed:state.streamed,
|
|
120
|
+
items:state.streamed ? [] : state.items.map(raw=>raw[READ_VALUE]),itemErrors:state.errors,
|
|
121
|
+
errors:state.errors.flatMap((message,i)=>message ? [{path:params.path[i],message}] : [])});
|
|
122
|
+
response.isError = params._independent!==true && state.errors.some(Boolean);
|
|
123
|
+
return response;
|
|
124
|
+
}
|
|
357
125
|
|
|
358
|
-
|
|
126
|
+
async function readAdapter(params, signal, onItem) {
|
|
359
127
|
signal?.throwIfAborted();
|
|
360
|
-
|
|
361
|
-
return
|
|
128
|
+
params = normalizeRead(normalizeReadWindow(params));
|
|
129
|
+
if (Array.isArray(params.path)) return readBatch(params,signal,onItem);
|
|
130
|
+
return reads.schedule("read",async()=>asReadResult(await readSingle(params,getCwd(),params.path,signal)),signal);
|
|
362
131
|
}
|
|
363
132
|
|
|
364
133
|
async function readSingle(params, cwd, targetParam, signal) {
|
|
@@ -369,7 +138,7 @@ export function createRead(ctx) {
|
|
|
369
138
|
const snapScope = (scoped, hit) => hit?.directory ? hit.path : scoped ? resolveReadPath(cwd, params.path) : cwd;
|
|
370
139
|
const kinds = {
|
|
371
140
|
session: async () => {
|
|
372
|
-
const target = await resolveSessionResource(params.path, signal);
|
|
141
|
+
const target = await resolveSessionResource(params.path, signal, hooks);
|
|
373
142
|
|
|
374
143
|
return params.resolve
|
|
375
144
|
? openSource({ status: "found", path: params.path, line: params.offset ?? 1 }, params, signal, target)
|
|
@@ -382,7 +151,7 @@ export function createRead(ctx) {
|
|
|
382
151
|
open: () => openSource({ status: "found", path: relOf(cls.existing), line: params.offset ?? 1 }, params, signal),
|
|
383
152
|
file: () => readFile(cls.existing ? cls.existing.path : resolveReadPath(cwd, params.path), params, undefined, undefined, undefined, signal),
|
|
384
153
|
dir: () => readDirectory(cls.existing.path, signal),
|
|
385
|
-
missing: () =>
|
|
154
|
+
missing: () => readResult({ status: "not_found", path: null, line: null, signature: "", confidence: 0, context: [] }, { isSnap: true }),
|
|
386
155
|
};
|
|
387
156
|
const run = kinds[cls.kind];
|
|
388
157
|
|
|
@@ -391,359 +160,21 @@ export function createRead(ctx) {
|
|
|
391
160
|
return run();
|
|
392
161
|
}
|
|
393
162
|
|
|
394
|
-
function aboutStems(about, requireStem) {
|
|
395
|
-
const tokens = tokenizeQuery(about).tokens;
|
|
396
|
-
|
|
397
|
-
if (tokens.length > 16) throw new Error("about is too broad; use at most 16 keywords");
|
|
398
|
-
const stems = [...new Set(tokens.map(token => stem(token).slice(0, 128)))];
|
|
399
|
-
|
|
400
|
-
if (requireStem && !stems.length) throw new Error("about needs at least one searchable keyword");
|
|
401
|
-
|
|
402
|
-
return stems;
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
function overlayHits(overlay, stems, signal) {
|
|
406
|
-
const hits = [];
|
|
407
|
-
let line = 1;
|
|
408
|
-
let start = 0;
|
|
409
|
-
|
|
410
|
-
while (start <= overlay.length) {
|
|
411
|
-
signal?.throwIfAborted();
|
|
412
|
-
const newline = overlay.indexOf("\n", start);
|
|
413
|
-
const end = newline < 0 ? overlay.length : newline + 1;
|
|
414
|
-
const row = overlay.slice(start, newline < 0 ? end : newline).replace(/\r$/, "").toLowerCase();
|
|
415
|
-
|
|
416
|
-
if (stems.some(st => row.includes(st))) {
|
|
417
|
-
hits.push(line);
|
|
418
|
-
if (hits.length >= 200) break;
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
if (end === overlay.length) break;
|
|
422
|
-
start = end;
|
|
423
|
-
line++;
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
return { hits, lineCount: line };
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
function overlayWindows(rel, overlay, hits, lineCount, budget) {
|
|
430
|
-
const out = [];
|
|
431
|
-
let cursor = 1;
|
|
432
|
-
let used = 0;
|
|
433
|
-
let truncated = hits.length >= 200;
|
|
434
|
-
|
|
435
|
-
for (const hit of hits) {
|
|
436
|
-
const from = Math.max(cursor, hit - 3);
|
|
437
|
-
const first = lineStartIndex(overlay, from);
|
|
438
|
-
const last = lineTextRange(overlay, Math.min(hit + 3, lineCount)).end;
|
|
439
|
-
const body = overlay.slice(first, last);
|
|
440
|
-
|
|
441
|
-
if (used + body.length > budget) { truncated = true; break; }
|
|
442
|
-
if (out.length && from > cursor) out.push("...");
|
|
443
|
-
out.push(`// ${rel}:${from}\n${body}`);
|
|
444
|
-
used += body.length;
|
|
445
|
-
cursor = Math.max(cursor, hit + 4);
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
return { out, truncated };
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
async function focusDisk(rel, about, targetPath, stems, budget, signal) {
|
|
452
|
-
const args = ["rg", "--fixed-strings", "--ignore-case", "--line-number", "--before-context", "3", "--after-context", "3"];
|
|
453
|
-
|
|
454
|
-
for (const token of stems) args.push("-e", token);
|
|
455
|
-
args.push("--", targetPath);
|
|
456
|
-
const observed = await fs.stat(targetPath);
|
|
457
|
-
const res = await runCommand(args, { cwd: path.dirname(targetPath), timeoutMs: 15000, maxOutputChars: budget, signal });
|
|
458
|
-
|
|
459
|
-
if (res.exitCode === 0 || res.exitCode === 1) await vfs.recordExpected(targetPath, observed);
|
|
460
|
-
if (res.exitCode === 1) return textResult("// " + rel + " · no matching text\n", { path: targetPath, outputTruncated: false, complete: false });
|
|
461
|
-
if (res.exitCode !== 0) throw new Error(res.stderr.trim() || `rg exited ${res.exitCode}`);
|
|
462
|
-
const marker = res.outputTruncated ? "\n[focused read truncated; narrow about or use read(path, line, count)]" : "";
|
|
463
|
-
|
|
464
|
-
return textResult("// " + rel + " · focused text windows (not a complete file); read(path, line, count) for raw text\n" + res.stdout + marker,
|
|
465
|
-
{ path: targetPath, outputTruncated: res.outputTruncated, complete: false });
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
async function focusAbout({ rel, about, overlay, targetPath, signal }) {
|
|
469
|
-
const stems = aboutStems(about, overlay === undefined);
|
|
470
|
-
const budget = readBudget(false);
|
|
471
|
-
|
|
472
|
-
if (overlay === undefined) return focusDisk(rel, about, targetPath, stems, budget, signal);
|
|
473
|
-
const { hits, lineCount } = overlayHits(overlay, stems, signal);
|
|
474
|
-
const { out, truncated } = overlayWindows(rel, overlay, hits, lineCount, budget);
|
|
475
|
-
|
|
476
|
-
if (!out.length) return textResult("// " + rel + (hits.length
|
|
477
|
-
? " · matching text exceeds view budget; first match at line " + hits[0] + "; use read(path, line, count)\n"
|
|
478
|
-
: " · no matching staged text\n"), { path: targetPath, outputTruncated: truncated, complete: false });
|
|
479
|
-
const marker = truncated ? "\n[focused read truncated; narrow about or use read(path, line, count)]" : "";
|
|
480
|
-
|
|
481
|
-
return textResult("// " + rel + " · focused staged text windows (not a complete file)\n" + out.join("\n") + marker, { path: targetPath, outputTruncated: truncated, complete: false });
|
|
482
|
-
}
|
|
483
|
-
|
|
484
163
|
function readBudget(resolve) {
|
|
485
164
|
return Math.max(1, Math.min(config.maxCallResultChars ?? 65536, config.maxReturnChars ?? 32000) - (resolve ? 1024 : 256));
|
|
486
165
|
}
|
|
487
166
|
|
|
488
|
-
function encodeJsonParts(project, document, rel, selectors, budget) {
|
|
489
|
-
const parts = [];
|
|
490
|
-
let remaining = budget;
|
|
491
|
-
let index = 0;
|
|
492
|
-
|
|
493
|
-
try {
|
|
494
|
-
for (const value of project(document)) {
|
|
495
|
-
const encoded = JSON.stringify(value);
|
|
496
|
-
|
|
497
|
-
if (encoded.length > remaining) {
|
|
498
|
-
const routed = routingText(buildSelectionRouting(rel, selectors[index] ?? "selector", value, encoded.length));
|
|
499
|
-
remaining = Math.max(0, remaining - routed.length);
|
|
500
|
-
parts.push(routed);
|
|
501
|
-
index++;
|
|
502
|
-
continue;
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
remaining -= encoded.length;
|
|
506
|
-
parts.push(encoded);
|
|
507
|
-
index++;
|
|
508
|
-
}
|
|
509
|
-
} catch (error) {
|
|
510
|
-
throw new Error("JSON selection failed for " + rel + " (" + selectors.join(", ") + "): " + (error instanceof Error ? error.message : String(error)));
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
return parts;
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
/** RFC 8259 lets parsers ignore one leading BOM; files from Windows tools carry it. */
|
|
517
|
-
const stripBom = text => text.charCodeAt(0) === 0xfeff ? text.slice(1) : text;
|
|
518
|
-
|
|
519
|
-
async function projectJson(rel, targetPath, params) {
|
|
520
|
-
const project = jsonProjector(params.json);
|
|
521
|
-
const text = await vfs.read(targetPath, { maxBytes: MAX_JSON_BYTES, label: "JSON input" });
|
|
522
|
-
let document;
|
|
523
|
-
|
|
524
|
-
try { document = JSON.parse(stripBom(text)); }
|
|
525
|
-
catch (error) {
|
|
526
|
-
const position = parsePosition(error.message, text);
|
|
527
|
-
|
|
528
|
-
throw new Error("invalid JSON in " + rel + ": " + error.message + "; the entire document must parse before projection" + (position ? sourceContext(text, position.line, position.column) : ""));
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
const many = Array.isArray(params.json);
|
|
532
|
-
const selectors = many ? params.json.map(String) : [params.json === true ? "." : String(params.json)];
|
|
533
|
-
const parts = encodeJsonParts(project, document, rel, selectors, readBudget(false) - (many ? params.json.length + 1 : 0));
|
|
534
|
-
|
|
535
|
-
return textResult(many ? "[" + parts.join(",") + "]" : parts[0], { path: targetPath, json: true, complete: true });
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
async function readImage(rel, targetPath, mime, signal) {
|
|
539
|
-
const staged = vfs.getOverlay(targetPath);
|
|
540
|
-
|
|
541
|
-
if (staged !== undefined) {
|
|
542
|
-
const size = Buffer.byteLength(staged, "utf8");
|
|
543
|
-
|
|
544
|
-
if (size > IMAGE_MAX_BYTES) throw imageTooLarge(rel, size);
|
|
545
|
-
|
|
546
|
-
return Buffer.from(staged);
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
let file;
|
|
550
|
-
|
|
551
|
-
try { file = await fs.open(targetPath, fs.constants.O_RDONLY | (fs.constants.O_NONBLOCK ?? 0)); }
|
|
552
|
-
catch (error) {
|
|
553
|
-
if (error.code === "ENOENT") throw missingFile(targetPath);
|
|
554
|
-
throw error;
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
try {
|
|
558
|
-
const stat = await file.stat();
|
|
559
|
-
|
|
560
|
-
if (!stat.isFile()) throw new Error("image read requires a regular file: " + targetPath);
|
|
561
|
-
if (stat.size > IMAGE_MAX_BYTES) throw imageTooLarge(rel, stat.size);
|
|
562
|
-
const bytes = await file.readFile({ signal });
|
|
563
|
-
await vfs.recordExpected(targetPath, stat);
|
|
564
|
-
|
|
565
|
-
return bytes;
|
|
566
|
-
} finally { await file.close(); }
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
async function loadText(targetPath, params, query, budget, signal) {
|
|
570
|
-
const canWindow = params.complete !== true && !isString(params?.about) && !isString(query);
|
|
571
|
-
|
|
572
|
-
if (!canWindow) {
|
|
573
|
-
return { text: await vfs.read(targetPath, { maxBytes: 64 * 1024 * 1024 }), windowed: false, windowSatisfied: true, windowWhole: false };
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
const startLine = isNumber(params?.offset) ? Math.max(1, Math.floor(params.offset)) : 1;
|
|
577
|
-
const lineCount = isNumber(params?.limit) ? Math.max(0, Math.floor(params.limit)) : undefined;
|
|
578
|
-
const window = await readWindow(targetPath, startLine, lineCount, budget * 4 + 1024, signal);
|
|
579
|
-
|
|
580
|
-
return { text: window.text, windowed: true, windowSatisfied: window.satisfied, windowWhole: window.whole === true };
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
async function maybeOutline(cwd, rel, targetPath, text, params, entry) {
|
|
584
|
-
if (!isString(params?.about)) return null;
|
|
585
|
-
const outline = entry && outlineFile(entry, rel, params.about, outlineOptions(params, await referenceFinder(cwd, targetPath), config));
|
|
586
|
-
|
|
587
|
-
if (!outline) return null;
|
|
588
|
-
recordOutlineOrigins(ledger, rel, outline.text);
|
|
589
|
-
|
|
590
|
-
return textResult(outline.text, { path: targetPath, outline: true, expanded: outline.expanded, declarations: outline.declarations });
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
function resolveSpan(entry, sourceLine, query, params) {
|
|
594
|
-
if (!params.resolve || !isString(query) || !entry) return { offset: params?.offset, limit: params?.limit, viewComplete: undefined };
|
|
595
|
-
const span = pickSpan(WorkspaceIndex.spansOf(entry), { line: sourceLine, name: query });
|
|
596
|
-
|
|
597
|
-
if (!span) return { offset: params?.offset, limit: params?.limit, viewComplete: undefined };
|
|
598
|
-
const spanLines = span.end - span.start + 1;
|
|
599
|
-
|
|
600
|
-
return { offset: span.start, limit: isNumber(params.limit) ? Math.min(params.limit, spanLines) : spanLines, viewComplete: (isNumber(params.limit) ? Math.min(params.limit, spanLines) : spanLines) >= spanLines };
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
function jsonFits(sliced, budget) {
|
|
604
|
-
return jsonStringLength(sliced) <= budget;
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
function jsonRoutingResult(rel, targetPath, text) {
|
|
608
|
-
const routing = routingText(buildJsonRouting(rel, text));
|
|
609
|
-
|
|
610
|
-
if (routing.length > ROUTING_MAX_CHARS) throw new Error("routing response exceeds its budget");
|
|
611
|
-
|
|
612
|
-
return textResult(routing, { path: targetPath, routed: true, complete: false });
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
async function routeWindowedJson(rel, targetPath) {
|
|
616
|
-
try {
|
|
617
|
-
return jsonRoutingResult(rel, targetPath, await vfs.read(targetPath, { maxBytes: MAX_JSON_BYTES, label: "JSON input" }));
|
|
618
|
-
} catch { return null; }
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
async function clipToBudget(rel, targetPath, sliced, firstLine, budget, explicit, params) {
|
|
622
|
-
if (!explicit && !params.resolve && path.extname(targetPath).toLowerCase() === ".json") {
|
|
623
|
-
const routed = await routeWindowedJson(rel, targetPath);
|
|
624
|
-
|
|
625
|
-
if (routed) return routed;
|
|
626
|
-
|
|
627
|
-
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");
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
const cap = params.resolve ? maxJsonStringPrefix(sliced, budget - 160) : budget - 160;
|
|
631
|
-
|
|
632
|
-
const end = sliced.lastIndexOf("\n", cap);
|
|
633
|
-
|
|
634
|
-
if (end < 0) throw new Error(`line ${firstLine} exceeds the read budget; use bash to inspect a bounded substring`);
|
|
635
|
-
const body = sliced.slice(0, end + 1);
|
|
636
|
-
const next = firstLine + body.split("\n").length - 1;
|
|
637
|
-
ledger.recordOrigin(rel, firstLine, sourceLines(body), explicit);
|
|
638
|
-
|
|
639
|
-
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 });
|
|
640
|
-
}
|
|
641
|
-
|
|
642
167
|
function assertAbout(params) {
|
|
643
168
|
if (isString(params?.about) && tokenizeQuery(params.about).tokens.length > ABOUT_TOKEN_MAX) {
|
|
644
169
|
throw new Error("about is too broad; use at most 16 keywords");
|
|
645
170
|
}
|
|
646
171
|
}
|
|
647
172
|
|
|
648
|
-
function rawReadSelected(params) {
|
|
649
|
-
return params.json !== undefined || isString(params.about) || params.outline === true || params.complete === true
|
|
650
|
-
|| isNumber(params.offset) || isNumber(params.limit) || params.resolve === true || params.evidence === true;
|
|
651
|
-
}
|
|
652
|
-
|
|
653
|
-
function checkRawSize(rel, targetPath, loaded, params) {
|
|
654
|
-
if (rawReadSelected(params)) return null;
|
|
655
|
-
if (SESSION_URI.test(rel)) return null;
|
|
656
|
-
if (loaded.windowed && loaded.windowWhole !== true) return null;
|
|
657
|
-
const n = loaded.text.length;
|
|
658
|
-
const ext = path.extname(targetPath).toLowerCase();
|
|
659
|
-
|
|
660
|
-
if (ext === ".json" && n > RAW_JSON_CHARS) {
|
|
661
|
-
try { return jsonRoutingResult(rel, targetPath, loaded.text); }
|
|
662
|
-
catch { throw new Error("raw JSON read of " + rel + " is " + n + " chars; use json:\".field\" (or .length), offset/limit, or complete:true"); }
|
|
663
|
-
}
|
|
664
|
-
|
|
665
|
-
const lines = contentLineInfo(loaded.text).count;
|
|
666
|
-
|
|
667
|
-
if (n > RAW_SOURCE_CHARS || lines > RAW_SOURCE_LINES) {
|
|
668
|
-
const p = JSON.stringify(rel);
|
|
669
|
-
throw new Error(`raw read of ${rel} is ${lines} lines / ${n} characters; path-only limit is ${RAW_SOURCE_LINES} lines / ${RAW_SOURCE_CHARS} characters. Use read(${p}, {offset:1, limit:80}) for a window, read(${p}, {about:"keywords"}) for matches, or read(${p}, {complete:true}) for the whole file within the read budget`);
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
return null;
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
async function maybeImage(rel, targetPath, signal) {
|
|
676
|
-
const mime = IMAGE_MIME[path.extname(targetPath).toLowerCase()];
|
|
677
|
-
|
|
678
|
-
if (!mime) return null;
|
|
679
|
-
assertModelImageMime(mime);
|
|
680
|
-
const bytes = await readImage(rel, targetPath, mime, signal);
|
|
681
|
-
|
|
682
|
-
if (bytes.length > IMAGE_MAX_BYTES) throw imageTooLarge(rel, bytes.length);
|
|
683
|
-
|
|
684
|
-
return { content: [{ type: "image", mimeType: mime, data: bytes.toString("base64") }], details: { path: targetPath } };
|
|
685
|
-
}
|
|
686
|
-
|
|
687
|
-
function viewOffset(span, sourceLine, loaded, budget) {
|
|
688
|
-
if (span.offset !== undefined) return span.offset;
|
|
689
|
-
if (sourceLine && loaded.text.length > budget) return Math.max(1, sourceLine - 2);
|
|
690
|
-
|
|
691
|
-
return 1;
|
|
692
|
-
}
|
|
693
|
-
|
|
694
|
-
function viewOverBudget(sliced, loaded, budget, params) {
|
|
695
|
-
return sliced.length > budget || (params.resolve && !jsonFits(sliced, budget)) || (loaded.windowed && !loaded.windowSatisfied);
|
|
696
|
-
}
|
|
697
|
-
|
|
698
|
-
function fileView(loaded, span, sourceLine, budget, params) {
|
|
699
|
-
const offset = viewOffset(span, sourceLine, loaded, budget);
|
|
700
|
-
const firstLine = isNumber(offset) ? Math.max(1, Math.floor(offset)) : 1;
|
|
701
|
-
const sliced = loaded.windowed ? loaded.text : sliceLinesRaw(loaded.text, offset, span.limit);
|
|
702
|
-
|
|
703
|
-
return { firstLine, sliced, overBudget: viewOverBudget(sliced, loaded, budget, params) };
|
|
704
|
-
}
|
|
705
|
-
|
|
706
|
-
function assertComplete(rel, sliced, loaded, budget, params) {
|
|
707
|
-
if (params.complete === true && (sliced !== loaded.text || sliced.length > budget || (params.resolve && !jsonFits(sliced, budget)))) {
|
|
708
|
-
throw new Error(`incomplete read of ${rel}: complete:true requires the entire file within the read budget (${budget} characters). Use read(${JSON.stringify(rel)}, {offset:1, limit:80}) for a window, json:".field" for JSON reports, edit() for replacements, or bash({command,args}) with a bounded parser for large text/JSONL files`);
|
|
709
|
-
}
|
|
710
|
-
}
|
|
711
|
-
|
|
712
|
-
function textFileResult(rel, targetPath, loaded, span, view, explicit) {
|
|
713
|
-
const slicedLines = view.sliced.length <= LARGE_FILE_BYTES ? sourceLines(view.sliced) : null;
|
|
714
|
-
|
|
715
|
-
if (slicedLines) ledger.recordOrigin(rel, view.firstLine, slicedLines, explicit);
|
|
716
|
-
|
|
717
|
-
return textResult(view.sliced, { path: targetPath, firstLine: view.firstLine, lastLine: view.firstLine + (slicedLines?.length ?? contentLineInfo(view.sliced).count) - 1, sourceChars: view.sliced.length, complete: loaded.windowed ? loaded.windowWhole : view.sliced === loaded.text, viewComplete: span.viewComplete });
|
|
718
|
-
}
|
|
719
|
-
|
|
720
|
-
async function readTextFile(targetPath, params, sourceLine, rel, query, signal) {
|
|
721
|
-
const explicit = isNumber(params?.offset) || isNumber(params?.limit);
|
|
722
|
-
const budget = readBudget(params.resolve);
|
|
723
|
-
const loaded = await loadText(targetPath, params, query, budget, signal);
|
|
724
|
-
const routed = checkRawSize(rel, targetPath, loaded, params);
|
|
725
|
-
|
|
726
|
-
if (routed) return routed;
|
|
727
|
-
index.touch(rel);
|
|
728
|
-
const needsIndex = isString(params?.about) || (params.resolve && isString(query));
|
|
729
|
-
const entry = needsIndex ? WorkspaceIndex.fromText(targetPath, loaded.text) : null;
|
|
730
|
-
const outlined = await maybeOutline(getCwd(), rel, targetPath, loaded.text, params, entry);
|
|
731
|
-
|
|
732
|
-
if (outlined) return outlined;
|
|
733
|
-
const span = resolveSpan(entry, sourceLine, query, params);
|
|
734
|
-
const view = fileView(loaded, span, sourceLine, budget, params);
|
|
735
|
-
assertComplete(rel, view.sliced, loaded, budget, params);
|
|
736
|
-
|
|
737
|
-
if (view.overBudget) return await clipToBudget(rel, targetPath, view.sliced, view.firstLine, budget, explicit, params);
|
|
738
|
-
|
|
739
|
-
return textFileResult(rel, targetPath, loaded, span, view, explicit);
|
|
740
|
-
}
|
|
741
|
-
|
|
742
173
|
async function readFile(targetPath, params, sourceLine, displayPath, query, signal) {
|
|
743
174
|
const rel = displayPath ?? relativeSlash(getCwd(), targetPath);
|
|
744
175
|
assertAbout(params);
|
|
745
176
|
|
|
746
|
-
if (params.json !== undefined) return projectJson(rel, targetPath, params);
|
|
177
|
+
if (params.json !== undefined) return projectJson(rel, targetPath, params, vfs);
|
|
747
178
|
const image = await maybeImage(rel, targetPath, signal);
|
|
748
179
|
|
|
749
180
|
if (image) return image;
|
|
@@ -776,7 +207,7 @@ export function createRead(ctx) {
|
|
|
776
207
|
signal,
|
|
777
208
|
});
|
|
778
209
|
|
|
779
|
-
return
|
|
210
|
+
return readResult(res, res);
|
|
780
211
|
}
|
|
781
212
|
|
|
782
213
|
function evidenceOptions(params) {
|
|
@@ -801,7 +232,7 @@ export function createRead(ctx) {
|
|
|
801
232
|
|
|
802
233
|
for (const span of res.spans) ledger.recordOrigin(span.path, span.lines[0], span.text.split("\n"));
|
|
803
234
|
|
|
804
|
-
return
|
|
235
|
+
return readResult(res, { route: res.route, count: res.spans.length });
|
|
805
236
|
}
|
|
806
237
|
|
|
807
238
|
async function surface(params, signal) {
|
|
@@ -813,7 +244,7 @@ export function createRead(ctx) {
|
|
|
813
244
|
const ext = path.extname(target);
|
|
814
245
|
const outline = extractStructuralSurface(text, ext);
|
|
815
246
|
|
|
816
|
-
return
|
|
247
|
+
return readResult(outline, { path: target, count: outline.items.length });
|
|
817
248
|
}
|
|
818
249
|
|
|
819
250
|
return {
|