pi-readseek 0.8.24 → 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 +22 -23
- package/dist/index.ts +649 -1688
- package/package.json +2 -2
- package/prompts/def.md +4 -1
- package/prompts/digest.md +37 -0
- package/prompts/edit.md +15 -22
- package/prompts/read.md +13 -17
- package/prompts/refs.md +2 -3
- package/prompts/view.md +10 -6
- package/prompts/write.md +0 -1
- package/prompts/check.md +0 -10
- package/prompts/hover.md +0 -7
package/dist/index.ts
CHANGED
|
@@ -35,7 +35,10 @@ var REPLACEABLE_TOOL_GUIDELINES = {
|
|
|
35
35
|
};
|
|
36
36
|
var COMPACT_GUIDELINES = {
|
|
37
37
|
"read.md": [
|
|
38
|
-
"Use readSeek_read with
|
|
38
|
+
"Use readSeek_read with symbol or offset/limit to inspect large files without reading them in full."
|
|
39
|
+
],
|
|
40
|
+
"digest.md": [
|
|
41
|
+
"Use readSeek_digest for map, diagnostics, and identity facets instead of separate tools."
|
|
39
42
|
],
|
|
40
43
|
"edit.md": [
|
|
41
44
|
"Prefer set_line, replace_lines, and insert_after; use replace only when anchors are impractical."
|
|
@@ -88,34 +91,10 @@ import * as Diff from "diff";
|
|
|
88
91
|
|
|
89
92
|
// src/hashline.ts
|
|
90
93
|
import xxhashWasm from "xxhash-wasm";
|
|
91
|
-
|
|
92
|
-
// src/runtime.ts
|
|
93
|
-
function throwIfAborted(signal) {
|
|
94
|
-
if (signal?.aborted)
|
|
95
|
-
throw new Error("Operation aborted");
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
// src/confusable-hyphens.ts
|
|
99
|
-
var CONFUSABLE_HYPHENS_RE = /[\u2010\u2011\u2012\u2013\u2014\u2015\u2212\uFE63\uFF0D]/g;
|
|
100
|
-
|
|
101
|
-
// src/hashline.ts
|
|
102
|
-
class HashlineMismatchError extends Error {
|
|
103
|
-
updatedAnchors;
|
|
104
|
-
constructor(message, updatedAnchors) {
|
|
105
|
-
super(message);
|
|
106
|
-
this.name = "HashlineMismatchError";
|
|
107
|
-
this.updatedAnchors = updatedAnchors;
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
94
|
var HASH_LEN = 3;
|
|
111
95
|
var RADIX = 16;
|
|
112
96
|
var HASH_MOD = RADIX ** HASH_LEN;
|
|
113
97
|
var DICT = Array.from({ length: HASH_MOD }, (_, i) => i.toString(RADIX).padStart(HASH_LEN, "0"));
|
|
114
|
-
var HASHLINE_PREFIX_RE = /^[\s>]*\d+:[0-9a-zA-Z]{1,16}\|/;
|
|
115
|
-
var DIFF_PLUS_RE = /^\+(?!\+)/;
|
|
116
|
-
var HASH_ONLY_PREFIX_RE = /^[0-9a-f]{3}\|/;
|
|
117
|
-
var HASH_RELOCATION_WINDOW_BASE = 20;
|
|
118
|
-
var HASH_RELOCATION_WINDOW_CAP = 100;
|
|
119
98
|
var HASHLINE_STATE_KEY = Symbol.for("pi-readseek.hashlineState.v1");
|
|
120
99
|
function getHashlineState() {
|
|
121
100
|
const globalObject = globalThis;
|
|
@@ -158,734 +137,19 @@ function escapeControlCharsForDisplay(text) {
|
|
|
158
137
|
function formatHashlineDisplay(lineNumber, content) {
|
|
159
138
|
return `${lineNumber}:${computeLineHash(content)}|${escapeControlCharsForDisplay(content)}`;
|
|
160
139
|
}
|
|
161
|
-
function parseLineRef(ref) {
|
|
162
|
-
const firstLine = ref.split(/\r?\n/, 1)[0];
|
|
163
|
-
const contentMatch = firstLine.match(/^[^|]*\|(.*)$/);
|
|
164
|
-
const contentAfterPipe = contentMatch ? contentMatch[1] : undefined;
|
|
165
|
-
const cleaned = firstLine.replace(/\|.*$/, "").replace(/^[\s>]+/, "").replace(/ {2}.*$/, "").trim();
|
|
166
|
-
const normalized = cleaned.replace(/\s*:\s*/, ":");
|
|
167
|
-
const match = normalized.match(new RegExp(`^(\\d+):([0-9a-fA-F]{${HASH_LEN}})$`));
|
|
168
|
-
if (!match)
|
|
169
|
-
throw new Error(`Invalid line reference "${ref}". Expected "LINE:HASH" (e.g. "5:abc").`);
|
|
170
|
-
const line = Number.parseInt(match[1], 10);
|
|
171
|
-
if (!Number.isSafeInteger(line))
|
|
172
|
-
throw new Error(`Line number must be a safe integer, got ${match[1]} in "${ref}".`);
|
|
173
|
-
if (line < 1)
|
|
174
|
-
throw new Error(`Line number must be >= 1, got ${line} in "${ref}".`);
|
|
175
|
-
return { line, hash: match[2], content: contentAfterPipe };
|
|
176
|
-
}
|
|
177
|
-
function tokenSimilarity(a, b) {
|
|
178
|
-
const tokA = new Set(a.trim().split(/\s+/));
|
|
179
|
-
const tokB = new Set(b.trim().split(/\s+/));
|
|
180
|
-
if (tokA.size === 0 && tokB.size === 0)
|
|
181
|
-
return 1;
|
|
182
|
-
if (tokA.size === 0 || tokB.size === 0)
|
|
183
|
-
return 0;
|
|
184
|
-
let overlap = 0;
|
|
185
|
-
for (const t of tokA) {
|
|
186
|
-
if (tokB.has(t))
|
|
187
|
-
overlap++;
|
|
188
|
-
}
|
|
189
|
-
return overlap / Math.max(tokA.size, tokB.size);
|
|
190
|
-
}
|
|
191
|
-
function findSimilarLines(expectedContent, fileLines, hintLine, maxSuggestions = 3) {
|
|
192
|
-
const SCAN_WINDOW = 50;
|
|
193
|
-
const MIN_SIMILARITY = 0.3;
|
|
194
|
-
const start = Math.max(0, hintLine - 1 - SCAN_WINDOW);
|
|
195
|
-
const end = Math.min(fileLines.length, hintLine - 1 + SCAN_WINDOW + 1);
|
|
196
|
-
const candidates = [];
|
|
197
|
-
for (let i = start;i < end; i++) {
|
|
198
|
-
const content = fileLines[i];
|
|
199
|
-
if (!content.trim())
|
|
200
|
-
continue;
|
|
201
|
-
const score = tokenSimilarity(expectedContent, content);
|
|
202
|
-
if (score >= MIN_SIMILARITY) {
|
|
203
|
-
candidates.push({ line: i + 1, score, content });
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
candidates.sort((a, b) => b.score - a.score);
|
|
207
|
-
return candidates.slice(0, maxSuggestions).map((c) => {
|
|
208
|
-
const hash = computeLineHash(c.content);
|
|
209
|
-
return ` ${c.line}:${hash}|${escapeControlCharsForDisplay(c.content)}`;
|
|
210
|
-
});
|
|
211
|
-
}
|
|
212
|
-
function formatMismatchError(mismatches, fileLines, relocationWindow) {
|
|
213
|
-
const mismatchSet = new Map;
|
|
214
|
-
for (const m of mismatches)
|
|
215
|
-
mismatchSet.set(m.line, m);
|
|
216
|
-
const updatedAnchors = mismatches.map((m) => {
|
|
217
|
-
const raw = fileLines[m.line - 1] ?? "";
|
|
218
|
-
const hash = computeLineHash(raw);
|
|
219
|
-
return {
|
|
220
|
-
line: m.line,
|
|
221
|
-
hash,
|
|
222
|
-
anchor: `${m.line}:${hash}`,
|
|
223
|
-
raw,
|
|
224
|
-
display: escapeControlCharsForDisplay(raw)
|
|
225
|
-
};
|
|
226
|
-
});
|
|
227
|
-
const displayLines = new Set;
|
|
228
|
-
for (const m of mismatches) {
|
|
229
|
-
for (let i = Math.max(1, m.line - 2);i <= Math.min(fileLines.length, m.line + 2); i++) {
|
|
230
|
-
displayLines.add(i);
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
const sorted = [...displayLines].sort((a, b) => a - b);
|
|
234
|
-
const out = [
|
|
235
|
-
"Edit rejected — nothing was written. The anchor hash did not match the current file content.",
|
|
236
|
-
`${mismatches.length} line${mismatches.length > 1 ? "s have" : " has"} changed since last read. Auto-relocation checks only within ±${relocationWindow} lines of each anchor. Use the updated LINE:HASH references shown below (>>> marks changed lines).`,
|
|
237
|
-
""
|
|
238
|
-
];
|
|
239
|
-
let prev = -1;
|
|
240
|
-
for (const num of sorted) {
|
|
241
|
-
if (prev !== -1 && num > prev + 1)
|
|
242
|
-
out.push(" ...");
|
|
243
|
-
prev = num;
|
|
244
|
-
const content = fileLines[num - 1];
|
|
245
|
-
const hash = computeLineHash(content);
|
|
246
|
-
const prefix = `${num}:${hash}`;
|
|
247
|
-
out.push(mismatchSet.has(num) ? `>>> ${prefix}|${escapeControlCharsForDisplay(content)}` : ` ${prefix}|${escapeControlCharsForDisplay(content)}`);
|
|
248
|
-
}
|
|
249
|
-
const withContent = mismatches.filter((m) => m.expectedContent !== undefined);
|
|
250
|
-
if (withContent.length > 0) {
|
|
251
|
-
for (const m of withContent) {
|
|
252
|
-
const suggestions = findSimilarLines(m.expectedContent, fileLines, m.line);
|
|
253
|
-
if (suggestions.length > 0) {
|
|
254
|
-
out.push("");
|
|
255
|
-
out.push("Did you mean one of these nearby lines?");
|
|
256
|
-
out.push(...suggestions);
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
return { message: out.join(`
|
|
261
|
-
`), updatedAnchors };
|
|
262
|
-
}
|
|
263
|
-
function splitDst(dst) {
|
|
264
|
-
if (dst === "")
|
|
265
|
-
return [];
|
|
266
|
-
const normalized = dst.replace(/\r\n/g, `
|
|
267
|
-
`).replace(/\r/g, `
|
|
268
|
-
`).replace(/\n$/, "");
|
|
269
|
-
return normalized.split(`
|
|
270
|
-
`);
|
|
271
|
-
}
|
|
272
|
-
function stripNewLinePrefixes(lines) {
|
|
273
|
-
let hashCount = 0;
|
|
274
|
-
let hashOnlyCount = 0;
|
|
275
|
-
let plusCount = 0;
|
|
276
|
-
let nonEmpty = 0;
|
|
277
|
-
for (const l of lines) {
|
|
278
|
-
if (!l.length)
|
|
279
|
-
continue;
|
|
280
|
-
nonEmpty++;
|
|
281
|
-
if (HASHLINE_PREFIX_RE.test(l))
|
|
282
|
-
hashCount++;
|
|
283
|
-
else if (HASH_ONLY_PREFIX_RE.test(l))
|
|
284
|
-
hashOnlyCount++;
|
|
285
|
-
if (DIFF_PLUS_RE.test(l))
|
|
286
|
-
plusCount++;
|
|
287
|
-
}
|
|
288
|
-
if (!nonEmpty)
|
|
289
|
-
return lines;
|
|
290
|
-
const stripHash = hashCount > 0 && hashCount >= nonEmpty * 0.5;
|
|
291
|
-
const stripHashOnly = !stripHash && nonEmpty >= 2 && hashOnlyCount > 0 && hashOnlyCount >= nonEmpty * 0.5;
|
|
292
|
-
const stripPlus = !stripHash && !stripHashOnly && plusCount > 0 && plusCount >= nonEmpty * 0.5;
|
|
293
|
-
if (!stripHash && !stripHashOnly && !stripPlus)
|
|
294
|
-
return lines;
|
|
295
|
-
return lines.map((l) => stripHash ? l.replace(HASHLINE_PREFIX_RE, "") : stripHashOnly ? l.replace(HASH_ONLY_PREFIX_RE, "") : stripPlus ? l.replace(DIFF_PLUS_RE, "") : l);
|
|
296
|
-
}
|
|
297
|
-
function stripAllWhitespace(s) {
|
|
298
|
-
return s.replace(/\s+/g, "");
|
|
299
|
-
}
|
|
300
|
-
function stripTrailingContinuationTokens(s) {
|
|
301
|
-
return s.replace(/(?:&&|\|\||\?\?|\?|:|=|,|\+|-|\*|\/|\.|\()\s*$/u, "");
|
|
302
|
-
}
|
|
303
|
-
function stripMergeOperatorChars(s) {
|
|
304
|
-
return s.replace(/[|&?]/g, "");
|
|
305
|
-
}
|
|
306
|
-
function normalizeConfusableHyphensInLines(lines) {
|
|
307
|
-
return lines.map((line) => line.replace(CONFUSABLE_HYPHENS_RE, "-"));
|
|
308
|
-
}
|
|
309
|
-
function wsEq(a, b) {
|
|
310
|
-
return a === b || a.replace(/\s+/g, "") === b.replace(/\s+/g, "");
|
|
311
|
-
}
|
|
312
|
-
function restoreIndent(tpl, line) {
|
|
313
|
-
if (!line.length)
|
|
314
|
-
return line;
|
|
315
|
-
const indent = tpl.match(/^\s*/)?.[0] ?? "";
|
|
316
|
-
if (!indent.length || (line.match(/^\s*/)?.[0] ?? "").length > 0)
|
|
317
|
-
return line;
|
|
318
|
-
return indent + line;
|
|
319
|
-
}
|
|
320
|
-
function restoreIndentPaired(old, next) {
|
|
321
|
-
if (old.length !== next.length)
|
|
322
|
-
return next;
|
|
323
|
-
let changed = false;
|
|
324
|
-
const out = next.map((line, i) => {
|
|
325
|
-
const restored = restoreIndent(old[i], line);
|
|
326
|
-
if (restored !== line)
|
|
327
|
-
changed = true;
|
|
328
|
-
return restored;
|
|
329
|
-
});
|
|
330
|
-
return changed ? out : next;
|
|
331
|
-
}
|
|
332
|
-
function restoreOldWrappedLines(oldLines, newLines) {
|
|
333
|
-
if (oldLines.length === 0 || newLines.length < 2)
|
|
334
|
-
return newLines;
|
|
335
|
-
const canonToOld = new Map;
|
|
336
|
-
for (const line of oldLines) {
|
|
337
|
-
const canon = stripAllWhitespace(line);
|
|
338
|
-
const bucket = canonToOld.get(canon);
|
|
339
|
-
if (bucket)
|
|
340
|
-
bucket.count++;
|
|
341
|
-
else
|
|
342
|
-
canonToOld.set(canon, { line, count: 1 });
|
|
343
|
-
}
|
|
344
|
-
const candidates = [];
|
|
345
|
-
for (let start = 0;start < newLines.length; start++) {
|
|
346
|
-
for (let len = 2;len <= 10 && start + len <= newLines.length; len++) {
|
|
347
|
-
const span = newLines.slice(start, start + len);
|
|
348
|
-
if (span.some((line) => line.trim().length === 0))
|
|
349
|
-
continue;
|
|
350
|
-
const canonSpan = stripAllWhitespace(span.join(""));
|
|
351
|
-
const old = canonToOld.get(canonSpan);
|
|
352
|
-
if (old && old.count === 1 && canonSpan.length >= 6) {
|
|
353
|
-
candidates.push({ start, len, replacement: old.line, canon: canonSpan });
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
if (candidates.length === 0)
|
|
358
|
-
return newLines;
|
|
359
|
-
const canonCounts = new Map;
|
|
360
|
-
for (const c of candidates) {
|
|
361
|
-
canonCounts.set(c.canon, (canonCounts.get(c.canon) ?? 0) + 1);
|
|
362
|
-
}
|
|
363
|
-
const uniqueCandidates = candidates.filter((c) => (canonCounts.get(c.canon) ?? 0) === 1);
|
|
364
|
-
if (uniqueCandidates.length === 0)
|
|
365
|
-
return newLines;
|
|
366
|
-
uniqueCandidates.sort((a, b) => b.start - a.start);
|
|
367
|
-
const out = [...newLines];
|
|
368
|
-
for (const c of uniqueCandidates) {
|
|
369
|
-
out.splice(c.start, c.len, c.replacement);
|
|
370
|
-
}
|
|
371
|
-
return out;
|
|
372
|
-
}
|
|
373
|
-
function stripInsertAnchorEcho(anchorLine, dst) {
|
|
374
|
-
if (dst.length > 1 && wsEq(dst[0], anchorLine))
|
|
375
|
-
return dst.slice(1);
|
|
376
|
-
return dst;
|
|
377
|
-
}
|
|
378
|
-
function stripRangeBoundaryEcho(fileLines, start, end, dst) {
|
|
379
|
-
const count = end - start + 1;
|
|
380
|
-
if (dst.length <= 1 || dst.length <= count)
|
|
381
|
-
return dst;
|
|
382
|
-
let out = dst;
|
|
383
|
-
if (start - 2 >= 0 && wsEq(out[0], fileLines[start - 2]))
|
|
384
|
-
out = out.slice(1);
|
|
385
|
-
if (end < fileLines.length && out.length > 0 && wsEq(out[out.length - 1], fileLines[end]))
|
|
386
|
-
out = out.slice(0, -1);
|
|
387
|
-
return out;
|
|
388
|
-
}
|
|
389
|
-
function parseHashlineEditItem(edit) {
|
|
390
|
-
if ("set_line" in edit) {
|
|
391
|
-
return {
|
|
392
|
-
spec: { kind: "single", ref: parseLineRef(edit.set_line.anchor) },
|
|
393
|
-
dstLines: stripNewLinePrefixes(splitDst(edit.set_line.new_text))
|
|
394
|
-
};
|
|
395
|
-
}
|
|
396
|
-
if ("replace_lines" in edit) {
|
|
397
|
-
const start = parseLineRef(edit.replace_lines.start_anchor);
|
|
398
|
-
const end = parseLineRef(edit.replace_lines.end_anchor);
|
|
399
|
-
return {
|
|
400
|
-
spec: start.line === end.line ? { kind: "single", ref: start } : { kind: "range", start, end },
|
|
401
|
-
dstLines: stripNewLinePrefixes(splitDst(edit.replace_lines.new_text))
|
|
402
|
-
};
|
|
403
|
-
}
|
|
404
|
-
if ("insert_after" in edit) {
|
|
405
|
-
return {
|
|
406
|
-
spec: { kind: "insertAfter", after: parseLineRef(edit.insert_after.anchor) },
|
|
407
|
-
dstLines: stripNewLinePrefixes(splitDst(edit.insert_after.new_text ?? ""))
|
|
408
|
-
};
|
|
409
|
-
}
|
|
410
|
-
throw new Error("replace edits are applied separately");
|
|
411
|
-
}
|
|
412
|
-
function applyHashlineEdits(content, edits, signal) {
|
|
413
|
-
throwIfAborted(signal);
|
|
414
|
-
if (!edits.length)
|
|
415
|
-
return { content, firstChangedLine: undefined };
|
|
416
|
-
const relocationWindow = Math.min(Math.max(HASH_RELOCATION_WINDOW_BASE, edits.length * 5), HASH_RELOCATION_WINDOW_CAP);
|
|
417
|
-
const fileLines = content.split(`
|
|
418
|
-
`);
|
|
419
|
-
const origLines = [...fileLines];
|
|
420
|
-
let firstChanged;
|
|
421
|
-
const noopEdits = [];
|
|
422
|
-
const parsed = edits.map((edit, idx) => ({
|
|
423
|
-
...parseHashlineEditItem(edit),
|
|
424
|
-
idx
|
|
425
|
-
}));
|
|
426
|
-
function collectExplicitlyTouchedLines() {
|
|
427
|
-
const touched = new Set;
|
|
428
|
-
for (const { spec } of parsed) {
|
|
429
|
-
if (spec.kind === "single")
|
|
430
|
-
touched.add(spec.ref.line);
|
|
431
|
-
else if (spec.kind === "insertAfter")
|
|
432
|
-
touched.add(spec.after.line);
|
|
433
|
-
else
|
|
434
|
-
for (let line = spec.start.line;line <= spec.end.line; line++)
|
|
435
|
-
touched.add(line);
|
|
436
|
-
}
|
|
437
|
-
return touched;
|
|
438
|
-
}
|
|
439
|
-
let explicitlyTouchedLines = collectExplicitlyTouchedLines();
|
|
440
|
-
const lineHashes = [];
|
|
441
|
-
const hashToLines = new Map;
|
|
442
|
-
for (let i = 0;i < fileLines.length; i++) {
|
|
443
|
-
throwIfAborted(signal);
|
|
444
|
-
const lineNumber = i + 1;
|
|
445
|
-
const h = computeLineHash(fileLines[i]);
|
|
446
|
-
lineHashes.push(h);
|
|
447
|
-
const lines = hashToLines.get(h);
|
|
448
|
-
if (lines)
|
|
449
|
-
lines.push(lineNumber);
|
|
450
|
-
else
|
|
451
|
-
hashToLines.set(h, [lineNumber]);
|
|
452
|
-
}
|
|
453
|
-
const relocationNotes = new Set;
|
|
454
|
-
function findRelocationLine(expectedHash, hintLine, relocationWindow2) {
|
|
455
|
-
const candidates = hashToLines.get(expectedHash);
|
|
456
|
-
if (!candidates?.length)
|
|
457
|
-
return;
|
|
458
|
-
const minLine = Math.max(1, hintLine - relocationWindow2);
|
|
459
|
-
const maxLine = Math.min(fileLines.length, hintLine + relocationWindow2);
|
|
460
|
-
let match;
|
|
461
|
-
for (const candidate of candidates) {
|
|
462
|
-
if (candidate < minLine || candidate > maxLine)
|
|
463
|
-
continue;
|
|
464
|
-
if (match !== undefined)
|
|
465
|
-
return;
|
|
466
|
-
match = candidate;
|
|
467
|
-
}
|
|
468
|
-
return match;
|
|
469
|
-
}
|
|
470
|
-
const mismatches = [];
|
|
471
|
-
function validate(ref) {
|
|
472
|
-
if (ref.line < 1 || ref.line > fileLines.length)
|
|
473
|
-
throw new Error(`Line ${ref.line} does not exist (file has ${fileLines.length} lines)`);
|
|
474
|
-
const expected = ref.hash.toLowerCase();
|
|
475
|
-
const originalLine = ref.line;
|
|
476
|
-
const actual = lineHashes[originalLine - 1];
|
|
477
|
-
if (actual === expected)
|
|
478
|
-
return true;
|
|
479
|
-
const relocated = findRelocationLine(expected, originalLine, relocationWindow);
|
|
480
|
-
if (relocated !== undefined) {
|
|
481
|
-
ref.line = relocated;
|
|
482
|
-
relocationNotes.add(`Auto-relocated anchor ${originalLine}:${ref.hash} -> ${relocated}:${ref.hash} (window ±${relocationWindow}).`);
|
|
483
|
-
return true;
|
|
484
|
-
}
|
|
485
|
-
if (ref.content) {
|
|
486
|
-
const FUZZY_THRESHOLD = 0.8;
|
|
487
|
-
const FUZZY_SCAN = 50;
|
|
488
|
-
const scanStart = Math.max(0, originalLine - 1 - FUZZY_SCAN);
|
|
489
|
-
const scanEnd = Math.min(fileLines.length, originalLine - 1 + FUZZY_SCAN + 1);
|
|
490
|
-
const fuzzyHits = [];
|
|
491
|
-
for (let i = scanStart;i < scanEnd; i++) {
|
|
492
|
-
const lineContent = fileLines[i];
|
|
493
|
-
if (!lineContent.trim())
|
|
494
|
-
continue;
|
|
495
|
-
const score = tokenSimilarity(ref.content, lineContent);
|
|
496
|
-
if (score > FUZZY_THRESHOLD) {
|
|
497
|
-
fuzzyHits.push({ line: i + 1, score });
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
if (fuzzyHits.length === 1) {
|
|
501
|
-
const hit = fuzzyHits[0];
|
|
502
|
-
const newHash = computeLineHash(fileLines[hit.line - 1]);
|
|
503
|
-
ref.line = hit.line;
|
|
504
|
-
ref.hash = newHash;
|
|
505
|
-
relocationNotes.add(`Fuzzy-relocated anchor ${originalLine}:${expected} → ${hit.line}:${newHash} (similarity: ${hit.score.toFixed(2)})`);
|
|
506
|
-
return true;
|
|
507
|
-
}
|
|
508
|
-
}
|
|
509
|
-
mismatches.push({ line: originalLine, expected: ref.hash, actual, expectedContent: ref.content });
|
|
510
|
-
return false;
|
|
511
|
-
}
|
|
512
|
-
for (const { spec } of parsed) {
|
|
513
|
-
throwIfAborted(signal);
|
|
514
|
-
if (spec.kind === "single") {
|
|
515
|
-
validate(spec.ref);
|
|
516
|
-
} else if (spec.kind === "insertAfter") {
|
|
517
|
-
validate(spec.after);
|
|
518
|
-
} else {
|
|
519
|
-
if (spec.start.line > spec.end.line) {
|
|
520
|
-
throw new Error(`Range start line ${spec.start.line} must be <= end line ${spec.end.line}`);
|
|
521
|
-
}
|
|
522
|
-
const originalStart = spec.start.line;
|
|
523
|
-
const originalEnd = spec.end.line;
|
|
524
|
-
const originalCount = originalEnd - originalStart + 1;
|
|
525
|
-
const startOk = validate(spec.start);
|
|
526
|
-
const endOk = validate(spec.end);
|
|
527
|
-
if (startOk && endOk) {
|
|
528
|
-
const relocatedCount = spec.end.line - spec.start.line + 1;
|
|
529
|
-
const invalidRange = spec.start.line > spec.end.line;
|
|
530
|
-
const scopeChanged = relocatedCount !== originalCount;
|
|
531
|
-
if (invalidRange || scopeChanged) {
|
|
532
|
-
spec.start.line = originalStart;
|
|
533
|
-
spec.end.line = originalEnd;
|
|
534
|
-
mismatches.push({ line: originalStart, expected: spec.start.hash, actual: lineHashes[originalStart - 1] }, { line: originalEnd, expected: spec.end.hash, actual: lineHashes[originalEnd - 1] });
|
|
535
|
-
}
|
|
536
|
-
}
|
|
537
|
-
}
|
|
538
|
-
}
|
|
539
|
-
if (mismatches.length) {
|
|
540
|
-
const formatted = formatMismatchError(mismatches, fileLines, relocationWindow);
|
|
541
|
-
throw new HashlineMismatchError(formatted.message, formatted.updatedAnchors);
|
|
542
|
-
}
|
|
543
|
-
explicitlyTouchedLines = collectExplicitlyTouchedLines();
|
|
544
|
-
const duplicateTargetWarnings = [];
|
|
545
|
-
const warnedSingleTargets = new Set;
|
|
546
|
-
const seenSingleTargets = new Map;
|
|
547
|
-
const seenSingleEditByKey = new Map;
|
|
548
|
-
const seenNonSingleEditByKey = new Map;
|
|
549
|
-
const dupes = new Set;
|
|
550
|
-
for (let i = 0;i < parsed.length; i++) {
|
|
551
|
-
throwIfAborted(signal);
|
|
552
|
-
const p = parsed[i];
|
|
553
|
-
const lk = p.spec.kind === "single" ? `s:${p.spec.ref.line}` : p.spec.kind === "range" ? `r:${p.spec.start.line}:${p.spec.end.line}` : `i:${p.spec.after.line}`;
|
|
554
|
-
const dstKey = p.dstLines.join(`
|
|
555
|
-
`);
|
|
556
|
-
const key = `${lk}|${dstKey}`;
|
|
557
|
-
if (p.spec.kind === "single") {
|
|
558
|
-
const previousIdx = seenSingleEditByKey.get(key);
|
|
559
|
-
if (previousIdx !== undefined)
|
|
560
|
-
dupes.add(previousIdx);
|
|
561
|
-
seenSingleEditByKey.set(key, i);
|
|
562
|
-
const previousDstKey = seenSingleTargets.get(lk);
|
|
563
|
-
if (previousDstKey !== undefined && previousDstKey !== dstKey && !warnedSingleTargets.has(lk)) {
|
|
564
|
-
duplicateTargetWarnings.push(`Warning: multiple edits target the same anchor ${p.spec.ref.line}:${p.spec.ref.hash} — only the last will apply`);
|
|
565
|
-
warnedSingleTargets.add(lk);
|
|
566
|
-
}
|
|
567
|
-
seenSingleTargets.set(lk, dstKey);
|
|
568
|
-
continue;
|
|
569
|
-
}
|
|
570
|
-
if (seenNonSingleEditByKey.has(key)) {
|
|
571
|
-
dupes.add(i);
|
|
572
|
-
} else {
|
|
573
|
-
seenNonSingleEditByKey.set(key, i);
|
|
574
|
-
}
|
|
575
|
-
}
|
|
576
|
-
const deduped = parsed.filter((_, i) => !dupes.has(i));
|
|
577
|
-
const sorted = deduped.map((p) => {
|
|
578
|
-
const sl = p.spec.kind === "single" ? p.spec.ref.line : p.spec.kind === "range" ? p.spec.end.line : p.spec.after.line;
|
|
579
|
-
const pr = p.spec.kind === "insertAfter" ? 1 : 0;
|
|
580
|
-
return { ...p, sl, pr };
|
|
581
|
-
}).sort((a, b) => b.sl - a.sl || a.pr - b.pr || a.idx - b.idx);
|
|
582
|
-
function track(line) {
|
|
583
|
-
if (firstChanged === undefined || line < firstChanged)
|
|
584
|
-
firstChanged = line;
|
|
585
|
-
}
|
|
586
|
-
function maybeExpandSingleLineMerge(line, dst) {
|
|
587
|
-
if (dst.length !== 1)
|
|
588
|
-
return null;
|
|
589
|
-
if (line < 1 || line > fileLines.length)
|
|
590
|
-
return null;
|
|
591
|
-
const newLine = dst[0];
|
|
592
|
-
const newCanon = stripAllWhitespace(newLine);
|
|
593
|
-
const newCanonForMergeOps = stripMergeOperatorChars(newCanon);
|
|
594
|
-
if (!newCanon.length)
|
|
595
|
-
return null;
|
|
596
|
-
const orig = fileLines[line - 1];
|
|
597
|
-
const origCanon = stripAllWhitespace(orig);
|
|
598
|
-
const origCanonForMatch = stripTrailingContinuationTokens(origCanon);
|
|
599
|
-
const origCanonForMergeOps = stripMergeOperatorChars(origCanon);
|
|
600
|
-
const origLooksLikeContinuation = origCanonForMatch.length < origCanon.length;
|
|
601
|
-
if (!origCanon.length)
|
|
602
|
-
return null;
|
|
603
|
-
const nextIdx = line;
|
|
604
|
-
const prevIdx = line - 2;
|
|
605
|
-
if (origLooksLikeContinuation && nextIdx < fileLines.length && !explicitlyTouchedLines.has(line + 1)) {
|
|
606
|
-
const next = fileLines[nextIdx];
|
|
607
|
-
const nextCanon = stripAllWhitespace(next);
|
|
608
|
-
const a = newCanon.indexOf(origCanonForMatch);
|
|
609
|
-
const b = newCanon.indexOf(nextCanon);
|
|
610
|
-
if (a !== -1 && b !== -1 && a < b && newCanon.length <= origCanon.length + nextCanon.length + 32) {
|
|
611
|
-
return { startLine: line, deleteCount: 2, newLines: [newLine] };
|
|
612
|
-
}
|
|
613
|
-
}
|
|
614
|
-
if (prevIdx >= 0 && !explicitlyTouchedLines.has(line - 1)) {
|
|
615
|
-
const prev = fileLines[prevIdx];
|
|
616
|
-
const prevCanon = stripAllWhitespace(prev);
|
|
617
|
-
const prevCanonForMatch = stripTrailingContinuationTokens(prevCanon);
|
|
618
|
-
const prevLooksLikeContinuation = prevCanonForMatch.length < prevCanon.length;
|
|
619
|
-
if (!prevLooksLikeContinuation)
|
|
620
|
-
return null;
|
|
621
|
-
const a = newCanonForMergeOps.indexOf(stripMergeOperatorChars(prevCanonForMatch));
|
|
622
|
-
const b = newCanonForMergeOps.indexOf(origCanonForMergeOps);
|
|
623
|
-
if (a !== -1 && b !== -1 && a < b && newCanon.length <= prevCanon.length + origCanon.length + 32) {
|
|
624
|
-
return { startLine: line - 1, deleteCount: 2, newLines: [newLine] };
|
|
625
|
-
}
|
|
626
|
-
}
|
|
627
|
-
return null;
|
|
628
|
-
}
|
|
629
|
-
for (const { spec, dstLines, idx } of sorted) {
|
|
630
|
-
throwIfAborted(signal);
|
|
631
|
-
if (spec.kind === "single") {
|
|
632
|
-
const merged = maybeExpandSingleLineMerge(spec.ref.line, dstLines);
|
|
633
|
-
if (merged) {
|
|
634
|
-
const orig2 = origLines.slice(merged.startLine - 1, merged.startLine - 1 + merged.deleteCount);
|
|
635
|
-
let newL2 = restoreIndentPaired([orig2[0] ?? ""], merged.newLines);
|
|
636
|
-
if (orig2.join(`
|
|
637
|
-
`) === newL2.join(`
|
|
638
|
-
`) && orig2.some((line) => CONFUSABLE_HYPHENS_RE.test(line))) {
|
|
639
|
-
newL2 = normalizeConfusableHyphensInLines(newL2);
|
|
640
|
-
}
|
|
641
|
-
if (orig2.join(`
|
|
642
|
-
`) === newL2.join(`
|
|
643
|
-
`)) {
|
|
644
|
-
noopEdits.push({ editIndex: idx, loc: `${spec.ref.line}:${spec.ref.hash}`, currentContent: orig2.join(`
|
|
645
|
-
`) });
|
|
646
|
-
continue;
|
|
647
|
-
}
|
|
648
|
-
fileLines.splice(merged.startLine - 1, merged.deleteCount, ...newL2);
|
|
649
|
-
track(merged.startLine);
|
|
650
|
-
continue;
|
|
651
|
-
}
|
|
652
|
-
const orig = origLines.slice(spec.ref.line - 1, spec.ref.line);
|
|
653
|
-
let stripped = stripRangeBoundaryEcho(origLines, spec.ref.line, spec.ref.line, dstLines);
|
|
654
|
-
stripped = restoreOldWrappedLines(orig, stripped);
|
|
655
|
-
let newL = restoreIndentPaired(orig, stripped);
|
|
656
|
-
if (orig.join(`
|
|
657
|
-
`) === newL.join(`
|
|
658
|
-
`) && orig.some((line) => CONFUSABLE_HYPHENS_RE.test(line))) {
|
|
659
|
-
newL = normalizeConfusableHyphensInLines(newL);
|
|
660
|
-
}
|
|
661
|
-
if (orig.length === newL.length && orig.join(`
|
|
662
|
-
`) === newL.join(`
|
|
663
|
-
`)) {
|
|
664
|
-
noopEdits.push({ editIndex: idx, loc: `${spec.ref.line}:${spec.ref.hash}`, currentContent: orig.join(`
|
|
665
|
-
`) });
|
|
666
|
-
continue;
|
|
667
|
-
}
|
|
668
|
-
fileLines.splice(spec.ref.line - 1, 1, ...newL);
|
|
669
|
-
track(spec.ref.line);
|
|
670
|
-
} else if (spec.kind === "range") {
|
|
671
|
-
const count = spec.end.line - spec.start.line + 1;
|
|
672
|
-
const orig = origLines.slice(spec.start.line - 1, spec.start.line - 1 + count);
|
|
673
|
-
let stripped = stripRangeBoundaryEcho(origLines, spec.start.line, spec.end.line, dstLines);
|
|
674
|
-
stripped = restoreOldWrappedLines(orig, stripped);
|
|
675
|
-
let newL = restoreIndentPaired(orig, stripped);
|
|
676
|
-
if (orig.join(`
|
|
677
|
-
`) === newL.join(`
|
|
678
|
-
`) && orig.some((line) => CONFUSABLE_HYPHENS_RE.test(line))) {
|
|
679
|
-
newL = normalizeConfusableHyphensInLines(newL);
|
|
680
|
-
}
|
|
681
|
-
if (orig.length === newL.length && orig.join(`
|
|
682
|
-
`) === newL.join(`
|
|
683
|
-
`)) {
|
|
684
|
-
noopEdits.push({ editIndex: idx, loc: `${spec.start.line}:${spec.start.hash}`, currentContent: orig.join(`
|
|
685
|
-
`) });
|
|
686
|
-
continue;
|
|
687
|
-
}
|
|
688
|
-
fileLines.splice(spec.start.line - 1, count, ...newL);
|
|
689
|
-
track(spec.start.line);
|
|
690
|
-
} else {
|
|
691
|
-
const anchor = origLines[spec.after.line - 1];
|
|
692
|
-
const inserted = stripInsertAnchorEcho(anchor, dstLines);
|
|
693
|
-
if (!inserted.length) {
|
|
694
|
-
noopEdits.push({ editIndex: idx, loc: `${spec.after.line}:${spec.after.hash}`, currentContent: anchor });
|
|
695
|
-
continue;
|
|
696
|
-
}
|
|
697
|
-
if (content === "" && spec.after.line === 1 && anchor === "") {
|
|
698
|
-
fileLines.splice(0, 1, ...inserted);
|
|
699
|
-
track(1);
|
|
700
|
-
continue;
|
|
701
|
-
}
|
|
702
|
-
fileLines.splice(spec.after.line, 0, ...inserted);
|
|
703
|
-
track(spec.after.line + 1);
|
|
704
|
-
}
|
|
705
|
-
}
|
|
706
|
-
const warnings = [...relocationNotes, ...duplicateTargetWarnings];
|
|
707
|
-
let diff = Math.abs(fileLines.length - origLines.length);
|
|
708
|
-
for (let i = 0;i < Math.min(fileLines.length, origLines.length); i++) {
|
|
709
|
-
if (fileLines[i] !== origLines[i])
|
|
710
|
-
diff++;
|
|
711
|
-
}
|
|
712
|
-
if (diff > edits.length * 4) {
|
|
713
|
-
warnings.push(`Edit changed ${diff} lines across ${edits.length} operations — verify no unintended reformatting.`);
|
|
714
|
-
}
|
|
715
|
-
return {
|
|
716
|
-
content: fileLines.join(`
|
|
717
|
-
`),
|
|
718
|
-
firstChangedLine: firstChanged,
|
|
719
|
-
...warnings.length ? { warnings } : {},
|
|
720
|
-
...noopEdits.length ? { noopEdits } : {}
|
|
721
|
-
};
|
|
722
|
-
}
|
|
723
140
|
|
|
724
141
|
// src/edit-diff.ts
|
|
725
|
-
function detectLineEnding(content) {
|
|
726
|
-
let crlf = 0;
|
|
727
|
-
let lf = 0;
|
|
728
|
-
for (let i = 0;i < content.length; i++) {
|
|
729
|
-
if (content[i] === "\r" && i + 1 < content.length && content[i + 1] === `
|
|
730
|
-
`) {
|
|
731
|
-
crlf++;
|
|
732
|
-
i++;
|
|
733
|
-
} else if (content[i] === `
|
|
734
|
-
`) {
|
|
735
|
-
lf++;
|
|
736
|
-
}
|
|
737
|
-
}
|
|
738
|
-
if (lf === 0 && crlf === 0)
|
|
739
|
-
return `
|
|
740
|
-
`;
|
|
741
|
-
if (lf === 0)
|
|
742
|
-
return `\r
|
|
743
|
-
`;
|
|
744
|
-
if (crlf === 0)
|
|
745
|
-
return `
|
|
746
|
-
`;
|
|
747
|
-
return crlf > lf ? `\r
|
|
748
|
-
` : `
|
|
749
|
-
`;
|
|
750
|
-
}
|
|
751
142
|
function normalizeToLF(text) {
|
|
752
143
|
return text.replace(/\r\n/g, `
|
|
753
144
|
`).replace(/\r/g, `
|
|
754
145
|
`);
|
|
755
146
|
}
|
|
756
|
-
function restoreLineEndings(text, ending) {
|
|
757
|
-
return ending === `\r
|
|
758
|
-
` ? text.replace(/\n/g, `\r
|
|
759
|
-
`) : text;
|
|
760
|
-
}
|
|
761
147
|
function stripBom(content) {
|
|
762
148
|
return content.startsWith("\uFEFF") ? { bom: "\uFEFF", text: content.slice(1) } : { bom: "", text: content };
|
|
763
149
|
}
|
|
764
150
|
function hasBareCarriageReturn(content) {
|
|
765
151
|
return content.replace(/\r\n/g, "").includes("\r");
|
|
766
152
|
}
|
|
767
|
-
var SINGLE_QUOTES_RE = /[\u2018\u2019\u201A\u201B]/g;
|
|
768
|
-
var DOUBLE_QUOTES_RE = /[\u201C\u201D\u201E\u201F]/g;
|
|
769
|
-
var UNICODE_SPACES_RE = /[\u00A0\u2002-\u200A\u202F\u205F\u3000]/g;
|
|
770
|
-
function normalizeFuzzyChar(ch) {
|
|
771
|
-
return ch.replace(SINGLE_QUOTES_RE, "'").replace(DOUBLE_QUOTES_RE, '"').replace(CONFUSABLE_HYPHENS_RE, "-").replace(UNICODE_SPACES_RE, " ");
|
|
772
|
-
}
|
|
773
|
-
function normalizeForFuzzyMatch(text) {
|
|
774
|
-
return text.split(`
|
|
775
|
-
`).map((line) => line.trimEnd()).join(`
|
|
776
|
-
`).replace(SINGLE_QUOTES_RE, "'").replace(DOUBLE_QUOTES_RE, '"').replace(CONFUSABLE_HYPHENS_RE, "-").replace(UNICODE_SPACES_RE, " ");
|
|
777
|
-
}
|
|
778
|
-
function buildNormalizedWithMap(text) {
|
|
779
|
-
const lines = text.split(`
|
|
780
|
-
`);
|
|
781
|
-
const normalizedChars = [];
|
|
782
|
-
const indexMap = [];
|
|
783
|
-
let originalOffset = 0;
|
|
784
|
-
for (let i = 0;i < lines.length; i++) {
|
|
785
|
-
const line = lines[i];
|
|
786
|
-
const trimmed = line.replace(/\s+$/u, "");
|
|
787
|
-
for (let j = 0;j < trimmed.length; j++) {
|
|
788
|
-
normalizedChars.push(normalizeFuzzyChar(trimmed[j]));
|
|
789
|
-
indexMap.push(originalOffset + j);
|
|
790
|
-
}
|
|
791
|
-
if (i < lines.length - 1) {
|
|
792
|
-
normalizedChars.push(`
|
|
793
|
-
`);
|
|
794
|
-
indexMap.push(originalOffset + line.length);
|
|
795
|
-
}
|
|
796
|
-
originalOffset += line.length + 1;
|
|
797
|
-
}
|
|
798
|
-
return { normalized: normalizedChars.join(""), indexMap };
|
|
799
|
-
}
|
|
800
|
-
function mapNormalizedSpanToOriginal(indexMap, normalizedStart, normalizedLength) {
|
|
801
|
-
if (normalizedStart < 0 || normalizedLength <= 0)
|
|
802
|
-
return null;
|
|
803
|
-
const normalizedEnd = normalizedStart + normalizedLength;
|
|
804
|
-
if (normalizedEnd > indexMap.length)
|
|
805
|
-
return null;
|
|
806
|
-
const start = indexMap[normalizedStart];
|
|
807
|
-
const end = indexMap[normalizedEnd - 1];
|
|
808
|
-
if (start === undefined || end === undefined || end < start)
|
|
809
|
-
return null;
|
|
810
|
-
return { index: start, matchLength: end - start + 1 };
|
|
811
|
-
}
|
|
812
|
-
function fuzzyFindText(content, oldText) {
|
|
813
|
-
const exactIndex = content.indexOf(oldText);
|
|
814
|
-
if (exactIndex !== -1) {
|
|
815
|
-
return { found: true, index: exactIndex, matchLength: oldText.length, usedFuzzyMatch: false };
|
|
816
|
-
}
|
|
817
|
-
const normalizedNeedle = normalizeForFuzzyMatch(oldText);
|
|
818
|
-
if (!normalizedNeedle.length)
|
|
819
|
-
return { found: false, index: -1, matchLength: 0, usedFuzzyMatch: false };
|
|
820
|
-
const { normalized, indexMap } = buildNormalizedWithMap(content);
|
|
821
|
-
const normalizedIndex = normalized.indexOf(normalizedNeedle);
|
|
822
|
-
if (normalizedIndex === -1) {
|
|
823
|
-
return { found: false, index: -1, matchLength: 0, usedFuzzyMatch: false };
|
|
824
|
-
}
|
|
825
|
-
const mapped = mapNormalizedSpanToOriginal(indexMap, normalizedIndex, normalizedNeedle.length);
|
|
826
|
-
if (!mapped) {
|
|
827
|
-
return { found: false, index: -1, matchLength: 0, usedFuzzyMatch: false };
|
|
828
|
-
}
|
|
829
|
-
return { found: true, index: mapped.index, matchLength: mapped.matchLength, usedFuzzyMatch: true };
|
|
830
|
-
}
|
|
831
|
-
function replaceText(content, oldText, newText, opts) {
|
|
832
|
-
if (!oldText.length)
|
|
833
|
-
return { content, count: 0, usedFuzzyMatch: false };
|
|
834
|
-
const normalizedNew = normalizeToLF(newText);
|
|
835
|
-
if (opts.all) {
|
|
836
|
-
const exactCount = content.split(oldText).length - 1;
|
|
837
|
-
if (exactCount > 0) {
|
|
838
|
-
return { content: content.split(oldText).join(normalizedNew), count: exactCount, usedFuzzyMatch: false };
|
|
839
|
-
}
|
|
840
|
-
if (!opts.fuzzy)
|
|
841
|
-
return { content, count: 0, usedFuzzyMatch: false };
|
|
842
|
-
const normalizedNeedle = normalizeForFuzzyMatch(oldText);
|
|
843
|
-
if (!normalizedNeedle.length)
|
|
844
|
-
return { content, count: 0, usedFuzzyMatch: false };
|
|
845
|
-
const { normalized, indexMap } = buildNormalizedWithMap(content);
|
|
846
|
-
const spans = [];
|
|
847
|
-
let searchFrom = 0;
|
|
848
|
-
while (searchFrom <= normalized.length - normalizedNeedle.length) {
|
|
849
|
-
const pos = normalized.indexOf(normalizedNeedle, searchFrom);
|
|
850
|
-
if (pos === -1)
|
|
851
|
-
break;
|
|
852
|
-
const mapped = mapNormalizedSpanToOriginal(indexMap, pos, normalizedNeedle.length);
|
|
853
|
-
if (mapped) {
|
|
854
|
-
const prev = spans[spans.length - 1];
|
|
855
|
-
if (!prev || mapped.index >= prev.index + prev.matchLength) {
|
|
856
|
-
spans.push(mapped);
|
|
857
|
-
}
|
|
858
|
-
}
|
|
859
|
-
searchFrom = pos + Math.max(1, normalizedNeedle.length);
|
|
860
|
-
}
|
|
861
|
-
if (!spans.length)
|
|
862
|
-
return { content, count: 0, usedFuzzyMatch: false };
|
|
863
|
-
let out = content;
|
|
864
|
-
for (let i = spans.length - 1;i >= 0; i--) {
|
|
865
|
-
const span = spans[i];
|
|
866
|
-
out = out.substring(0, span.index) + normalizedNew + out.substring(span.index + span.matchLength);
|
|
867
|
-
}
|
|
868
|
-
return { content: out, count: spans.length, usedFuzzyMatch: true };
|
|
869
|
-
}
|
|
870
|
-
const exactIndex = content.indexOf(oldText);
|
|
871
|
-
if (exactIndex !== -1) {
|
|
872
|
-
return {
|
|
873
|
-
content: content.substring(0, exactIndex) + normalizedNew + content.substring(exactIndex + oldText.length),
|
|
874
|
-
count: 1,
|
|
875
|
-
usedFuzzyMatch: false
|
|
876
|
-
};
|
|
877
|
-
}
|
|
878
|
-
if (!opts.fuzzy)
|
|
879
|
-
return { content, count: 0, usedFuzzyMatch: false };
|
|
880
|
-
const result = fuzzyFindText(content, oldText);
|
|
881
|
-
if (!result.found)
|
|
882
|
-
return { content, count: 0, usedFuzzyMatch: false };
|
|
883
|
-
return {
|
|
884
|
-
content: content.substring(0, result.index) + normalizedNew + content.substring(result.index + result.matchLength),
|
|
885
|
-
count: 1,
|
|
886
|
-
usedFuzzyMatch: result.usedFuzzyMatch
|
|
887
|
-
};
|
|
888
|
-
}
|
|
889
153
|
function generateDiffString(oldContent, newContent, contextLines = 4) {
|
|
890
154
|
const parts = Diff.diffLines(oldContent, newContent);
|
|
891
155
|
const output = [];
|
|
@@ -1121,6 +385,12 @@ function resolveToCwd(filePath, cwd) {
|
|
|
1121
385
|
return isAbsolute(expanded) ? expanded : resolvePath(cwd, expanded);
|
|
1122
386
|
}
|
|
1123
387
|
|
|
388
|
+
// src/runtime.ts
|
|
389
|
+
function throwIfAborted(signal) {
|
|
390
|
+
if (signal?.aborted)
|
|
391
|
+
throw new Error("Operation aborted");
|
|
392
|
+
}
|
|
393
|
+
|
|
1124
394
|
// src/fs-error.ts
|
|
1125
395
|
var SYSLOG_PATH_SUFFIX = /,\s+[a-z]+(\s+'.*')?$/;
|
|
1126
396
|
function strerror(err) {
|
|
@@ -1182,7 +452,7 @@ var READSEEK_TOOL_REPLACEMENTS = {
|
|
|
1182
452
|
grep: "readSeek_grep",
|
|
1183
453
|
write: "readSeek_write"
|
|
1184
454
|
};
|
|
1185
|
-
var READSEEK_KEYS = ["
|
|
455
|
+
var READSEEK_KEYS = ["overrideTools", "imageMode", "syntaxValidation", "timeoutMs", "grep", "display"];
|
|
1186
456
|
var READSEEK_GREP_KEYS = ["maxLines", "maxBytes"];
|
|
1187
457
|
var READSEEK_DISPLAY_KEYS = ["read", "grep", "edit", "write"];
|
|
1188
458
|
var DEFAULT_DISPLAY_MODES = {
|
|
@@ -1238,12 +508,12 @@ function readImageMode(raw, source, warnings) {
|
|
|
1238
508
|
warnings.push(invalid(source, "readseek.imageMode"));
|
|
1239
509
|
return;
|
|
1240
510
|
}
|
|
1241
|
-
function
|
|
1242
|
-
if (!("
|
|
511
|
+
function readOverrideTools(raw, source, warnings) {
|
|
512
|
+
if (!("overrideTools" in raw))
|
|
1243
513
|
return;
|
|
1244
|
-
const value = raw.
|
|
514
|
+
const value = raw.overrideTools;
|
|
1245
515
|
if (!Array.isArray(value)) {
|
|
1246
|
-
warnings.push(invalid(source, "readseek.
|
|
516
|
+
warnings.push(invalid(source, "readseek.overrideTools"));
|
|
1247
517
|
return;
|
|
1248
518
|
}
|
|
1249
519
|
const tools = [];
|
|
@@ -1251,7 +521,7 @@ function readReplacedTools(raw, source, warnings) {
|
|
|
1251
521
|
if (typeof tool === "string" && Object.hasOwn(READSEEK_TOOL_REPLACEMENTS, tool)) {
|
|
1252
522
|
tools.push(tool);
|
|
1253
523
|
} else {
|
|
1254
|
-
warnings.push(invalid(source, `readseek.
|
|
524
|
+
warnings.push(invalid(source, `readseek.overrideTools[${index}]`));
|
|
1255
525
|
}
|
|
1256
526
|
});
|
|
1257
527
|
return tools;
|
|
@@ -1296,9 +566,9 @@ function validateSettings(raw, source) {
|
|
|
1296
566
|
}
|
|
1297
567
|
const section = raw.readseek;
|
|
1298
568
|
warnUnknownKeys(section, READSEEK_KEYS, "readseek", source, warnings);
|
|
1299
|
-
const
|
|
1300
|
-
if (
|
|
1301
|
-
settings.
|
|
569
|
+
const overrideTools = readOverrideTools(section, source, warnings);
|
|
570
|
+
if (overrideTools !== undefined)
|
|
571
|
+
settings.overrideTools = overrideTools;
|
|
1302
572
|
const imageMode = readImageMode(section, source, warnings);
|
|
1303
573
|
if (imageMode !== undefined)
|
|
1304
574
|
settings.imageMode = imageMode;
|
|
@@ -1398,6 +668,9 @@ function appendGitSelection(args, options) {
|
|
|
1398
668
|
if (selection.length > 0)
|
|
1399
669
|
args.push("--git", selection.join(","));
|
|
1400
670
|
}
|
|
671
|
+
function lineLocationSelector(line, column) {
|
|
672
|
+
return column === undefined ? `line:${line}` : `line:${line}:${column}`;
|
|
673
|
+
}
|
|
1401
674
|
function normalizeLanguage(language) {
|
|
1402
675
|
return language === "java" ? "Java" : language;
|
|
1403
676
|
}
|
|
@@ -1458,6 +731,8 @@ function readSeekPlatform() {
|
|
|
1458
731
|
return `${process.platform}-${process.arch}`;
|
|
1459
732
|
}
|
|
1460
733
|
function readSeekBinaryPath() {
|
|
734
|
+
if (process.env.READSEEK_BIN)
|
|
735
|
+
return process.env.READSEEK_BIN;
|
|
1461
736
|
const platform = readSeekPlatform();
|
|
1462
737
|
const platformPackage = READSEEK_PLATFORM_PACKAGES[platform];
|
|
1463
738
|
if (!platformPackage) {
|
|
@@ -1553,13 +828,19 @@ async function spawnReadSeekRaw(args, options = {}) {
|
|
|
1553
828
|
};
|
|
1554
829
|
const stdin = options.stdin;
|
|
1555
830
|
const stdio = [stdin === undefined ? "ignore" : "pipe", "pipe", "pipe"];
|
|
1556
|
-
const
|
|
1557
|
-
const
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
831
|
+
const cancelable = options.cancelable !== false;
|
|
832
|
+
const child = spawn(readSeekBinaryPath(), args, {
|
|
833
|
+
stdio,
|
|
834
|
+
signal: cancelable ? options.signal : undefined
|
|
835
|
+
});
|
|
836
|
+
if (cancelable) {
|
|
837
|
+
const timeoutMs = options.timeoutMs ?? readSeekTimeoutMs();
|
|
838
|
+
timeout = setTimeout(() => {
|
|
839
|
+
child.kill("SIGKILL");
|
|
840
|
+
fail(new Error(`readseek timed out after ${timeoutMs} ms`));
|
|
841
|
+
}, timeoutMs);
|
|
842
|
+
timeout.unref?.();
|
|
843
|
+
}
|
|
1563
844
|
const childStdout = child.stdout;
|
|
1564
845
|
const childStderr = child.stderr;
|
|
1565
846
|
const childStdin = child.stdin;
|
|
@@ -1644,10 +925,15 @@ async function readSeekView(filePath, options = {}) {
|
|
|
1644
925
|
args.push("--depth", String(options.depth));
|
|
1645
926
|
if (options.outline)
|
|
1646
927
|
args.push("--outline");
|
|
1647
|
-
|
|
928
|
+
if (options.visionMode !== undefined)
|
|
929
|
+
args.push("--vision-mode", options.visionMode);
|
|
930
|
+
if (options.visionLevel !== undefined)
|
|
931
|
+
args.push("--vision-level", options.visionLevel);
|
|
932
|
+
const runOptions = { signal: options.signal };
|
|
933
|
+
return options.visionMode !== undefined ? runReadSeekVisionRaw(args, runOptions) : runReadSeekRaw(args, runOptions);
|
|
1648
934
|
}
|
|
1649
935
|
var visionInvocationTail = Promise.resolve();
|
|
1650
|
-
async function
|
|
936
|
+
async function runReadSeekVisionRaw(args, options = {}) {
|
|
1651
937
|
let release;
|
|
1652
938
|
const gate = new Promise((resolve) => {
|
|
1653
939
|
release = resolve;
|
|
@@ -1676,7 +962,7 @@ async function runReadSeekVision(args, options = {}) {
|
|
|
1676
962
|
await predecessor;
|
|
1677
963
|
}
|
|
1678
964
|
signal?.throwIfAborted();
|
|
1679
|
-
return await
|
|
965
|
+
return await runReadSeekRaw(args, {
|
|
1680
966
|
...options,
|
|
1681
967
|
timeoutMs: options.timeoutMs ?? resolveReadSeekTimeoutMs() ?? DEFAULT_READSEEK_VISION_TIMEOUT_MS
|
|
1682
968
|
});
|
|
@@ -1684,6 +970,9 @@ async function runReadSeekVision(args, options = {}) {
|
|
|
1684
970
|
release();
|
|
1685
971
|
}
|
|
1686
972
|
}
|
|
973
|
+
async function runReadSeekVision(args, options = {}) {
|
|
974
|
+
return JSON.parse(await runReadSeekVisionRaw(args, options));
|
|
975
|
+
}
|
|
1687
976
|
function requireNumber(value, field) {
|
|
1688
977
|
if (typeof value !== "number" || !Number.isSafeInteger(value))
|
|
1689
978
|
throw new Error(`invalid readseek ${field}: expected safe integer`);
|
|
@@ -1808,39 +1097,6 @@ function parseSearchOutput(value) {
|
|
|
1808
1097
|
})
|
|
1809
1098
|
};
|
|
1810
1099
|
}
|
|
1811
|
-
async function readSeekRead(filePath, startLine, endLine, options = {}) {
|
|
1812
|
-
const args = ["read", startLine === undefined ? filePath : `${filePath}:${startLine}`];
|
|
1813
|
-
if (endLine !== undefined)
|
|
1814
|
-
args.push("--end", String(endLine));
|
|
1815
|
-
return parseReadOutput(await runReadSeek(args, { signal: options.signal }));
|
|
1816
|
-
}
|
|
1817
|
-
function fileMapFromReadSeekOutput(output, filePath, totalBytes) {
|
|
1818
|
-
if (output.language === "unknown" && output.symbols.length === 0)
|
|
1819
|
-
return null;
|
|
1820
|
-
return {
|
|
1821
|
-
path: filePath,
|
|
1822
|
-
totalLines: output.line_count,
|
|
1823
|
-
totalBytes,
|
|
1824
|
-
language: normalizeLanguage(output.language),
|
|
1825
|
-
detailLevel: "full" /* Full */,
|
|
1826
|
-
symbols: symbolsFromReadSeek(output.symbols)
|
|
1827
|
-
};
|
|
1828
|
-
}
|
|
1829
|
-
async function readSeekMap(filePath, totalBytes, options = {}) {
|
|
1830
|
-
const output = parseMapOutput(await runReadSeek(["map", filePath], { signal: options.signal }));
|
|
1831
|
-
return fileMapFromReadSeekOutput(output, filePath, totalBytes);
|
|
1832
|
-
}
|
|
1833
|
-
async function readSeekSearch(target, pattern, options = {}) {
|
|
1834
|
-
const args = ["search", target, pattern];
|
|
1835
|
-
if (options.language)
|
|
1836
|
-
args.push("--language", options.language);
|
|
1837
|
-
appendGitSelection(args, options);
|
|
1838
|
-
return parseSearchOutput(await runReadSeek(args, { signal: options.signal })).results;
|
|
1839
|
-
}
|
|
1840
|
-
async function readSeekMapContent(filePath, content, options = {}) {
|
|
1841
|
-
const output = parseMapOutput(await runReadSeek(["map", `stdin:${filePath}`], { signal: options.signal, stdin: content }));
|
|
1842
|
-
return fileMapFromReadSeekOutput(output, filePath, Buffer.byteLength(content, "utf8"));
|
|
1843
|
-
}
|
|
1844
1100
|
function optionalString(value, field) {
|
|
1845
1101
|
if (value === undefined || value === null)
|
|
1846
1102
|
return;
|
|
@@ -1870,19 +1126,6 @@ function parseRefsOutput(value) {
|
|
|
1870
1126
|
})
|
|
1871
1127
|
};
|
|
1872
1128
|
}
|
|
1873
|
-
async function readSeekRefs(target, name, options = {}) {
|
|
1874
|
-
const args = ["refs", target, name];
|
|
1875
|
-
if (options.scope)
|
|
1876
|
-
args.push("--scope");
|
|
1877
|
-
if (options.line !== undefined)
|
|
1878
|
-
args.push("--line", String(options.line));
|
|
1879
|
-
if (options.column !== undefined)
|
|
1880
|
-
args.push("--column", String(options.column));
|
|
1881
|
-
if (options.language)
|
|
1882
|
-
args.push("--language", options.language);
|
|
1883
|
-
appendGitSelection(args, options);
|
|
1884
|
-
return parseRefsOutput(await runReadSeek(args, { signal: options.signal })).references;
|
|
1885
|
-
}
|
|
1886
1129
|
function parseDiagnosticKind(value) {
|
|
1887
1130
|
if (value === "error" || value === "missing")
|
|
1888
1131
|
return value;
|
|
@@ -1909,9 +1152,6 @@ function parseCheckOutput(value) {
|
|
|
1909
1152
|
})
|
|
1910
1153
|
};
|
|
1911
1154
|
}
|
|
1912
|
-
async function readSeekCheck(filePath, content, options = {}) {
|
|
1913
|
-
return parseCheckOutput(await runReadSeek(["check", `stdin:${filePath}`], { signal: options.signal, stdin: content }));
|
|
1914
|
-
}
|
|
1915
1155
|
function parseOcrText(value) {
|
|
1916
1156
|
return optionalString(value, "ocr");
|
|
1917
1157
|
}
|
|
@@ -1933,6 +1173,11 @@ function parseDetectedObjects(value) {
|
|
|
1933
1173
|
};
|
|
1934
1174
|
});
|
|
1935
1175
|
}
|
|
1176
|
+
function parseImageEncoding(value) {
|
|
1177
|
+
if (value === "base64")
|
|
1178
|
+
return value;
|
|
1179
|
+
throw new Error("invalid image encoding");
|
|
1180
|
+
}
|
|
1936
1181
|
function parseDetectOutput(value) {
|
|
1937
1182
|
if (!value || typeof value !== "object")
|
|
1938
1183
|
throw new Error("invalid readseek detect output");
|
|
@@ -1983,20 +1228,228 @@ function parseDetectOutput(value) {
|
|
|
1983
1228
|
}
|
|
1984
1229
|
return { kind: "text", type, file, mime };
|
|
1985
1230
|
}
|
|
1986
|
-
function
|
|
1987
|
-
if (value
|
|
1988
|
-
|
|
1989
|
-
|
|
1231
|
+
function parseDocumentCache(value) {
|
|
1232
|
+
if (!value || typeof value !== "object")
|
|
1233
|
+
throw new Error("invalid readseek document cache");
|
|
1234
|
+
const cache = value;
|
|
1235
|
+
const state = requireString(cache.state, "cache.state");
|
|
1236
|
+
if (state !== "built" && state !== "reused")
|
|
1237
|
+
throw new Error("invalid readseek cache.state");
|
|
1238
|
+
return {
|
|
1239
|
+
content_hash: requireString(cache.content_hash, "cache.content_hash"),
|
|
1240
|
+
state
|
|
1241
|
+
};
|
|
1242
|
+
}
|
|
1243
|
+
function parseIdentifyOutput(value) {
|
|
1244
|
+
if (!value || typeof value !== "object")
|
|
1245
|
+
throw new Error("invalid readseek identify output");
|
|
1246
|
+
const output = value;
|
|
1247
|
+
const identifier = output.identifier;
|
|
1248
|
+
const symbol = output.symbol;
|
|
1249
|
+
return {
|
|
1250
|
+
file: requireString(output.file, "file"),
|
|
1251
|
+
language: requireString(output.language, "language"),
|
|
1252
|
+
engine: optionalString(output.engine, "engine"),
|
|
1253
|
+
line_count: requireNumber(output.line_count, "line_count"),
|
|
1254
|
+
file_hash: requireString(output.file_hash, "file_hash"),
|
|
1255
|
+
line: requireNumber(output.line, "line"),
|
|
1256
|
+
column: requireNumber(output.column, "column"),
|
|
1257
|
+
line_hash: requireString(output.line_hash, "line_hash"),
|
|
1258
|
+
identifier: identifier && typeof identifier === "object" ? {
|
|
1259
|
+
text: requireString(identifier.text, "identifier.text"),
|
|
1260
|
+
start_column: requireNumber(identifier.start_column, "identifier.start_column"),
|
|
1261
|
+
end_column: requireNumber(identifier.end_column, "identifier.end_column"),
|
|
1262
|
+
start_byte: requireNumber(identifier.start_byte, "identifier.start_byte"),
|
|
1263
|
+
end_byte: requireNumber(identifier.end_byte, "identifier.end_byte")
|
|
1264
|
+
} : undefined,
|
|
1265
|
+
symbol: symbol && typeof symbol === "object" ? {
|
|
1266
|
+
name: requireString(symbol.name, "symbol.name"),
|
|
1267
|
+
kind: requireString(symbol.kind, "symbol.kind"),
|
|
1268
|
+
qualified_name: requireString(symbol.qualified_name, "symbol.qualified_name"),
|
|
1269
|
+
start_line: requireNumber(symbol.start_line, "symbol.start_line"),
|
|
1270
|
+
end_line: requireNumber(symbol.end_line, "symbol.end_line")
|
|
1271
|
+
} : undefined
|
|
1272
|
+
};
|
|
1273
|
+
}
|
|
1274
|
+
function parseImageContent(value) {
|
|
1275
|
+
const detection = parseDetectOutput(value);
|
|
1276
|
+
if (detection.kind !== "image")
|
|
1277
|
+
throw new Error("invalid readseek image content");
|
|
1278
|
+
return detection;
|
|
1279
|
+
}
|
|
1280
|
+
function parseTextContent(value) {
|
|
1281
|
+
if (!value || typeof value !== "object")
|
|
1282
|
+
throw new Error("invalid readseek content");
|
|
1283
|
+
const output = value;
|
|
1284
|
+
if (output.hashlines !== undefined && output.start_line === undefined) {
|
|
1285
|
+
const hashlines = parseSearchHashlines(output.hashlines, "content.hashlines");
|
|
1286
|
+
const first = hashlines[0];
|
|
1287
|
+
const last = hashlines[hashlines.length - 1];
|
|
1288
|
+
return {
|
|
1289
|
+
file: requireString(output.file, "file"),
|
|
1290
|
+
language: requireString(output.language, "language"),
|
|
1291
|
+
line_count: requireNumber(output.line_count, "line_count"),
|
|
1292
|
+
file_hash: requireString(output.file_hash, "file_hash"),
|
|
1293
|
+
start_line: first?.line ?? 1,
|
|
1294
|
+
end_line: last?.line ?? 0,
|
|
1295
|
+
hashlines
|
|
1296
|
+
};
|
|
1297
|
+
}
|
|
1298
|
+
return parseReadOutput(value);
|
|
1299
|
+
}
|
|
1300
|
+
function parseDigestEnvelope(value) {
|
|
1301
|
+
if (!value || typeof value !== "object")
|
|
1302
|
+
throw new Error("invalid readseek digest output");
|
|
1303
|
+
const output = value;
|
|
1304
|
+
if (output.metadata === undefined)
|
|
1305
|
+
throw new Error("invalid readseek digest metadata");
|
|
1306
|
+
const result = {
|
|
1307
|
+
metadata: parseDetectOutput(output.metadata)
|
|
1308
|
+
};
|
|
1309
|
+
if (output.cache !== undefined)
|
|
1310
|
+
result.cache = parseDocumentCache(output.cache);
|
|
1311
|
+
if (output.content !== undefined)
|
|
1312
|
+
result.content = output.content;
|
|
1313
|
+
if (output.map !== undefined)
|
|
1314
|
+
result.map = parseMapOutput(output.map);
|
|
1315
|
+
if (output.diagnostics !== undefined)
|
|
1316
|
+
result.diagnostics = parseCheckOutput(output.diagnostics);
|
|
1317
|
+
if (output.identity !== undefined)
|
|
1318
|
+
result.identity = parseIdentifyOutput(output.identity);
|
|
1319
|
+
return result;
|
|
1320
|
+
}
|
|
1321
|
+
function digestSelectArg(select) {
|
|
1322
|
+
if (select === undefined)
|
|
1323
|
+
return "content";
|
|
1324
|
+
return Array.isArray(select) ? select.join(",") : select;
|
|
1325
|
+
}
|
|
1326
|
+
function digestArgs(target, options) {
|
|
1327
|
+
const args = ["digest", target, "--select", digestSelectArg(options.select)];
|
|
1328
|
+
if (options.at !== undefined)
|
|
1329
|
+
args.push("--at", options.at);
|
|
1330
|
+
if (options.end !== undefined)
|
|
1331
|
+
args.push("--end", String(options.end));
|
|
1332
|
+
if (options.limit !== undefined)
|
|
1333
|
+
args.push("--limit", String(options.limit));
|
|
1334
|
+
if (options.language)
|
|
1335
|
+
args.push("--language", options.language);
|
|
1336
|
+
if (options.visionMode !== undefined)
|
|
1337
|
+
args.push("--vision-mode", options.visionMode);
|
|
1338
|
+
if (options.visionLevel !== undefined)
|
|
1339
|
+
args.push("--vision-level", options.visionLevel);
|
|
1340
|
+
if (options.stdin !== undefined) {
|
|
1341
|
+
const stdinName = options.stdinName ?? target;
|
|
1342
|
+
if (target === "-")
|
|
1343
|
+
args.push("--stdin-name", stdinName);
|
|
1344
|
+
}
|
|
1345
|
+
return args;
|
|
1346
|
+
}
|
|
1347
|
+
async function runDigestRaw(target, options = {}) {
|
|
1348
|
+
const args = digestArgs(target, options);
|
|
1349
|
+
const runOptions = {
|
|
1350
|
+
signal: options.signal,
|
|
1351
|
+
stdin: options.stdin,
|
|
1352
|
+
timeoutMs: options.timeoutMs
|
|
1353
|
+
};
|
|
1354
|
+
return options.vision ? await runReadSeekVision(args, runOptions) : await runReadSeek(args, runOptions);
|
|
1355
|
+
}
|
|
1356
|
+
async function readSeekDigestNative(target, options = {}) {
|
|
1357
|
+
return runDigestRaw(target, options);
|
|
1358
|
+
}
|
|
1359
|
+
async function readSeekDigest(target, options = {}) {
|
|
1360
|
+
return parseDigestEnvelope(await runDigestRaw(target, options));
|
|
1361
|
+
}
|
|
1362
|
+
function requireDigestContent(envelope, field) {
|
|
1363
|
+
if (envelope.content === undefined)
|
|
1364
|
+
throw new Error(`invalid readseek ${field}: missing content`);
|
|
1365
|
+
return envelope.content;
|
|
1366
|
+
}
|
|
1367
|
+
function fileMapFromReadSeekOutput(output, filePath, totalBytes) {
|
|
1368
|
+
if (output.language === "unknown" && output.symbols.length === 0)
|
|
1369
|
+
return null;
|
|
1370
|
+
return {
|
|
1371
|
+
path: filePath,
|
|
1372
|
+
totalLines: output.line_count,
|
|
1373
|
+
totalBytes,
|
|
1374
|
+
language: normalizeLanguage(output.language),
|
|
1375
|
+
detailLevel: "full" /* Full */,
|
|
1376
|
+
symbols: symbolsFromReadSeek(output.symbols)
|
|
1377
|
+
};
|
|
1378
|
+
}
|
|
1379
|
+
async function readSeekMap(filePath, totalBytes, options = {}) {
|
|
1380
|
+
const envelope = await readSeekDigest(filePath, { select: "map", signal: options.signal });
|
|
1381
|
+
if (!envelope.map)
|
|
1382
|
+
return null;
|
|
1383
|
+
return fileMapFromReadSeekOutput(envelope.map, filePath, totalBytes);
|
|
1384
|
+
}
|
|
1385
|
+
async function readSeekSearch(target, pattern, options = {}) {
|
|
1386
|
+
const args = ["search", target, pattern];
|
|
1387
|
+
if (options.language)
|
|
1388
|
+
args.push("--language", options.language);
|
|
1389
|
+
appendGitSelection(args, options);
|
|
1390
|
+
return parseSearchOutput(await runReadSeek(args, { signal: options.signal })).results;
|
|
1391
|
+
}
|
|
1392
|
+
async function readSeekRefs(target, name, options = {}) {
|
|
1393
|
+
const args = ["refs", target];
|
|
1394
|
+
if (options.scope) {
|
|
1395
|
+
if (options.line !== undefined)
|
|
1396
|
+
args.push("--at", lineLocationSelector(options.line, options.column));
|
|
1397
|
+
} else {
|
|
1398
|
+
args.push(name);
|
|
1399
|
+
}
|
|
1400
|
+
if (options.language)
|
|
1401
|
+
args.push("--language", options.language);
|
|
1402
|
+
appendGitSelection(args, options);
|
|
1403
|
+
return parseRefsOutput(await runReadSeek(args, { signal: options.signal })).references;
|
|
1404
|
+
}
|
|
1405
|
+
async function readSeekCheck(filePath, content, options = {}) {
|
|
1406
|
+
const envelope = await readSeekDigest("-", {
|
|
1407
|
+
select: "diagnostics",
|
|
1408
|
+
stdin: content,
|
|
1409
|
+
stdinName: filePath,
|
|
1410
|
+
signal: options.signal
|
|
1411
|
+
});
|
|
1412
|
+
if (!envelope.diagnostics)
|
|
1413
|
+
throw new Error("invalid readseek diagnostics: missing diagnostics facet");
|
|
1414
|
+
return envelope.diagnostics;
|
|
1990
1415
|
}
|
|
1991
1416
|
async function readSeekDetect(filePath, options = {}) {
|
|
1992
|
-
|
|
1417
|
+
const envelope = await readSeekDigest(filePath, { select: "metadata", signal: options.signal });
|
|
1418
|
+
return envelope.metadata;
|
|
1419
|
+
}
|
|
1420
|
+
async function readSeekDigestContent(filePath, options = {}) {
|
|
1421
|
+
const envelope = await readSeekDigest(filePath, {
|
|
1422
|
+
select: "content",
|
|
1423
|
+
at: options.startLine !== undefined ? lineLocationSelector(options.startLine) : undefined,
|
|
1424
|
+
end: options.endLine,
|
|
1425
|
+
visionMode: options.visionMode,
|
|
1426
|
+
signal: options.signal,
|
|
1427
|
+
vision: options.vision
|
|
1428
|
+
});
|
|
1429
|
+
if (envelope.metadata.kind === "pdf") {
|
|
1430
|
+
return { metadata: envelope.metadata, cache: envelope.cache };
|
|
1431
|
+
}
|
|
1432
|
+
const content = requireDigestContent(envelope, "content");
|
|
1433
|
+
if (envelope.metadata.kind === "image") {
|
|
1434
|
+
return { metadata: envelope.metadata, image: parseImageContent(content) };
|
|
1435
|
+
}
|
|
1436
|
+
return { metadata: envelope.metadata, text: parseTextContent(content) };
|
|
1993
1437
|
}
|
|
1994
1438
|
async function readSeekImage(filePath, modes, options = {}) {
|
|
1995
1439
|
const requestedModes = modes.length > 1 ? ["all"] : modes;
|
|
1996
|
-
const results = await Promise.allSettled(requestedModes.map(async (mode) =>
|
|
1440
|
+
const results = await Promise.allSettled(requestedModes.map(async (mode) => {
|
|
1441
|
+
const digested = await readSeekDigestContent(filePath, {
|
|
1442
|
+
visionMode: mode,
|
|
1443
|
+
signal: options.signal,
|
|
1444
|
+
vision: true
|
|
1445
|
+
});
|
|
1446
|
+
if (!digested.image)
|
|
1447
|
+
throw new Error(`readseek returned no image analysis for ${filePath}`);
|
|
1448
|
+
return digested.image;
|
|
1449
|
+
}));
|
|
1997
1450
|
let merged;
|
|
1998
1451
|
for (const result of results) {
|
|
1999
|
-
if (result.status !== "fulfilled"
|
|
1452
|
+
if (result.status !== "fulfilled")
|
|
2000
1453
|
continue;
|
|
2001
1454
|
const detection = result.value;
|
|
2002
1455
|
merged = merged === undefined ? detection : {
|
|
@@ -2014,52 +1467,46 @@ async function readSeekImage(filePath, modes, options = {}) {
|
|
|
2014
1467
|
throw new Error(`readseek returned no image analysis for ${filePath}`);
|
|
2015
1468
|
}
|
|
2016
1469
|
async function readSeekPreparedImage(filePath, options = {}) {
|
|
2017
|
-
const
|
|
2018
|
-
|
|
1470
|
+
const digested = await readSeekDigestContent(filePath, {
|
|
1471
|
+
visionMode: "none",
|
|
1472
|
+
signal: options.signal
|
|
1473
|
+
});
|
|
1474
|
+
const output = digested.image;
|
|
1475
|
+
if (!output || output.encoding !== "base64" || output.data === undefined || output.mime === undefined) {
|
|
2019
1476
|
throw new Error(`readseek returned no prepared image for ${filePath}`);
|
|
2020
1477
|
}
|
|
2021
1478
|
return { mime: output.mime, encoding: output.encoding, data: output.data };
|
|
2022
1479
|
}
|
|
2023
|
-
function
|
|
1480
|
+
function parseEditOutput(value) {
|
|
2024
1481
|
if (!value || typeof value !== "object")
|
|
2025
|
-
throw new Error("invalid readseek
|
|
2026
|
-
const image = value;
|
|
2027
|
-
const mode = requireString(image.mode, "PDF image.mode");
|
|
2028
|
-
if (mode !== "none" && mode !== "all" && mode !== "ocr" && mode !== "caption" && mode !== "objects") {
|
|
2029
|
-
throw new Error("invalid readseek PDF image.mode");
|
|
2030
|
-
}
|
|
2031
|
-
return {
|
|
2032
|
-
page: requireNumber(image.page, "PDF image.page"),
|
|
2033
|
-
width: requireNumber(image.width, "PDF image.width"),
|
|
2034
|
-
height: requireNumber(image.height, "PDF image.height"),
|
|
2035
|
-
mime: requireString(image.mime, "PDF image.mime"),
|
|
2036
|
-
mode,
|
|
2037
|
-
encoding: image.encoding === undefined ? undefined : parseImageEncoding(image.encoding),
|
|
2038
|
-
data: optionalString(image.data, "PDF image.data"),
|
|
2039
|
-
ocr: parseOcrText(image.ocr),
|
|
2040
|
-
caption: optionalString(image.caption, "PDF image.caption"),
|
|
2041
|
-
objects: parseDetectedObjects(image.objects)
|
|
2042
|
-
};
|
|
2043
|
-
}
|
|
2044
|
-
function parsePdfOutput(value) {
|
|
2045
|
-
if (!value || typeof value !== "object")
|
|
2046
|
-
throw new Error("invalid readseek PDF output");
|
|
1482
|
+
throw new Error("invalid readseek edit output");
|
|
2047
1483
|
const output = value;
|
|
2048
|
-
if (output.format !== "pdf" || !Array.isArray(output.images))
|
|
2049
|
-
throw new Error("invalid readseek PDF output");
|
|
2050
1484
|
return {
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
1485
|
+
file: requireString(output.file, "file"),
|
|
1486
|
+
before_hash: requireString(output.before_hash, "before_hash"),
|
|
1487
|
+
file_hash: requireString(output.file_hash, "file_hash"),
|
|
1488
|
+
plan_hash: requireString(output.plan_hash, "plan_hash"),
|
|
1489
|
+
applied: requireBoolean(output.applied, "applied"),
|
|
1490
|
+
changed: requireBoolean(output.changed, "changed"),
|
|
1491
|
+
edit_count: requireNumber(output.edit_count, "edit_count"),
|
|
1492
|
+
before_content: optionalString(output.before_content, "before_content"),
|
|
1493
|
+
content: optionalString(output.content, "content")
|
|
2055
1494
|
};
|
|
2056
1495
|
}
|
|
2057
|
-
async function
|
|
2058
|
-
const
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
1496
|
+
async function readSeekEdit(filePath, edits, options = {}) {
|
|
1497
|
+
const args = ["edit", filePath];
|
|
1498
|
+
if (options.apply)
|
|
1499
|
+
args.push("--apply");
|
|
1500
|
+
if (options.planHash)
|
|
1501
|
+
args.push("--plan-hash", options.planHash);
|
|
1502
|
+
if (options.language)
|
|
1503
|
+
args.push("--language", options.language);
|
|
1504
|
+
return parseEditOutput(await runReadSeek(args, {
|
|
1505
|
+
signal: options.signal,
|
|
1506
|
+
stdin: JSON.stringify({ edits }),
|
|
1507
|
+
timeoutMs: options.timeoutMs,
|
|
1508
|
+
cancelable: options.cancelable
|
|
1509
|
+
}));
|
|
2063
1510
|
}
|
|
2064
1511
|
function parseRenameConflicts(value, field) {
|
|
2065
1512
|
if (value === undefined)
|
|
@@ -2131,10 +1578,7 @@ function parseRenameOutput(value) {
|
|
|
2131
1578
|
};
|
|
2132
1579
|
}
|
|
2133
1580
|
async function readSeekRename(filePath, options) {
|
|
2134
|
-
const args = ["rename", filePath, "--
|
|
2135
|
-
if (options.column !== undefined)
|
|
2136
|
-
args.push("--column", String(options.column));
|
|
2137
|
-
args.push("--to", options.to);
|
|
1581
|
+
const args = ["rename", filePath, "--at", lineLocationSelector(options.line, options.column), "--to", options.to];
|
|
2138
1582
|
if (options.apply)
|
|
2139
1583
|
args.push("--apply");
|
|
2140
1584
|
if (options.workspace)
|
|
@@ -2144,46 +1588,6 @@ async function readSeekRename(filePath, options) {
|
|
|
2144
1588
|
appendGitSelection(args, options);
|
|
2145
1589
|
return parseRenameOutput(await runReadSeek(args, { signal: options.signal }));
|
|
2146
1590
|
}
|
|
2147
|
-
function parseIdentifyOutput(value) {
|
|
2148
|
-
if (!value || typeof value !== "object")
|
|
2149
|
-
throw new Error("invalid readseek identify output");
|
|
2150
|
-
const output = value;
|
|
2151
|
-
const identifier = output.identifier;
|
|
2152
|
-
const symbol = output.symbol;
|
|
2153
|
-
return {
|
|
2154
|
-
file: requireString(output.file, "file"),
|
|
2155
|
-
language: requireString(output.language, "language"),
|
|
2156
|
-
engine: optionalString(output.engine, "engine"),
|
|
2157
|
-
line_count: requireNumber(output.line_count, "line_count"),
|
|
2158
|
-
file_hash: requireString(output.file_hash, "file_hash"),
|
|
2159
|
-
line: requireNumber(output.line, "line"),
|
|
2160
|
-
column: requireNumber(output.column, "column"),
|
|
2161
|
-
line_hash: requireString(output.line_hash, "line_hash"),
|
|
2162
|
-
identifier: identifier && typeof identifier === "object" ? {
|
|
2163
|
-
text: requireString(identifier.text, "identifier.text"),
|
|
2164
|
-
start_column: requireNumber(identifier.start_column, "identifier.start_column"),
|
|
2165
|
-
end_column: requireNumber(identifier.end_column, "identifier.end_column"),
|
|
2166
|
-
start_byte: requireNumber(identifier.start_byte, "identifier.start_byte"),
|
|
2167
|
-
end_byte: requireNumber(identifier.end_byte, "identifier.end_byte")
|
|
2168
|
-
} : undefined,
|
|
2169
|
-
symbol: symbol && typeof symbol === "object" ? {
|
|
2170
|
-
name: requireString(symbol.name, "symbol.name"),
|
|
2171
|
-
kind: requireString(symbol.kind, "symbol.kind"),
|
|
2172
|
-
qualified_name: requireString(symbol.qualified_name, "symbol.qualified_name"),
|
|
2173
|
-
start_line: requireNumber(symbol.start_line, "symbol.start_line"),
|
|
2174
|
-
end_line: requireNumber(symbol.end_line, "symbol.end_line")
|
|
2175
|
-
} : undefined
|
|
2176
|
-
};
|
|
2177
|
-
}
|
|
2178
|
-
async function readSeekIdentify(filePath, content, options = {}) {
|
|
2179
|
-
const target = options.line === undefined ? `stdin:${filePath}` : `stdin:${filePath}:${options.line}`;
|
|
2180
|
-
const args = ["identify", target];
|
|
2181
|
-
if (options.column !== undefined)
|
|
2182
|
-
args.push("--column", String(options.column));
|
|
2183
|
-
if (options.language)
|
|
2184
|
-
args.push("--language", options.language);
|
|
2185
|
-
return parseIdentifyOutput(await runReadSeek(args, { signal: options.signal, stdin: content }));
|
|
2186
|
-
}
|
|
2187
1591
|
function parseDefCompact(value) {
|
|
2188
1592
|
if (!value || typeof value !== "object")
|
|
2189
1593
|
throw new Error("invalid readseek def output");
|
|
@@ -2871,9 +2275,6 @@ function formatReadCallText(args) {
|
|
|
2871
2275
|
if (typeof args?.symbol === "string" && args.symbol) {
|
|
2872
2276
|
return { path: rawPath, suffix: `→ symbol ${args.symbol}` };
|
|
2873
2277
|
}
|
|
2874
|
-
if (args?.map === true) {
|
|
2875
|
-
return { path: rawPath, suffix: "+ map" };
|
|
2876
|
-
}
|
|
2877
2278
|
if (typeof args?.offset === "number") {
|
|
2878
2279
|
const offset = args.offset;
|
|
2879
2280
|
const limit = typeof args?.limit === "number" ? args.limit : undefined;
|
|
@@ -3162,9 +2563,6 @@ function optionalIntOrString(description) {
|
|
|
3162
2563
|
function filePathParam() {
|
|
3163
2564
|
return Type.String({ description: "Absolute or relative file path" });
|
|
3164
2565
|
}
|
|
3165
|
-
function mapParam() {
|
|
3166
|
-
return Type.Optional(Type.Boolean({ description: "Append structural map" }));
|
|
3167
|
-
}
|
|
3168
2566
|
function registerReadSeekTool(pi, tool) {
|
|
3169
2567
|
pi.registerTool(tool);
|
|
3170
2568
|
return tool;
|
|
@@ -3211,50 +2609,9 @@ ${lines.join(`
|
|
|
3211
2609
|
return "No image analysis returned.";
|
|
3212
2610
|
return "Image caption returned no text.";
|
|
3213
2611
|
}
|
|
3214
|
-
function
|
|
3215
|
-
const
|
|
3216
|
-
for
|
|
3217
|
-
const details = [];
|
|
3218
|
-
const ocr = image.ocr?.trim();
|
|
3219
|
-
if (ocr)
|
|
3220
|
-
details.push(`OCR text:
|
|
3221
|
-
${ocr}`);
|
|
3222
|
-
const caption = image.caption?.trim();
|
|
3223
|
-
if (caption)
|
|
3224
|
-
details.push(`Caption:
|
|
3225
|
-
${caption}`);
|
|
3226
|
-
if (image.objects?.length) {
|
|
3227
|
-
const lines = image.objects.map((object) => `- ${object.label} [${object.bbox.join(", ")}]`);
|
|
3228
|
-
details.push(`Detected objects:
|
|
3229
|
-
${lines.join(`
|
|
3230
|
-
`)}`);
|
|
3231
|
-
}
|
|
3232
|
-
if (details.length > 0)
|
|
3233
|
-
sections.push(`PDF page ${image.page} image:
|
|
3234
|
-
${details.join(`
|
|
3235
|
-
|
|
3236
|
-
`)}`);
|
|
3237
|
-
}
|
|
3238
|
-
return sections.filter(Boolean).join(`
|
|
3239
|
-
|
|
3240
|
-
`);
|
|
3241
|
-
}
|
|
3242
|
-
function preparedPdfContent(pdf) {
|
|
3243
|
-
const content = [{ type: "text", text: truncateDocumentText(pdf.markdown) }];
|
|
3244
|
-
for (const image of pdf.images) {
|
|
3245
|
-
if (image.encoding !== "base64" || image.data === undefined)
|
|
3246
|
-
continue;
|
|
3247
|
-
content.push({ type: "text", text: `[PDF page ${image.page} image]` });
|
|
3248
|
-
content.push({ type: "image", data: image.data, mimeType: image.mime });
|
|
3249
|
-
}
|
|
3250
|
-
return content;
|
|
3251
|
-
}
|
|
3252
|
-
function truncateDocumentText(text) {
|
|
3253
|
-
const truncation = truncateHead2(text, { maxLines: DEFAULT_MAX_LINES3, maxBytes: DEFAULT_MAX_BYTES3 });
|
|
3254
|
-
if (!truncation.truncated)
|
|
3255
|
-
return truncation.content;
|
|
3256
|
-
return `${truncation.content}
|
|
3257
|
-
[… document output truncated; use readSeek_view with page or node selectors]`;
|
|
2612
|
+
function documentRedirectMessage(path2, cache) {
|
|
2613
|
+
const cacheNote = cache ? ` Index ${cache.state} (hash ${cache.content_hash}).` : "";
|
|
2614
|
+
return `Document content is not available via readSeek_read for ${path2}.${cacheNote} Use readSeek_view to digest structure or selected content.`;
|
|
3258
2615
|
}
|
|
3259
2616
|
function skippedImageFile(path2) {
|
|
3260
2617
|
return {
|
|
@@ -3282,25 +2639,14 @@ async function executeRead(opts) {
|
|
|
3282
2639
|
const message = `Invalid offset: expected a positive integer, received ${offset.value}.`;
|
|
3283
2640
|
return buildToolErrorResult("read", "invalid-offset", message, { path: rawParams.path });
|
|
3284
2641
|
}
|
|
3285
|
-
const page = coerceObviousBase10Int(rawParams.page, "page");
|
|
3286
|
-
if (!page.ok) {
|
|
3287
|
-
return buildToolErrorResult("read", "invalid-page", page.message, { path: rawParams.path });
|
|
3288
|
-
}
|
|
3289
|
-
if (page.value !== undefined && page.value < 1) {
|
|
3290
|
-
const message = `Invalid page: expected a positive integer, received ${page.value}.`;
|
|
3291
|
-
return buildToolErrorResult("read", "invalid-page", message, { path: rawParams.path });
|
|
3292
|
-
}
|
|
3293
2642
|
const rawSymbol = typeof rawParams.symbol === "string" && rawParams.symbol.trim().length > 0 ? rawParams.symbol.trim() : undefined;
|
|
3294
2643
|
const rawBundle = typeof rawParams.bundle === "string" ? rawParams.bundle.trim() : undefined;
|
|
3295
|
-
const requestedMapViaBundle = rawBundle === "map" || rawBundle === "local" && rawSymbol === undefined && (rawParams.map ?? true);
|
|
3296
2644
|
const p = {
|
|
3297
2645
|
...rawParams,
|
|
3298
2646
|
offset: offset.value,
|
|
3299
2647
|
limit: limit.value,
|
|
3300
|
-
page: page.value,
|
|
3301
2648
|
symbol: rawSymbol,
|
|
3302
|
-
|
|
3303
|
-
bundle: requestedMapViaBundle ? undefined : rawBundle
|
|
2649
|
+
bundle: rawBundle
|
|
3304
2650
|
};
|
|
3305
2651
|
const rawPath = p.path.replace(/^@/, "");
|
|
3306
2652
|
const absolutePath = resolveToCwd(rawPath, cwd);
|
|
@@ -3324,14 +2670,6 @@ async function executeRead(opts) {
|
|
|
3324
2670
|
const message = 'Cannot use bundle without symbol. Use read({ path, symbol, bundle: "local" }).';
|
|
3325
2671
|
return buildToolErrorResult("read", "invalid-params-combo", message, { path: rawParams.path });
|
|
3326
2672
|
}
|
|
3327
|
-
if (p.bundle && p.map) {
|
|
3328
|
-
const message = "Cannot combine bundle with map. Use one or the other.";
|
|
3329
|
-
return buildToolErrorResult("read", "invalid-params-combo", message, { path: rawParams.path });
|
|
3330
|
-
}
|
|
3331
|
-
if (p.map && p.symbol) {
|
|
3332
|
-
const message = "Cannot combine map with symbol. Use one or the other.";
|
|
3333
|
-
return buildToolErrorResult("read", "invalid-params-combo", message, { path: rawParams.path });
|
|
3334
|
-
}
|
|
3335
2673
|
throwIfAborted(signal);
|
|
3336
2674
|
const ext = rawPath.split(".").pop()?.toLowerCase() ?? "";
|
|
3337
2675
|
let rawBuffer;
|
|
@@ -3345,27 +2683,31 @@ async function executeRead(opts) {
|
|
|
3345
2683
|
}
|
|
3346
2684
|
const hasBinaryContent = looksLikeBinary(rawBuffer);
|
|
3347
2685
|
const hasPdfHeader = rawBuffer.subarray(0, 1024).includes(Buffer.from("%PDF-"));
|
|
3348
|
-
|
|
2686
|
+
const maybeVisual = hasBinaryContent || ext === "pdf" || hasPdfHeader;
|
|
2687
|
+
let earlyDigest;
|
|
2688
|
+
if (maybeVisual) {
|
|
3349
2689
|
let detection;
|
|
3350
2690
|
try {
|
|
3351
|
-
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
path: rawParams.path
|
|
2691
|
+
earlyDigest = await readSeekDigestContent(absolutePath, {
|
|
2692
|
+
...ext === "pdf" || hasPdfHeader ? {} : { visionMode: "none" },
|
|
2693
|
+
signal
|
|
3355
2694
|
});
|
|
2695
|
+
detection = earlyDigest.metadata;
|
|
2696
|
+
} catch {
|
|
2697
|
+
try {
|
|
2698
|
+
detection = await readSeekDetect(absolutePath, { signal });
|
|
2699
|
+
} catch {
|
|
2700
|
+
return buildToolErrorResult("read", "read-error", `Visual file detection failed for ${absolutePath}.`, {
|
|
2701
|
+
path: rawParams.path
|
|
2702
|
+
});
|
|
2703
|
+
}
|
|
3356
2704
|
}
|
|
3357
|
-
if (
|
|
3358
|
-
return buildToolErrorResult("read", "
|
|
3359
|
-
path: rawParams.path
|
|
3360
|
-
});
|
|
2705
|
+
if (detection.kind === "pdf") {
|
|
2706
|
+
return buildToolErrorResult("read", "document-use-view", documentRedirectMessage(rawParams.path, earlyDigest?.cache), { path: rawParams.path });
|
|
3361
2707
|
}
|
|
3362
|
-
if (detection.kind === "image"
|
|
2708
|
+
if (detection.kind === "image") {
|
|
3363
2709
|
if (p.offset !== undefined || p.limit !== undefined) {
|
|
3364
|
-
const message = "Cannot combine offset/limit with image
|
|
3365
|
-
return buildToolErrorResult("read", "invalid-params-combo", message, { path: rawParams.path });
|
|
3366
|
-
}
|
|
3367
|
-
if (detection.kind !== "pdf" && p.page !== undefined) {
|
|
3368
|
-
const message = "The page parameter applies to PDFs only.";
|
|
2710
|
+
const message = "Cannot combine offset/limit with image reads.";
|
|
3369
2711
|
return buildToolErrorResult("read", "invalid-params-combo", message, { path: rawParams.path });
|
|
3370
2712
|
}
|
|
3371
2713
|
const imageMode = resolveReadSeekImageMode();
|
|
@@ -3375,38 +2717,14 @@ async function executeRead(opts) {
|
|
|
3375
2717
|
path: rawParams.path
|
|
3376
2718
|
});
|
|
3377
2719
|
}
|
|
3378
|
-
if (detection.kind === "pdf") {
|
|
3379
|
-
let pdfImageMode = "none";
|
|
3380
|
-
if (imageMode !== "off" && p.image !== undefined && p.image !== "none" && !useNativeVision) {
|
|
3381
|
-
pdfImageMode = p.image;
|
|
3382
|
-
}
|
|
3383
|
-
try {
|
|
3384
|
-
const pdf = await readSeekPdf(absolutePath, pdfImageMode, { page: p.page ?? 1, signal });
|
|
3385
|
-
if (pdfImageMode !== "none") {
|
|
3386
|
-
return succeed({
|
|
3387
|
-
content: [{ type: "text", text: truncateDocumentText(formatPdfAnalysis(pdf)) }],
|
|
3388
|
-
details: {}
|
|
3389
|
-
});
|
|
3390
|
-
}
|
|
3391
|
-
return succeed({
|
|
3392
|
-
content: useNativeVision ? preparedPdfContent(pdf) : [{ type: "text", text: truncateDocumentText(pdf.markdown) }],
|
|
3393
|
-
details: {}
|
|
3394
|
-
});
|
|
3395
|
-
} catch (error) {
|
|
3396
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
3397
|
-
return buildToolErrorResult("read", "read-error", `PDF reading unavailable: ${message}`, {
|
|
3398
|
-
path: rawParams.path
|
|
3399
|
-
});
|
|
3400
|
-
}
|
|
3401
|
-
}
|
|
3402
2720
|
if (imageMode === "off" || p.image === undefined) {
|
|
3403
2721
|
return skippedImageFile(rawParams.path);
|
|
3404
2722
|
}
|
|
3405
2723
|
if (useNativeVision || p.image === "none") {
|
|
3406
2724
|
try {
|
|
3407
|
-
const
|
|
2725
|
+
const prepared = earlyDigest?.image && earlyDigest.image.encoding === "base64" && earlyDigest.image.data !== undefined && earlyDigest.image.mime !== undefined ? { mime: earlyDigest.image.mime, encoding: earlyDigest.image.encoding, data: earlyDigest.image.data } : await readSeekPreparedImage(absolutePath, { signal });
|
|
3408
2726
|
return succeed({
|
|
3409
|
-
content: [{ type: "image", data:
|
|
2727
|
+
content: [{ type: "image", data: prepared.data, mimeType: prepared.mime }],
|
|
3410
2728
|
details: {}
|
|
3411
2729
|
});
|
|
3412
2730
|
} catch (error) {
|
|
@@ -3436,10 +2754,6 @@ async function executeRead(opts) {
|
|
|
3436
2754
|
});
|
|
3437
2755
|
}
|
|
3438
2756
|
}
|
|
3439
|
-
if (p.page !== undefined) {
|
|
3440
|
-
const message = "The page parameter applies to PDFs only.";
|
|
3441
|
-
return buildToolErrorResult("read", "invalid-params-combo", message, { path: rawParams.path });
|
|
3442
|
-
}
|
|
3443
2757
|
throwIfAborted(signal);
|
|
3444
2758
|
const rawText = rawBuffer.toString("utf-8");
|
|
3445
2759
|
const normalized = normalizeToLF(stripBom(rawText).text);
|
|
@@ -3562,7 +2876,11 @@ async function executeRead(opts) {
|
|
|
3562
2876
|
throwIfAborted(signal);
|
|
3563
2877
|
let readSeekOutput;
|
|
3564
2878
|
try {
|
|
3565
|
-
|
|
2879
|
+
const digested = total === 0 ? await readSeekDigestContent(absolutePath, { signal }) : await readSeekDigestContent(absolutePath, { startLine, endLine: endIdx, signal });
|
|
2880
|
+
if (!digested.text) {
|
|
2881
|
+
throw new Error(`readseek returned no text content for ${absolutePath}`);
|
|
2882
|
+
}
|
|
2883
|
+
readSeekOutput = digested.text;
|
|
3566
2884
|
} catch (err) {
|
|
3567
2885
|
const detail = err?.message ? ` — ${err.message}` : "";
|
|
3568
2886
|
const message = `readseek failed while reading ${rawPath}${detail}`;
|
|
@@ -3584,7 +2902,7 @@ async function executeRead(opts) {
|
|
|
3584
2902
|
const selected = readSeekLines.map((line) => line.raw);
|
|
3585
2903
|
const formatted = renderReadSeekLines(readSeekLines);
|
|
3586
2904
|
const truncation = truncateHead2(formatted, { maxLines: DEFAULT_MAX_LINES3, maxBytes: DEFAULT_MAX_BYTES3 });
|
|
3587
|
-
const shouldAppendMap = !!
|
|
2905
|
+
const shouldAppendMap = !!truncation.truncated && !p.offset && !p.limit && !symbolMatch;
|
|
3588
2906
|
let appendedMap = false;
|
|
3589
2907
|
let mapText = null;
|
|
3590
2908
|
if (shouldAppendMap) {
|
|
@@ -3597,6 +2915,16 @@ async function executeRead(opts) {
|
|
|
3597
2915
|
}
|
|
3598
2916
|
} catch {}
|
|
3599
2917
|
}
|
|
2918
|
+
let currentBuffer;
|
|
2919
|
+
try {
|
|
2920
|
+
currentBuffer = await fsReadFile(absolutePath);
|
|
2921
|
+
} catch (err) {
|
|
2922
|
+
const { code, message } = formatFsError(err, "read-error");
|
|
2923
|
+
return buildToolErrorResult("read", code, message, { path: rawParams.path });
|
|
2924
|
+
}
|
|
2925
|
+
if (!currentBuffer.equals(rawBuffer)) {
|
|
2926
|
+
return buildToolErrorResult("read", "file-changed-during-read", `File changed while it was being read: ${rawPath}. Re-read it to obtain a consistent snapshot.`, { path: rawParams.path });
|
|
2927
|
+
}
|
|
3600
2928
|
if (symbolWarning) {
|
|
3601
2929
|
structuredWarnings.push(buildReadSeekWarning("symbol-warning", symbolWarning.trim()));
|
|
3602
2930
|
}
|
|
@@ -3632,7 +2960,7 @@ async function executeRead(opts) {
|
|
|
3632
2960
|
endLine: symbolMatch.endLine
|
|
3633
2961
|
} : null,
|
|
3634
2962
|
map: {
|
|
3635
|
-
requested:
|
|
2963
|
+
requested: false,
|
|
3636
2964
|
appended: appendedMap,
|
|
3637
2965
|
text: mapText
|
|
3638
2966
|
},
|
|
@@ -3660,13 +2988,13 @@ function registerReadTool(pi, options = {}) {
|
|
|
3660
2988
|
const imageModes = imageMode === "auto" ? ["none", "all", "ocr", "caption", "objects"] : ["all", "ocr", "caption", "objects"];
|
|
3661
2989
|
const promptMetadata = defineToolPromptMetadata({
|
|
3662
2990
|
promptUrl: new URL("../prompts/read.md", import.meta.url),
|
|
3663
|
-
promptSnippet: imageMode === "off" ? "Read anchored text
|
|
2991
|
+
promptSnippet: imageMode === "off" ? "Read anchored text or symbols" : "Read anchored text, symbols, or images",
|
|
3664
2992
|
registeredName: name
|
|
3665
2993
|
});
|
|
3666
2994
|
const tool = registerReadSeekTool(pi, {
|
|
3667
2995
|
name,
|
|
3668
2996
|
label: "Read",
|
|
3669
|
-
description: imageMode === "off" ? "Read anchored text by range
|
|
2997
|
+
description: imageMode === "off" ? "Read anchored text by range or symbol. Images and documents are disabled; use readSeek_view for documents." : promptMetadata.description,
|
|
3670
2998
|
promptSnippet: promptMetadata.promptSnippet,
|
|
3671
2999
|
promptGuidelines: [
|
|
3672
3000
|
...promptMetadata.promptGuidelines,
|
|
@@ -3676,15 +3004,13 @@ function registerReadTool(pi, options = {}) {
|
|
|
3676
3004
|
path: filePathParam(),
|
|
3677
3005
|
offset: optionalIntOrString("One-based starting line"),
|
|
3678
3006
|
limit: optionalIntOrString("Maximum lines to return"),
|
|
3679
|
-
page: optionalIntOrString("One-based PDF page; defaults to 1"),
|
|
3680
3007
|
symbol: Type2.Optional(Type2.String({ description: "Symbol name to read" })),
|
|
3681
|
-
map: mapParam(),
|
|
3682
3008
|
bundle: Type2.Optional(Type2.Literal("local", {
|
|
3683
3009
|
description: "Include related symbols from the same file; requires symbol"
|
|
3684
3010
|
})),
|
|
3685
3011
|
...imageMode === "off" ? {} : {
|
|
3686
3012
|
image: Type2.Optional(Type2.Union(imageModes.map((mode) => Type2.Literal(mode)), {
|
|
3687
|
-
description: `Standalone
|
|
3013
|
+
description: `Standalone image mode: ${imageModes.join(", ")}. Required for standalone images.${imageMode === "auto" ? " For standalone images, none skips local analysis." : ""}`
|
|
3688
3014
|
}))
|
|
3689
3015
|
}
|
|
3690
3016
|
}),
|
|
@@ -3773,7 +3099,7 @@ ${textContent}`;
|
|
|
3773
3099
|
}
|
|
3774
3100
|
|
|
3775
3101
|
// src/edit.ts
|
|
3776
|
-
import { readFile as fsReadFile2
|
|
3102
|
+
import { readFile as fsReadFile2 } from "node:fs/promises";
|
|
3777
3103
|
import { createPatch } from "diff";
|
|
3778
3104
|
import { withFileMutationQueue } from "@earendil-works/pi-coding-agent";
|
|
3779
3105
|
import { Type as Type3 } from "@sinclair/typebox";
|
|
@@ -4056,77 +3382,12 @@ async function validateSyntaxRegression(input, options = {}) {
|
|
|
4056
3382
|
};
|
|
4057
3383
|
}
|
|
4058
3384
|
|
|
4059
|
-
// src/syntax-validate-mode.ts
|
|
4060
|
-
var DEFAULT = "warn";
|
|
4061
|
-
function resolveSyntaxValidateMode(opts) {
|
|
4062
|
-
return opts.syntaxValidate ?? resolveReadSeekSyntaxValidation() ?? DEFAULT;
|
|
4063
|
-
}
|
|
4064
|
-
|
|
4065
|
-
// src/replace-symbol.ts
|
|
4066
|
-
function detectIndent(line) {
|
|
4067
|
-
return line.match(/^\s*/)?.[0] ?? "";
|
|
4068
|
-
}
|
|
4069
|
-
function dedent(text) {
|
|
4070
|
-
const lines = text.split(`
|
|
4071
|
-
`);
|
|
4072
|
-
const nonEmpty = lines.filter((l) => l.trim().length);
|
|
4073
|
-
if (!nonEmpty.length)
|
|
4074
|
-
return text;
|
|
4075
|
-
const minIndent = Math.min(...nonEmpty.map((l) => l.match(/^\s*/)?.[0].length ?? 0));
|
|
4076
|
-
return lines.map((l) => l.slice(minIndent)).join(`
|
|
4077
|
-
`);
|
|
4078
|
-
}
|
|
4079
|
-
function reindent(text, indent) {
|
|
4080
|
-
return text.split(`
|
|
4081
|
-
`).map((l) => l.length ? indent + l : l).join(`
|
|
4082
|
-
`);
|
|
4083
|
-
}
|
|
4084
|
-
async function replaceSymbol(input) {
|
|
4085
|
-
const map = await readSeekMapContent(input.filePath, input.content);
|
|
4086
|
-
if (!map) {
|
|
4087
|
-
return {
|
|
4088
|
-
type: "unsupported",
|
|
4089
|
-
message: `Unsupported file for replace_symbol: readseek could not map '${input.filePath}'. Use anchored edits for this file type.`
|
|
4090
|
-
};
|
|
4091
|
-
}
|
|
4092
|
-
const lookup = findSymbol(map, input.symbol);
|
|
4093
|
-
if (lookup.type === "not-found") {
|
|
4094
|
-
return { type: "not-found", message: formatNotFound(input.symbol, map) };
|
|
4095
|
-
}
|
|
4096
|
-
if (lookup.type === "ambiguous") {
|
|
4097
|
-
return { type: "ambiguous", message: formatAmbiguous(input.symbol, lookup.candidates) };
|
|
4098
|
-
}
|
|
4099
|
-
const sym = lookup.symbol;
|
|
4100
|
-
const lines = input.content.split(`
|
|
4101
|
-
`);
|
|
4102
|
-
const sigLine = lines[sym.startLine - 1] ?? "";
|
|
4103
|
-
const indent = detectIndent(sigLine);
|
|
4104
|
-
const reindented = reindent(dedent(normalizeToLF(input.newBody)), indent);
|
|
4105
|
-
const warnings = [];
|
|
4106
|
-
const leaf = input.symbol.replace(/@\d+$/, "").split(".").pop() ?? "";
|
|
4107
|
-
const declNameRe = /\b(?:(?:export\s+(?:default\s+)?|async\s+|public\s+|private\s+|protected\s+|static\s+)*)(?:function\*?|class|const|let|var|fn|def|method|type|interface|enum)\s+([A-Za-z_$][\w$]*)/s;
|
|
4108
|
-
const firstDeclName = reindented.match(declNameRe)?.[1] ?? reindented.match(/^\s*(?:(?:async\s+)?[\w$<>,?\s]+\s+)?([A-Za-z_$][\w$]*)\s*\(/)?.[1];
|
|
4109
|
-
if (leaf && firstDeclName && firstDeclName !== leaf) {
|
|
4110
|
-
warnings.push(`name-mismatch: expected ${leaf}, got ${firstDeclName}`);
|
|
4111
|
-
}
|
|
4112
|
-
const before = lines.slice(0, sym.startLine - 1).join(`
|
|
4113
|
-
`);
|
|
4114
|
-
const after = lines.slice(sym.endLine).join(`
|
|
4115
|
-
`);
|
|
4116
|
-
const beforePart = before.length ? before + `
|
|
4117
|
-
` : "";
|
|
4118
|
-
const afterPart = after.length ? `
|
|
4119
|
-
` + after : "";
|
|
4120
|
-
const newContent = beforePart + reindented + afterPart;
|
|
4121
|
-
return {
|
|
4122
|
-
type: "ok",
|
|
4123
|
-
content: newContent,
|
|
4124
|
-
replacement: reindented,
|
|
4125
|
-
warnings,
|
|
4126
|
-
range: { start: sym.startLine, end: sym.endLine }
|
|
4127
|
-
};
|
|
4128
|
-
}
|
|
4129
|
-
|
|
3385
|
+
// src/syntax-validate-mode.ts
|
|
3386
|
+
var DEFAULT = "warn";
|
|
3387
|
+
function resolveSyntaxValidateMode(opts) {
|
|
3388
|
+
return opts.syntaxValidate ?? resolveReadSeekSyntaxValidation() ?? DEFAULT;
|
|
3389
|
+
}
|
|
3390
|
+
|
|
4130
3391
|
// src/pending-diff-preview.ts
|
|
4131
3392
|
import { existsSync as existsSync2, readFileSync as readFileSync4, realpathSync, statSync as statSync3 } from "node:fs";
|
|
4132
3393
|
import { dirname, isAbsolute as isAbsolute2, relative as relative2, resolve, sep } from "node:path";
|
|
@@ -4188,57 +3449,6 @@ function buildData(filePath, previousContent, nextContent, existed, headerLabel)
|
|
|
4188
3449
|
function getEdits(input) {
|
|
4189
3450
|
return Array.isArray(input.edits) ? input.edits : [];
|
|
4190
3451
|
}
|
|
4191
|
-
function isReplaceEdit(edit) {
|
|
4192
|
-
if (!edit || typeof edit !== "object" || !("replace" in edit))
|
|
4193
|
-
return false;
|
|
4194
|
-
const replace = edit.replace;
|
|
4195
|
-
return typeof replace?.old_text === "string" && typeof replace?.new_text === "string";
|
|
4196
|
-
}
|
|
4197
|
-
function isAnchorEdit(edit) {
|
|
4198
|
-
return !!edit && typeof edit === "object" && (("set_line" in edit) || ("replace_lines" in edit) || ("insert_after" in edit));
|
|
4199
|
-
}
|
|
4200
|
-
function applyAnchoredPreview(content, edits) {
|
|
4201
|
-
try {
|
|
4202
|
-
return { type: "ok", content: applyHashlineEdits(content, edits).content };
|
|
4203
|
-
} catch (err) {
|
|
4204
|
-
return { type: "skip", reason: `anchor projection failed: ${err?.message ?? String(err)}` };
|
|
4205
|
-
}
|
|
4206
|
-
}
|
|
4207
|
-
function isReplaceSymbolEdit(edit) {
|
|
4208
|
-
if (!edit || typeof edit !== "object" || !("replace_symbol" in edit))
|
|
4209
|
-
return false;
|
|
4210
|
-
const replaceSymbol2 = edit.replace_symbol;
|
|
4211
|
-
return typeof replaceSymbol2?.symbol === "string" && typeof replaceSymbol2?.new_body === "string";
|
|
4212
|
-
}
|
|
4213
|
-
async function applyReplaceSymbolPreview(filePath, content, edit) {
|
|
4214
|
-
try {
|
|
4215
|
-
if (!edit.replace_symbol.new_body.trim())
|
|
4216
|
-
return { type: "skip", reason: "replace_symbol new_body is empty" };
|
|
4217
|
-
const probe = await replaceSymbol({
|
|
4218
|
-
filePath,
|
|
4219
|
-
content,
|
|
4220
|
-
symbol: edit.replace_symbol.symbol,
|
|
4221
|
-
newBody: edit.replace_symbol.new_body
|
|
4222
|
-
});
|
|
4223
|
-
if (probe.type !== "ok")
|
|
4224
|
-
return { type: "skip", reason: `symbol projection failed: ${probe.message}` };
|
|
4225
|
-
return { type: "ok", content: probe.content };
|
|
4226
|
-
} catch (err) {
|
|
4227
|
-
return { type: "skip", reason: `symbol projection failed: ${err?.message ?? String(err)}` };
|
|
4228
|
-
}
|
|
4229
|
-
}
|
|
4230
|
-
function applyReplacePreview(content, edit) {
|
|
4231
|
-
const { old_text, new_text } = edit.replace;
|
|
4232
|
-
if (!old_text.length)
|
|
4233
|
-
return { type: "skip", reason: "replace old_text is empty" };
|
|
4234
|
-
const replacement = replaceText(content, old_text, new_text, {
|
|
4235
|
-
all: edit.replace.all ?? false,
|
|
4236
|
-
fuzzy: edit.replace.fuzzy ?? false
|
|
4237
|
-
});
|
|
4238
|
-
if (!replacement.count)
|
|
4239
|
-
return { type: "skip", reason: "replace old_text was not found" };
|
|
4240
|
-
return { type: "ok", content: replacement.content };
|
|
4241
|
-
}
|
|
4242
3452
|
function buildPendingWritePreviewData(input, cwd) {
|
|
4243
3453
|
if (typeof input.content !== "string")
|
|
4244
3454
|
return skip("missing content");
|
|
@@ -4262,50 +3472,16 @@ async function buildPendingEditPreviewData(input, cwd) {
|
|
|
4262
3472
|
const previous = readUtf8File(resolved.path);
|
|
4263
3473
|
if (previous.type === "skip")
|
|
4264
3474
|
return previous;
|
|
4265
|
-
|
|
4266
|
-
|
|
4267
|
-
|
|
4268
|
-
|
|
4269
|
-
|
|
4270
|
-
|
|
4271
|
-
|
|
4272
|
-
|
|
4273
|
-
|
|
4274
|
-
anchorBatch.length = 0;
|
|
4275
|
-
}
|
|
4276
|
-
const projected = applyReplacePreview(next, edit);
|
|
4277
|
-
if (projected.type === "skip")
|
|
4278
|
-
return projected;
|
|
4279
|
-
next = projected.content;
|
|
4280
|
-
continue;
|
|
4281
|
-
}
|
|
4282
|
-
if (isAnchorEdit(edit)) {
|
|
4283
|
-
anchorBatch.push(edit);
|
|
4284
|
-
continue;
|
|
4285
|
-
}
|
|
4286
|
-
if (isReplaceSymbolEdit(edit)) {
|
|
4287
|
-
if (anchorBatch.length > 0) {
|
|
4288
|
-
const anchored = applyAnchoredPreview(next, anchorBatch);
|
|
4289
|
-
if (anchored.type === "skip")
|
|
4290
|
-
return anchored;
|
|
4291
|
-
next = anchored.content;
|
|
4292
|
-
anchorBatch.length = 0;
|
|
4293
|
-
}
|
|
4294
|
-
const projected = await applyReplaceSymbolPreview(resolved.path, next, edit);
|
|
4295
|
-
if (projected.type === "skip")
|
|
4296
|
-
return projected;
|
|
4297
|
-
next = projected.content;
|
|
4298
|
-
continue;
|
|
4299
|
-
}
|
|
4300
|
-
return skip("unsupported edit variant");
|
|
4301
|
-
}
|
|
4302
|
-
if (anchorBatch.length > 0) {
|
|
4303
|
-
const anchored = applyAnchoredPreview(next, anchorBatch);
|
|
4304
|
-
if (anchored.type === "skip")
|
|
4305
|
-
return anchored;
|
|
4306
|
-
next = anchored.content;
|
|
3475
|
+
try {
|
|
3476
|
+
const plan = await readSeekEdit(resolved.path, edits, {
|
|
3477
|
+
language: typeof input.language === "string" ? input.language : undefined
|
|
3478
|
+
});
|
|
3479
|
+
if (typeof plan.content !== "string")
|
|
3480
|
+
return skip("edit plan did not include content");
|
|
3481
|
+
return buildData(resolved.path, previous.content, plan.content, true, "pending edit");
|
|
3482
|
+
} catch (err) {
|
|
3483
|
+
return skip(`edit projection failed: ${err?.message ?? String(err)}`);
|
|
4307
3484
|
}
|
|
4308
|
-
return buildData(resolved.path, previous.content, next, true, "pending edit");
|
|
4309
3485
|
}
|
|
4310
3486
|
function buildEditPreviewKey(input) {
|
|
4311
3487
|
if (typeof input.path !== "string")
|
|
@@ -4313,7 +3489,7 @@ function buildEditPreviewKey(input) {
|
|
|
4313
3489
|
const edits = getEdits(input);
|
|
4314
3490
|
if (edits.length === 0)
|
|
4315
3491
|
return;
|
|
4316
|
-
return JSON.stringify({ path: input.path, edits });
|
|
3492
|
+
return JSON.stringify({ path: input.path, edits, language: input.language });
|
|
4317
3493
|
}
|
|
4318
3494
|
function buildWritePreviewKey(input) {
|
|
4319
3495
|
if (typeof input.path !== "string" || typeof input.content !== "string")
|
|
@@ -4796,26 +3972,25 @@ var hashlineEditItemSchema = Type3.Object({
|
|
|
4796
3972
|
set_line: Type3.Optional(Type3.Object({
|
|
4797
3973
|
anchor: Type3.String({ description: "Fresh LINE:HASH anchor for the line to replace" }),
|
|
4798
3974
|
new_text: Type3.String({ description: "Replacement text; use an empty string to delete the line" })
|
|
4799
|
-
}, { description: 'Replace one line: {"set_line":{"anchor":"LINE:HASH","new_text":"..."}}' })),
|
|
3975
|
+
}, { additionalProperties: false, description: 'Replace one line: {"set_line":{"anchor":"LINE:HASH","new_text":"..."}}' })),
|
|
4800
3976
|
replace_lines: Type3.Optional(Type3.Object({
|
|
4801
3977
|
start_anchor: Type3.String({ description: "Fresh LINE:HASH anchor for the first line in the range" }),
|
|
4802
3978
|
end_anchor: Type3.String({ description: "Fresh LINE:HASH anchor for the last line in the range" }),
|
|
4803
3979
|
new_text: Type3.String({ description: "Replacement text; use an empty string to delete the range" })
|
|
4804
|
-
}, { description: 'Replace a range: {"replace_lines":{"start_anchor":"LINE:HASH","end_anchor":"LINE:HASH","new_text":"..."}}' })),
|
|
3980
|
+
}, { additionalProperties: false, description: 'Replace a range: {"replace_lines":{"start_anchor":"LINE:HASH","end_anchor":"LINE:HASH","new_text":"..."}}' })),
|
|
4805
3981
|
insert_after: Type3.Optional(Type3.Object({
|
|
4806
3982
|
anchor: Type3.String({ description: "Fresh LINE:HASH anchor for the line after which to insert" }),
|
|
4807
3983
|
new_text: Type3.String({ description: "Text to insert after the anchored line" })
|
|
4808
|
-
}, { description: 'Insert text: {"insert_after":{"anchor":"LINE:HASH","new_text":"..."}}' })),
|
|
3984
|
+
}, { additionalProperties: false, description: 'Insert text: {"insert_after":{"anchor":"LINE:HASH","new_text":"..."}}' })),
|
|
4809
3985
|
replace: Type3.Optional(Type3.Object({
|
|
4810
3986
|
old_text: Type3.String({ description: "Exact text to find" }),
|
|
4811
3987
|
new_text: Type3.String({ description: "Replacement text" }),
|
|
4812
|
-
all: Type3.Optional(Type3.Boolean({ description: "Replace every exact match" }))
|
|
4813
|
-
|
|
4814
|
-
}, { description: 'Replace text: {"replace":{"old_text":"...","new_text":"..."}}' })),
|
|
3988
|
+
all: Type3.Optional(Type3.Boolean({ description: "Replace every exact match" }))
|
|
3989
|
+
}, { additionalProperties: false, description: 'Replace text: {"replace":{"old_text":"...","new_text":"..."}}' })),
|
|
4815
3990
|
replace_symbol: Type3.Optional(Type3.Object({
|
|
4816
3991
|
symbol: Type3.String({ description: "Mapped symbol name" }),
|
|
4817
3992
|
new_body: Type3.String({ description: "Complete replacement body for the symbol" })
|
|
4818
|
-
}, { description: 'Replace a symbol: {"replace_symbol":{"symbol":"name","new_body":"..."}}' }))
|
|
3993
|
+
}, { additionalProperties: false, description: 'Replace a symbol: {"replace_symbol":{"symbol":"name","new_body":"..."}}' }))
|
|
4819
3994
|
}, {
|
|
4820
3995
|
additionalProperties: false,
|
|
4821
3996
|
minProperties: 1,
|
|
@@ -4824,13 +3999,15 @@ var hashlineEditItemSchema = Type3.Object({
|
|
|
4824
3999
|
});
|
|
4825
4000
|
var hashlineEditSchema = Type3.Object({
|
|
4826
4001
|
path: filePathParam(),
|
|
4827
|
-
|
|
4002
|
+
language: Type3.Optional(Type3.String({ description: "Language override for symbol replacement" })),
|
|
4003
|
+
edits: Type3.Array(hashlineEditItemSchema, {
|
|
4004
|
+
minItems: 1,
|
|
4828
4005
|
description: "Use set_line, replace_lines, insert_after, replace_symbol, or replace"
|
|
4829
|
-
})
|
|
4006
|
+
}),
|
|
4830
4007
|
postEditVerify: Type3.Optional(Type3.Boolean({
|
|
4831
4008
|
description: "Read back and verify persisted content"
|
|
4832
4009
|
}))
|
|
4833
|
-
}, { additionalProperties:
|
|
4010
|
+
}, { additionalProperties: false });
|
|
4834
4011
|
function buildEditError(path2, code, message, hint, errorDetails) {
|
|
4835
4012
|
return {
|
|
4836
4013
|
content: [{ type: "text", text: message }],
|
|
@@ -4854,9 +4031,7 @@ function mapEditFileError(err, filePath, _displayPath, _phase) {
|
|
|
4854
4031
|
}
|
|
4855
4032
|
async function executeEdit(opts) {
|
|
4856
4033
|
const { params, signal, cwd, wasReadInSession, onFileMutated, syntaxValidate } = opts;
|
|
4857
|
-
await ensureHashInit();
|
|
4858
4034
|
const parsed = params;
|
|
4859
|
-
const input = params;
|
|
4860
4035
|
const rawPath = parsed.path;
|
|
4861
4036
|
const path2 = rawPath.replace(/^@/, "");
|
|
4862
4037
|
const absolutePath = resolveToCwd(path2, cwd);
|
|
@@ -4865,345 +4040,105 @@ async function executeEdit(opts) {
|
|
|
4865
4040
|
return await withFileMutationQueue(absolutePath, async () => {
|
|
4866
4041
|
throwIfAborted(signal);
|
|
4867
4042
|
if (wasReadInSession && !wasReadInSession(absolutePath)) {
|
|
4868
|
-
const
|
|
4869
|
-
|
|
4870
|
-
`Call readSeek_read(${JSON.stringify(rawPath)}) first, or use readSeek_grep, readSeek_search, or readSeek_write to produce fresh anchors for this file.`,
|
|
4871
|
-
"readSeek_edit requires fresh LINE:HASH anchors from readSeek_read, readSeek_grep, readSeek_search, or readSeek_write so the hashes match the current file contents."
|
|
4872
|
-
].join(" ");
|
|
4873
|
-
return buildEditError(absolutePath, "file-not-read", message, `Call readSeek_read(${JSON.stringify(rawPath)}) first, or use readSeek_grep, readSeek_search, or readSeek_write to produce fresh anchors for this file.`);
|
|
4874
|
-
}
|
|
4875
|
-
const hasTopLevelReplaceInput = typeof input.oldText === "string" || typeof input.newText === "string" || typeof input.old_text === "string" || typeof input.new_text === "string";
|
|
4876
|
-
if (hasTopLevelReplaceInput) {
|
|
4877
|
-
return buildEditError(absolutePath, "invalid-edit-variant", "Top-level oldText/newText and old_text/new_text are no longer supported. Use edits[0].replace instead.", "Use edits: [{ replace: { old_text, new_text } }].");
|
|
4043
|
+
const hint = `Call readSeek_read(${JSON.stringify(rawPath)}) first, or use readSeek_grep, readSeek_search, or readSeek_write to produce fresh anchors for this file.`;
|
|
4044
|
+
return buildEditError(absolutePath, "file-not-read", `You must get fresh anchors for ${absolutePath} before editing it. ${hint}`, hint);
|
|
4878
4045
|
}
|
|
4879
4046
|
const edits = parsed.edits ?? [];
|
|
4880
4047
|
if (!edits.length) {
|
|
4881
4048
|
return buildEditError(absolutePath, "invalid-edit-variant", "No edits provided.");
|
|
4882
4049
|
}
|
|
4883
|
-
|
|
4884
|
-
|
|
4885
|
-
const edit = edits[
|
|
4886
|
-
|
|
4887
|
-
|
|
4888
|
-
return buildEditError(absolutePath, "invalid-edit-variant",
|
|
4889
|
-
}
|
|
4890
|
-
const e = edit;
|
|
4891
|
-
if ((("old_text" in e) || ("new_text" in e)) && !("replace" in e)) {
|
|
4892
|
-
const message = `edits[${i}] has top-level 'old_text'/'new_text'. Use {replace: {old_text, new_text}} or {set_line}, {replace_lines}, {insert_after}.`;
|
|
4893
|
-
return buildEditError(absolutePath, "invalid-edit-variant", message);
|
|
4894
|
-
}
|
|
4895
|
-
if ("diff" in e) {
|
|
4896
|
-
const message = `edits[${i}] contains 'diff' from patch mode. Hashline edit expects one of: {set_line}, {replace_lines}, {insert_after}, {replace}.`;
|
|
4897
|
-
return buildEditError(absolutePath, "invalid-edit-variant", message);
|
|
4898
|
-
}
|
|
4899
|
-
const variantCount = Number("set_line" in e) + Number("replace_lines" in e) + Number("insert_after" in e) + Number("replace" in e) + Number("replace_symbol" in e);
|
|
4900
|
-
if (variantCount !== 1 || Object.keys(e).length !== 1) {
|
|
4901
|
-
const message = `edits[${i}] must contain exactly one of: 'set_line', 'replace_lines', 'insert_after', 'replace', 'replace_symbol'. Got: [${Object.keys(e).join(", ")}].`;
|
|
4902
|
-
return buildEditError(absolutePath, "invalid-edit-variant", message);
|
|
4903
|
-
}
|
|
4904
|
-
}
|
|
4905
|
-
const anchorEdits = edits.filter((e) => ("set_line" in e) || ("replace_lines" in e) || ("insert_after" in e));
|
|
4906
|
-
const replaceEdits = edits.filter((e) => ("replace" in e));
|
|
4907
|
-
const replaceSymbolEdits = edits.filter((e) => ("replace_symbol" in e));
|
|
4908
|
-
for (const rs of replaceSymbolEdits) {
|
|
4909
|
-
if (!rs.replace_symbol.new_body.trim()) {
|
|
4910
|
-
return buildEditError(absolutePath, "invalid-edit-variant", "replace_symbol.new_body must not be empty or whitespace-only.");
|
|
4911
|
-
}
|
|
4912
|
-
}
|
|
4913
|
-
let rawBuffer;
|
|
4914
|
-
try {
|
|
4915
|
-
rawBuffer = await fsReadFile2(absolutePath);
|
|
4916
|
-
} catch (err) {
|
|
4917
|
-
return mapEditFileError(err, absolutePath, path2, "read");
|
|
4918
|
-
}
|
|
4919
|
-
if (looksLikeBinary(rawBuffer)) {
|
|
4920
|
-
const message = `Cannot edit binary file: ${path2}`;
|
|
4921
|
-
return buildEditError(absolutePath, "binary-file", message);
|
|
4922
|
-
}
|
|
4923
|
-
throwIfAborted(signal);
|
|
4924
|
-
const raw = rawBuffer.toString("utf-8");
|
|
4925
|
-
const { bom, text: content } = stripBom(raw);
|
|
4926
|
-
const originalEnding = detectLineEnding(content);
|
|
4927
|
-
const originalNormalized = normalizeToLF(content);
|
|
4928
|
-
const origLines = originalNormalized.split(`
|
|
4929
|
-
`);
|
|
4930
|
-
const anchorSnapshots = new Map;
|
|
4931
|
-
for (const edit of anchorEdits) {
|
|
4932
|
-
const refs = [];
|
|
4933
|
-
if ("set_line" in edit)
|
|
4934
|
-
refs.push(edit.set_line.anchor);
|
|
4935
|
-
else if ("replace_lines" in edit) {
|
|
4936
|
-
refs.push(edit.replace_lines.start_anchor, edit.replace_lines.end_anchor);
|
|
4937
|
-
} else if ("insert_after" in edit)
|
|
4938
|
-
refs.push(edit.insert_after.anchor);
|
|
4939
|
-
for (const ref of refs) {
|
|
4940
|
-
try {
|
|
4941
|
-
const parsed2 = parseLineRef(ref);
|
|
4942
|
-
if (parsed2.line >= 1 && parsed2.line <= origLines.length) {
|
|
4943
|
-
const lineContent = origLines[parsed2.line - 1];
|
|
4944
|
-
const hash = computeLineHash(lineContent);
|
|
4945
|
-
anchorSnapshots.set(ref, `${parsed2.line}:${hash}|${escapeControlCharsForDisplay(lineContent)}`);
|
|
4946
|
-
}
|
|
4947
|
-
} catch {}
|
|
4050
|
+
const variants = new Set(["set_line", "replace_lines", "insert_after", "replace", "replace_symbol"]);
|
|
4051
|
+
for (let index = 0;index < edits.length; index++) {
|
|
4052
|
+
const edit = edits[index];
|
|
4053
|
+
const keys = edit && typeof edit === "object" && !Array.isArray(edit) ? Object.keys(edit) : [];
|
|
4054
|
+
if (keys.length !== 1 || !variants.has(keys[0])) {
|
|
4055
|
+
return buildEditError(absolutePath, "invalid-edit-variant", `edits[${index}] must contain exactly one of: 'set_line', 'replace_lines', 'insert_after', 'replace', 'replace_symbol'. Got: [${keys.join(", ")}].`);
|
|
4948
4056
|
}
|
|
4949
4057
|
}
|
|
4950
|
-
let
|
|
4951
|
-
const replaceSymbolRanges = [];
|
|
4952
|
-
const rsProbeResults = [];
|
|
4058
|
+
let plan;
|
|
4953
4059
|
try {
|
|
4954
|
-
|
|
4955
|
-
throwIfAborted(signal);
|
|
4956
|
-
const probe = await replaceSymbol({
|
|
4957
|
-
filePath: absolutePath,
|
|
4958
|
-
content: originalNormalized,
|
|
4959
|
-
symbol: rs.replace_symbol.symbol,
|
|
4960
|
-
newBody: rs.replace_symbol.new_body
|
|
4961
|
-
});
|
|
4962
|
-
if (probe.type !== "ok") {
|
|
4963
|
-
return buildEditError(absolutePath, "invalid-edit-variant", probe.message);
|
|
4964
|
-
}
|
|
4965
|
-
rsProbeResults.push(probe);
|
|
4966
|
-
replaceSymbolRanges.push(probe.range);
|
|
4967
|
-
}
|
|
4060
|
+
plan = await readSeekEdit(absolutePath, edits, { signal, language: parsed.language });
|
|
4968
4061
|
} catch (err) {
|
|
4969
4062
|
throwIfAborted(signal);
|
|
4970
4063
|
const failure = classifyReadSeekFailure(err);
|
|
4971
|
-
|
|
4972
|
-
|
|
4973
|
-
const sortedReplaceSymbolRanges = [...replaceSymbolRanges].sort((a, b) => a.start - b.start || a.end - b.end);
|
|
4974
|
-
for (let i = 1;i < sortedReplaceSymbolRanges.length; i++) {
|
|
4975
|
-
const prev = sortedReplaceSymbolRanges[i - 1];
|
|
4976
|
-
const current = sortedReplaceSymbolRanges[i];
|
|
4977
|
-
if (current.start <= prev.end) {
|
|
4978
|
-
const message = `replace_symbol ranges overlap or duplicate (lines ${prev.start}-${prev.end} and ${current.start}-${current.end}).`;
|
|
4979
|
-
return buildEditError(absolutePath, "invalid-edit-variant", message);
|
|
4980
|
-
}
|
|
4064
|
+
const code = failure.message.includes("stale anchor") ? "hash-mismatch" : failure.code;
|
|
4065
|
+
return buildEditError(absolutePath, code, failure.message, failure.hint);
|
|
4981
4066
|
}
|
|
4982
|
-
if (
|
|
4983
|
-
|
|
4984
|
-
if ("replace_lines" in edit) {
|
|
4985
|
-
let startLine;
|
|
4986
|
-
let endLine;
|
|
4987
|
-
try {
|
|
4988
|
-
startLine = parseLineRef(edit.replace_lines.start_anchor).line;
|
|
4989
|
-
endLine = parseLineRef(edit.replace_lines.end_anchor).line;
|
|
4990
|
-
} catch {}
|
|
4991
|
-
if (startLine !== undefined && endLine !== undefined) {
|
|
4992
|
-
const lo = Math.min(startLine, endLine);
|
|
4993
|
-
const hi = Math.max(startLine, endLine);
|
|
4994
|
-
for (const range of replaceSymbolRanges) {
|
|
4995
|
-
if (lo <= range.end && hi >= range.start) {
|
|
4996
|
-
const message = `replace_lines range ${lo}-${hi} overlaps a replace_symbol range (lines ${range.start}-${range.end}).`;
|
|
4997
|
-
return buildEditError(absolutePath, "invalid-edit-variant", message);
|
|
4998
|
-
}
|
|
4999
|
-
}
|
|
5000
|
-
}
|
|
5001
|
-
}
|
|
5002
|
-
const refs = [];
|
|
5003
|
-
if ("set_line" in edit)
|
|
5004
|
-
refs.push(edit.set_line.anchor);
|
|
5005
|
-
else if ("replace_lines" in edit) {
|
|
5006
|
-
refs.push(edit.replace_lines.start_anchor, edit.replace_lines.end_anchor);
|
|
5007
|
-
} else if ("insert_after" in edit)
|
|
5008
|
-
refs.push(edit.insert_after.anchor);
|
|
5009
|
-
for (const ref of refs) {
|
|
5010
|
-
let parsedLine;
|
|
5011
|
-
try {
|
|
5012
|
-
parsedLine = parseLineRef(ref).line;
|
|
5013
|
-
} catch {
|
|
5014
|
-
continue;
|
|
5015
|
-
}
|
|
5016
|
-
for (const range of replaceSymbolRanges) {
|
|
5017
|
-
if (parsedLine >= range.start && parsedLine <= range.end) {
|
|
5018
|
-
const message = `Anchor at line ${parsedLine} falls inside a replace_symbol range (lines ${range.start}-${range.end}).`;
|
|
5019
|
-
return buildEditError(absolutePath, "invalid-edit-variant", message);
|
|
5020
|
-
}
|
|
5021
|
-
}
|
|
5022
|
-
}
|
|
5023
|
-
}
|
|
5024
|
-
}
|
|
5025
|
-
const anchorIntervals = [];
|
|
5026
|
-
for (const edit of anchorEdits) {
|
|
5027
|
-
try {
|
|
5028
|
-
if ("set_line" in edit) {
|
|
5029
|
-
const line = parseLineRef(edit.set_line.anchor).line;
|
|
5030
|
-
anchorIntervals.push({ lo: line, hi: line });
|
|
5031
|
-
} else if ("replace_lines" in edit) {
|
|
5032
|
-
const start = parseLineRef(edit.replace_lines.start_anchor).line;
|
|
5033
|
-
const end = parseLineRef(edit.replace_lines.end_anchor).line;
|
|
5034
|
-
anchorIntervals.push({ lo: Math.min(start, end), hi: Math.max(start, end) });
|
|
5035
|
-
}
|
|
5036
|
-
} catch {}
|
|
5037
|
-
}
|
|
5038
|
-
const seenAnchorIntervals = new Set;
|
|
5039
|
-
const uniqueAnchorIntervals = [];
|
|
5040
|
-
for (const interval of anchorIntervals) {
|
|
5041
|
-
const key = `${interval.lo}:${interval.hi}`;
|
|
5042
|
-
if (seenAnchorIntervals.has(key))
|
|
5043
|
-
continue;
|
|
5044
|
-
seenAnchorIntervals.add(key);
|
|
5045
|
-
uniqueAnchorIntervals.push(interval);
|
|
5046
|
-
}
|
|
5047
|
-
uniqueAnchorIntervals.sort((a, b) => a.lo - b.lo || a.hi - b.hi);
|
|
5048
|
-
for (let i = 1;i < uniqueAnchorIntervals.length; i++) {
|
|
5049
|
-
const prev = uniqueAnchorIntervals[i - 1];
|
|
5050
|
-
const current = uniqueAnchorIntervals[i];
|
|
5051
|
-
if (current.lo <= prev.hi) {
|
|
5052
|
-
const message = `Anchored edits overlap (lines ${prev.lo}-${prev.hi} and ${current.lo}-${current.hi}). Split into separate, non-overlapping edits or re-read for fresh anchors.`;
|
|
5053
|
-
return buildEditError(absolutePath, "invalid-edit-variant", message);
|
|
5054
|
-
}
|
|
5055
|
-
}
|
|
5056
|
-
const replaceSymbolWarnings = [];
|
|
5057
|
-
if (rsProbeResults.length > 0) {
|
|
5058
|
-
const lines = originalNormalized.split(`
|
|
5059
|
-
`);
|
|
5060
|
-
for (const probe of rsProbeResults) {
|
|
5061
|
-
replaceSymbolWarnings.push(...probe.warnings);
|
|
5062
|
-
}
|
|
5063
|
-
for (const probe of [...rsProbeResults].sort((a, b) => b.range.start - a.range.start)) {
|
|
5064
|
-
lines.splice(probe.range.start - 1, probe.range.end - probe.range.start + 1, ...probe.replacement.split(`
|
|
5065
|
-
`));
|
|
5066
|
-
}
|
|
5067
|
-
preAnchorContent = lines.join(`
|
|
5068
|
-
`);
|
|
5069
|
-
}
|
|
5070
|
-
let result = preAnchorContent;
|
|
5071
|
-
let anchorResult;
|
|
5072
|
-
try {
|
|
5073
|
-
anchorResult = applyHashlineEdits(result, anchorEdits, signal);
|
|
5074
|
-
} catch (err) {
|
|
5075
|
-
if (err instanceof HashlineMismatchError) {
|
|
5076
|
-
return buildEditError(absolutePath, "hash-mismatch", err.message, undefined, {
|
|
5077
|
-
updatedAnchors: err.updatedAnchors
|
|
5078
|
-
});
|
|
5079
|
-
}
|
|
5080
|
-
throw err;
|
|
4067
|
+
if (typeof plan.before_content !== "string" || typeof plan.content !== "string") {
|
|
4068
|
+
return buildEditError(absolutePath, "invalid-output", "readseek edit plan did not include before_content and content");
|
|
5081
4069
|
}
|
|
5082
|
-
|
|
5083
|
-
const
|
|
5084
|
-
|
|
5085
|
-
|
|
5086
|
-
|
|
5087
|
-
|
|
5088
|
-
return buildEditError(absolutePath, "invalid-edit-variant", message);
|
|
5089
|
-
}
|
|
5090
|
-
const rep = replaceText(result, r.replace.old_text, r.replace.new_text, {
|
|
5091
|
-
all: r.replace.all ?? false,
|
|
5092
|
-
fuzzy: r.replace.fuzzy ?? false
|
|
5093
|
-
});
|
|
5094
|
-
if (!rep.count) {
|
|
5095
|
-
const message = `Could not find exact text to replace in ${path2}.`;
|
|
5096
|
-
const hint = "Re-read the file and prefer set_line/replace_lines/insert_after for hash-verified edits. " + "The replace variant is exact-only by default because fuzzy fallback is unverified.";
|
|
5097
|
-
return buildEditError(absolutePath, "text-not-found", message, hint);
|
|
5098
|
-
}
|
|
5099
|
-
if (rep.usedFuzzyMatch) {
|
|
5100
|
-
replaceWarnings.push("replace used fuzzy matching because exact old_text was not found; re-read the file and prefer set_line/replace_lines/insert_after for hash-verified edits.");
|
|
5101
|
-
}
|
|
5102
|
-
result = rep.content;
|
|
5103
|
-
}
|
|
5104
|
-
if (originalNormalized === result) {
|
|
5105
|
-
let diagnostic = `No changes made to ${path2}. The edits produced identical content.`;
|
|
5106
|
-
if (anchorResult.noopEdits?.length) {
|
|
5107
|
-
diagnostic += `
|
|
5108
|
-
` + anchorResult.noopEdits.map((e) => `Edit ${e.editIndex}: replacement for ${e.loc} is identical to current content:
|
|
5109
|
-
${e.loc}| ${escapeControlCharsForDisplay(e.currentContent)}`).join(`
|
|
5110
|
-
`);
|
|
5111
|
-
diagnostic += `
|
|
5112
|
-
Re-read the file to see the current state.`;
|
|
5113
|
-
} else {
|
|
5114
|
-
const targetLines = [...new Set(anchorSnapshots.values())];
|
|
5115
|
-
if (targetLines.length > 0) {
|
|
5116
|
-
const preview = targetLines.slice(0, 5).join(`
|
|
5117
|
-
`);
|
|
5118
|
-
diagnostic += `
|
|
5119
|
-
The file currently contains:
|
|
5120
|
-
${preview}
|
|
5121
|
-
Your edits were normalized back to the original content. Ensure your replacement changes actual code, not just formatting.`;
|
|
5122
|
-
}
|
|
5123
|
-
}
|
|
5124
|
-
return buildEditError(absolutePath, "no-op", diagnostic);
|
|
4070
|
+
const original = plan.before_content;
|
|
4071
|
+
const result = plan.content;
|
|
4072
|
+
const originalForSyntax = normalizeToLF(stripBom(original).text);
|
|
4073
|
+
const resultForSyntax = normalizeToLF(stripBom(result).text);
|
|
4074
|
+
if (!plan.changed) {
|
|
4075
|
+
return buildEditError(absolutePath, "no-op", `No changes made to ${path2}. The edits produced identical content.`);
|
|
5125
4076
|
}
|
|
5126
|
-
throwIfAborted(signal);
|
|
5127
4077
|
const syntaxMode = resolveSyntaxValidateMode({ syntaxValidate });
|
|
5128
4078
|
let syntaxWarning;
|
|
5129
4079
|
if (syntaxMode !== "off") {
|
|
5130
4080
|
const regression = await validateSyntaxRegression({
|
|
5131
4081
|
filePath: absolutePath,
|
|
5132
|
-
before:
|
|
5133
|
-
after:
|
|
4082
|
+
before: originalForSyntax,
|
|
4083
|
+
after: resultForSyntax
|
|
5134
4084
|
}, { signal });
|
|
5135
4085
|
if (regression) {
|
|
5136
|
-
const
|
|
5137
|
-
const message = `syntax-regression: lines ${lines}`;
|
|
4086
|
+
const message = `syntax-regression: lines ${regression.errorLines.join(", ")}`;
|
|
5138
4087
|
if (syntaxMode === "block") {
|
|
5139
4088
|
return buildEditError(absolutePath, "syntax-regression", message);
|
|
5140
4089
|
}
|
|
5141
4090
|
syntaxWarning = message;
|
|
5142
4091
|
}
|
|
5143
4092
|
}
|
|
5144
|
-
|
|
4093
|
+
throwIfAborted(signal);
|
|
4094
|
+
let applied;
|
|
5145
4095
|
try {
|
|
5146
|
-
await
|
|
4096
|
+
applied = await readSeekEdit(absolutePath, edits, {
|
|
4097
|
+
apply: true,
|
|
4098
|
+
planHash: plan.plan_hash,
|
|
4099
|
+
language: parsed.language,
|
|
4100
|
+
cancelable: false
|
|
4101
|
+
});
|
|
5147
4102
|
} catch (err) {
|
|
5148
|
-
|
|
4103
|
+
throwIfAborted(signal);
|
|
4104
|
+
const failure = classifyReadSeekFailure(err);
|
|
4105
|
+
return buildEditError(absolutePath, failure.code, failure.message, failure.hint);
|
|
4106
|
+
}
|
|
4107
|
+
if (!applied.applied) {
|
|
4108
|
+
return buildEditError(absolutePath, "invalid-output", "readseek did not confirm that the edit was applied");
|
|
5149
4109
|
}
|
|
5150
4110
|
onFileMutated?.(absolutePath);
|
|
5151
|
-
if (
|
|
4111
|
+
if (parsed.postEditVerify === true) {
|
|
5152
4112
|
let verifiedContent;
|
|
5153
4113
|
try {
|
|
5154
|
-
|
|
5155
|
-
verifiedContent = verified;
|
|
4114
|
+
verifiedContent = await fsReadFile2(absolutePath, "utf-8");
|
|
5156
4115
|
} catch (err) {
|
|
5157
|
-
|
|
5158
|
-
return buildEditError(absolutePath, "post-edit-verification-read-failed", message, undefined, {
|
|
5159
|
-
fsCode: err?.code,
|
|
5160
|
-
fsMessage: err?.message
|
|
5161
|
-
});
|
|
4116
|
+
return buildEditError(absolutePath, "post-edit-verification-read-failed", `Edit write completed but post-edit verification failed: could not read ${path2} after writing.`, undefined, { fsCode: err?.code, fsMessage: err?.message });
|
|
5162
4117
|
}
|
|
5163
|
-
if (verifiedContent !==
|
|
5164
|
-
|
|
5165
|
-
return buildEditError(absolutePath, "post-edit-verification-mismatch", message, undefined, {
|
|
5166
|
-
expectedLength: writeContent.length,
|
|
5167
|
-
actualLength: verifiedContent.length
|
|
5168
|
-
});
|
|
4118
|
+
if (verifiedContent !== plan.content) {
|
|
4119
|
+
return buildEditError(absolutePath, "post-edit-verification-mismatch", `Edit write completed but post-edit verification did not confirm the intended content for ${path2}. Re-read the file before making follow-up edits.`, undefined, { expectedLength: plan.content.length, actualLength: verifiedContent.length });
|
|
5169
4120
|
}
|
|
5170
4121
|
}
|
|
5171
|
-
const diffResult = generateCompactOrFullDiff(
|
|
5172
|
-
const patch = createPatch(path2,
|
|
5173
|
-
const blockRanges = rsProbeResults.map((probe) => ({
|
|
5174
|
-
kind: "remove",
|
|
5175
|
-
startLine: probe.range.start,
|
|
5176
|
-
endLine: probe.range.end
|
|
5177
|
-
}));
|
|
4122
|
+
const diffResult = generateCompactOrFullDiff(original, result);
|
|
4123
|
+
const patch = createPatch(path2, original, result);
|
|
5178
4124
|
const diffData = buildDiffData({
|
|
5179
4125
|
path: absolutePath,
|
|
5180
|
-
oldContent:
|
|
4126
|
+
oldContent: original,
|
|
5181
4127
|
newContent: result,
|
|
5182
|
-
diff: diffResult.diff
|
|
5183
|
-
...blockRanges.length ? { blockRanges } : {}
|
|
4128
|
+
diff: diffResult.diff
|
|
5184
4129
|
});
|
|
5185
|
-
const warnings = [];
|
|
5186
|
-
|
|
5187
|
-
|
|
5188
|
-
if (replaceWarnings.length)
|
|
5189
|
-
warnings.push(...replaceWarnings);
|
|
5190
|
-
if (replaceSymbolWarnings.length)
|
|
5191
|
-
warnings.push(...replaceSymbolWarnings);
|
|
5192
|
-
if (syntaxWarning)
|
|
5193
|
-
warnings.push(syntaxWarning);
|
|
5194
|
-
const internalClassification = classifyEdit(originalNormalized, result);
|
|
5195
|
-
const semanticSummary = {
|
|
5196
|
-
classification: internalClassification.classification
|
|
5197
|
-
};
|
|
4130
|
+
const warnings = syntaxWarning ? [syntaxWarning] : [];
|
|
4131
|
+
const classification = classifyEdit(original, result);
|
|
4132
|
+
const semanticSummary = { classification: classification.classification };
|
|
5198
4133
|
const builtOutput = buildEditOutput({
|
|
5199
4134
|
path: absolutePath,
|
|
5200
4135
|
displayPath: path2,
|
|
5201
4136
|
diff: diffResult.diff,
|
|
5202
4137
|
patch,
|
|
5203
4138
|
diffData,
|
|
5204
|
-
firstChangedLine:
|
|
4139
|
+
firstChangedLine: diffResult.firstChangedLine,
|
|
5205
4140
|
warnings,
|
|
5206
|
-
noopEdits:
|
|
4141
|
+
noopEdits: [],
|
|
5207
4142
|
edits,
|
|
5208
4143
|
semanticSummary
|
|
5209
4144
|
});
|
|
@@ -5213,7 +4148,7 @@ Your edits were normalized back to the original content. Ensure your replacement
|
|
|
5213
4148
|
diff: diffResult.diff,
|
|
5214
4149
|
patch: builtOutput.patch,
|
|
5215
4150
|
diffData,
|
|
5216
|
-
firstChangedLine:
|
|
4151
|
+
firstChangedLine: diffResult.firstChangedLine,
|
|
5217
4152
|
readSeekValue: builtOutput.readSeekValue
|
|
5218
4153
|
}
|
|
5219
4154
|
};
|
|
@@ -6627,120 +5562,12 @@ ${linkedPathLine(theme, file.file, display, cwd, ` (${file.edits.length} edits)`
|
|
|
6627
5562
|
});
|
|
6628
5563
|
}
|
|
6629
5564
|
|
|
6630
|
-
// src/hover.ts
|
|
6631
|
-
import { readFile } from "node:fs/promises";
|
|
6632
|
-
import { Type as Type9 } from "@sinclair/typebox";
|
|
6633
|
-
import { Text as Text7 } from "@earendil-works/pi-tui";
|
|
6634
|
-
var HOVER_PROMPT_METADATA = defineToolPromptMetadata({
|
|
6635
|
-
promptUrl: new URL("../prompts/hover.md", import.meta.url),
|
|
6636
|
-
promptSnippet: "Inspect the symbol at a source location"
|
|
6637
|
-
});
|
|
6638
|
-
var hoverSchema = Type9.Object({
|
|
6639
|
-
path: filePathParam(),
|
|
6640
|
-
line: Type9.Integer({ minimum: 1, description: "One-based cursor line" }),
|
|
6641
|
-
column: Type9.Optional(Type9.Integer({ minimum: 1, description: "One-based cursor byte column for disambiguation" }))
|
|
6642
|
-
});
|
|
6643
|
-
async function executeHover(opts) {
|
|
6644
|
-
const { params, signal, cwd } = opts;
|
|
6645
|
-
const p = params;
|
|
6646
|
-
if (!Number.isSafeInteger(p.line) || p.line < 1) {
|
|
6647
|
-
return buildToolErrorResult("hover", "invalid-parameter", "hover parameter 'line' must be a positive integer");
|
|
6648
|
-
}
|
|
6649
|
-
if (p.column !== undefined && (!Number.isSafeInteger(p.column) || p.column < 1)) {
|
|
6650
|
-
return buildToolErrorResult("hover", "invalid-parameter", "hover parameter 'column' must be a positive integer");
|
|
6651
|
-
}
|
|
6652
|
-
const filePath = resolveToCwd(p.path, cwd);
|
|
6653
|
-
let content;
|
|
6654
|
-
try {
|
|
6655
|
-
content = await readFile(filePath, "utf-8");
|
|
6656
|
-
} catch (err) {
|
|
6657
|
-
const { code, message } = formatFsError(err, "hover-error");
|
|
6658
|
-
return buildToolErrorResult("hover", code, message, { path: p.path });
|
|
6659
|
-
}
|
|
6660
|
-
try {
|
|
6661
|
-
const output = await readSeekIdentify(filePath, content, {
|
|
6662
|
-
line: p.line,
|
|
6663
|
-
column: p.column,
|
|
6664
|
-
signal
|
|
6665
|
-
});
|
|
6666
|
-
const lines = [];
|
|
6667
|
-
if (output.identifier) {
|
|
6668
|
-
lines.push(`identifier: ${output.identifier.text}`);
|
|
6669
|
-
}
|
|
6670
|
-
if (output.symbol) {
|
|
6671
|
-
lines.push(`symbol: ${output.symbol.name}`);
|
|
6672
|
-
lines.push(`kind: ${output.symbol.kind}`);
|
|
6673
|
-
lines.push(`qualified: ${output.symbol.qualified_name}`);
|
|
6674
|
-
}
|
|
6675
|
-
lines.push(`file: ${output.file}`);
|
|
6676
|
-
lines.push(`language: ${output.language}`);
|
|
6677
|
-
lines.push(`location: ${output.line}:${output.column}`);
|
|
6678
|
-
return {
|
|
6679
|
-
content: [{ type: "text", text: lines.join(`
|
|
6680
|
-
`) }],
|
|
6681
|
-
details: {
|
|
6682
|
-
readSeekValue: {
|
|
6683
|
-
tool: "hover",
|
|
6684
|
-
ok: true,
|
|
6685
|
-
path: filePath,
|
|
6686
|
-
output
|
|
6687
|
-
}
|
|
6688
|
-
}
|
|
6689
|
-
};
|
|
6690
|
-
} catch (err) {
|
|
6691
|
-
const failure = classifyReadSeekFailure(err);
|
|
6692
|
-
return buildToolErrorResult("hover", failure.code, failure.message, failure.hint ? { hint: failure.hint } : {});
|
|
6693
|
-
}
|
|
6694
|
-
}
|
|
6695
|
-
function registerHoverTool(pi) {
|
|
6696
|
-
registerReadSeekTool(pi, {
|
|
6697
|
-
name: "readSeek_hover",
|
|
6698
|
-
label: "Hover",
|
|
6699
|
-
description: HOVER_PROMPT_METADATA.description,
|
|
6700
|
-
promptSnippet: HOVER_PROMPT_METADATA.promptSnippet,
|
|
6701
|
-
promptGuidelines: HOVER_PROMPT_METADATA.promptGuidelines,
|
|
6702
|
-
parameters: hoverSchema,
|
|
6703
|
-
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
6704
|
-
return executeHover({ params, signal, cwd: ctx.cwd });
|
|
6705
|
-
},
|
|
6706
|
-
renderCall(args, theme, ...rest) {
|
|
6707
|
-
const context = rest[0] ?? {};
|
|
6708
|
-
const cwd = context.cwd ?? process.cwd();
|
|
6709
|
-
const displayPath = typeof args?.path === "string" ? args.path : "?";
|
|
6710
|
-
let text = theme.fg("toolTitle", theme.bold("hover"));
|
|
6711
|
-
text += ` ${linkToolPath(theme.fg("accent", displayPath), displayPath, cwd)}`;
|
|
6712
|
-
if (args?.line)
|
|
6713
|
-
text += theme.fg("dim", `:${args.line}`);
|
|
6714
|
-
return new Text7(clampLineToWidth(text, context.width), 0, 0);
|
|
6715
|
-
},
|
|
6716
|
-
renderResult(result, options, theme, ...rest) {
|
|
6717
|
-
const { isPartial, isError, expanded, width } = resolveRenderResultContext(options, rest);
|
|
6718
|
-
if (isPartial)
|
|
6719
|
-
return renderPendingResult("pending hover", width, theme);
|
|
6720
|
-
const content = result.content?.[0];
|
|
6721
|
-
const textContent = content?.type === "text" ? content.text : "";
|
|
6722
|
-
if (isError || result.isError) {
|
|
6723
|
-
return renderErrorResult(textContent, { expanded, width, fallback: "hover failed", theme });
|
|
6724
|
-
}
|
|
6725
|
-
const output = result.details?.readSeekValue?.output;
|
|
6726
|
-
const label = output?.identifier?.text ? `identified ${output.identifier.text}` : output?.symbol?.name ? `symbol ${output.symbol.name}` : "identified cursor";
|
|
6727
|
-
let text = summaryLine(label, { hidden: !!textContent && !expanded, theme, style: "success" });
|
|
6728
|
-
if (expanded && textContent)
|
|
6729
|
-
text += `
|
|
6730
|
-
${textContent}`;
|
|
6731
|
-
return new Text7(clampLinesToWidth(text.split(`
|
|
6732
|
-
`), width).join(`
|
|
6733
|
-
`), 0, 0);
|
|
6734
|
-
}
|
|
6735
|
-
});
|
|
6736
|
-
}
|
|
6737
|
-
|
|
6738
5565
|
// src/write.ts
|
|
6739
|
-
import { mkdir, readFile
|
|
5566
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
6740
5567
|
import { dirname as dirname2, relative as relative3 } from "node:path";
|
|
6741
5568
|
import { withFileMutationQueue as withFileMutationQueue2, truncateHead as truncateHead4 } from "@earendil-works/pi-coding-agent";
|
|
6742
|
-
import { Text as
|
|
6743
|
-
import { Type as
|
|
5569
|
+
import { Text as Text7 } from "@earendil-works/pi-tui";
|
|
5570
|
+
import { Type as Type9 } from "@sinclair/typebox";
|
|
6744
5571
|
var WRITE_PENDING_PREVIEW_STATE_KEY = "hashline-write-pending-preview";
|
|
6745
5572
|
var CONTENT_PREVIEW_MAX_LINES = 200;
|
|
6746
5573
|
function formatContentPreviewLines(content, theme) {
|
|
@@ -6780,7 +5607,7 @@ function isToolErrorResult(result) {
|
|
|
6780
5607
|
}
|
|
6781
5608
|
async function readPreviousTextForDiff(filePath) {
|
|
6782
5609
|
try {
|
|
6783
|
-
const previous = await
|
|
5610
|
+
const previous = await readFile(filePath);
|
|
6784
5611
|
return {
|
|
6785
5612
|
content: looksLikeBinary(previous) ? "" : previous.toString("utf-8"),
|
|
6786
5613
|
existed: true
|
|
@@ -6818,7 +5645,7 @@ function buildWriteFsErrorResult(err, absolutePath) {
|
|
|
6818
5645
|
}
|
|
6819
5646
|
async function executeWrite(opts) {
|
|
6820
5647
|
await ensureHashInit();
|
|
6821
|
-
const { path: filePath, content,
|
|
5648
|
+
const { path: filePath, content, cwd } = opts;
|
|
6822
5649
|
const warnings = [];
|
|
6823
5650
|
const readSeekWarnings = [];
|
|
6824
5651
|
if (hasBareCarriageReturn(content)) {
|
|
@@ -6844,8 +5671,15 @@ async function executeWrite(opts) {
|
|
|
6844
5671
|
const { content: previousContent, existed: existedBeforeWrite } = await readPreviousTextForDiff(filePath);
|
|
6845
5672
|
await mkdir(dirname2(filePath), { recursive: true });
|
|
6846
5673
|
await writeFile(filePath, content, "utf-8");
|
|
6847
|
-
const
|
|
5674
|
+
const normalizedContent = stripBom(content).text;
|
|
5675
|
+
const rawLines = normalizedContent === "" ? [] : normalizedContent.split(`
|
|
6848
5676
|
`);
|
|
5677
|
+
if (rawLines[rawLines.length - 1] === "")
|
|
5678
|
+
rawLines.pop();
|
|
5679
|
+
for (let i = 0;i < rawLines.length; i++) {
|
|
5680
|
+
if (rawLines[i].endsWith("\r"))
|
|
5681
|
+
rawLines[i] = rawLines[i].slice(0, -1);
|
|
5682
|
+
}
|
|
6849
5683
|
const readSeekLines = [];
|
|
6850
5684
|
const displayLines = [];
|
|
6851
5685
|
for (let i = 0;i < rawLines.length; i++) {
|
|
@@ -6867,21 +5701,6 @@ async function executeWrite(opts) {
|
|
|
6867
5701
|
[… output truncated at 50 KB — full anchors in readSeekValue]`;
|
|
6868
5702
|
}
|
|
6869
5703
|
}
|
|
6870
|
-
let mapAppended = false;
|
|
6871
|
-
if (requestMap) {
|
|
6872
|
-
try {
|
|
6873
|
-
const fileMap = await getOrGenerateMap(filePath);
|
|
6874
|
-
if (fileMap) {
|
|
6875
|
-
const mapText = formatFileMapWithBudget(fileMap);
|
|
6876
|
-
if (mapText) {
|
|
6877
|
-
text += `
|
|
6878
|
-
|
|
6879
|
-
` + mapText;
|
|
6880
|
-
mapAppended = true;
|
|
6881
|
-
}
|
|
6882
|
-
}
|
|
6883
|
-
} catch {}
|
|
6884
|
-
}
|
|
6885
5704
|
const displayPath = cwd ? relative3(cwd, filePath) || filePath : filePath;
|
|
6886
5705
|
const normalizedPrevious = normalizeToLF(previousContent);
|
|
6887
5706
|
const normalizedNext = normalizeToLF(content);
|
|
@@ -6904,8 +5723,7 @@ async function executeWrite(opts) {
|
|
|
6904
5723
|
lines: readSeekLines,
|
|
6905
5724
|
warnings: readSeekWarnings,
|
|
6906
5725
|
diff: diffResult.diff,
|
|
6907
|
-
diffData
|
|
6908
|
-
...requestMap ? { map: { appended: mapAppended } } : {}
|
|
5726
|
+
diffData
|
|
6909
5727
|
}
|
|
6910
5728
|
};
|
|
6911
5729
|
}
|
|
@@ -6922,10 +5740,9 @@ function registerWriteTool(pi, options = {}) {
|
|
|
6922
5740
|
description: promptMetadata.description,
|
|
6923
5741
|
promptSnippet: promptMetadata.promptSnippet,
|
|
6924
5742
|
promptGuidelines: promptMetadata.promptGuidelines,
|
|
6925
|
-
parameters:
|
|
5743
|
+
parameters: Type9.Object({
|
|
6926
5744
|
path: filePathParam(),
|
|
6927
|
-
content:
|
|
6928
|
-
map: mapParam()
|
|
5745
|
+
content: Type9.String({ description: "Complete text file content" })
|
|
6929
5746
|
}),
|
|
6930
5747
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
6931
5748
|
const cwd = ctx?.cwd ?? process.cwd();
|
|
@@ -6937,7 +5754,6 @@ function registerWriteTool(pi, options = {}) {
|
|
|
6937
5754
|
result = await executeWrite({
|
|
6938
5755
|
path: absolutePath,
|
|
6939
5756
|
content: params.content,
|
|
6940
|
-
map: params.map,
|
|
6941
5757
|
cwd
|
|
6942
5758
|
});
|
|
6943
5759
|
} catch (err) {
|
|
@@ -7007,7 +5823,7 @@ function registerWriteTool(pi, options = {}) {
|
|
|
7007
5823
|
`);
|
|
7008
5824
|
lines.push(...formatContentPreviewLines(content, theme));
|
|
7009
5825
|
}
|
|
7010
|
-
return new
|
|
5826
|
+
return new Text7(clampLinesToWidth(lines, width).join(`
|
|
7011
5827
|
`), 0, 0);
|
|
7012
5828
|
}
|
|
7013
5829
|
const hasExpandableDiff = !!diffData;
|
|
@@ -7016,7 +5832,7 @@ function registerWriteTool(pi, options = {}) {
|
|
|
7016
5832
|
return upsertDiffComponent(context.lastComponent, { prefixLines: text.split(`
|
|
7017
5833
|
`), diffData, theme, expanded: true });
|
|
7018
5834
|
}
|
|
7019
|
-
return new
|
|
5835
|
+
return new Text7(clampLinesToWidth(text.split(`
|
|
7020
5836
|
`), width).join(`
|
|
7021
5837
|
`), 0, 0);
|
|
7022
5838
|
}
|
|
@@ -7025,7 +5841,7 @@ function registerWriteTool(pi, options = {}) {
|
|
|
7025
5841
|
}
|
|
7026
5842
|
|
|
7027
5843
|
// src/def.ts
|
|
7028
|
-
import { Type as
|
|
5844
|
+
import { Type as Type10 } from "@sinclair/typebox";
|
|
7029
5845
|
import path6 from "node:path";
|
|
7030
5846
|
var DEF_PROMPT_METADATA = defineToolPromptMetadata({
|
|
7031
5847
|
promptUrl: new URL("../prompts/def.md", import.meta.url),
|
|
@@ -7101,8 +5917,8 @@ function registerDefTool(pi, options = {}) {
|
|
|
7101
5917
|
description: DEF_PROMPT_METADATA.description,
|
|
7102
5918
|
promptSnippet: DEF_PROMPT_METADATA.promptSnippet,
|
|
7103
5919
|
promptGuidelines: DEF_PROMPT_METADATA.promptGuidelines,
|
|
7104
|
-
parameters:
|
|
7105
|
-
name:
|
|
5920
|
+
parameters: Type10.Object({
|
|
5921
|
+
name: Type10.String({ description: "Qualified or unqualified symbol name" }),
|
|
7106
5922
|
path: searchPathParam(),
|
|
7107
5923
|
lang: langParam(),
|
|
7108
5924
|
...readSeekGitSearchParams()
|
|
@@ -7128,77 +5944,205 @@ function registerDefTool(pi, options = {}) {
|
|
|
7128
5944
|
});
|
|
7129
5945
|
}
|
|
7130
5946
|
|
|
7131
|
-
// src/
|
|
7132
|
-
import {
|
|
7133
|
-
import {
|
|
7134
|
-
|
|
7135
|
-
var
|
|
7136
|
-
promptUrl: new URL("../prompts/
|
|
7137
|
-
promptSnippet: "
|
|
5947
|
+
// src/digest.ts
|
|
5948
|
+
import { Text as Text8 } from "@earendil-works/pi-tui";
|
|
5949
|
+
import { Type as Type11 } from "@sinclair/typebox";
|
|
5950
|
+
var DIGEST_FACETS = ["metadata", "content", "map", "diagnostics", "identity"];
|
|
5951
|
+
var DIGEST_PROMPT_METADATA = defineToolPromptMetadata({
|
|
5952
|
+
promptUrl: new URL("../prompts/digest.md", import.meta.url),
|
|
5953
|
+
promptSnippet: "Digest source facets as a native CLI envelope"
|
|
7138
5954
|
});
|
|
7139
|
-
|
|
7140
|
-
|
|
5955
|
+
var facetSchema = Type11.Union(DIGEST_FACETS.map((facet) => Type11.Literal(facet)));
|
|
5956
|
+
var digestSchema = Type11.Object({
|
|
5957
|
+
path: filePathParam(),
|
|
5958
|
+
select: Type11.Optional(Type11.Union([
|
|
5959
|
+
facetSchema,
|
|
5960
|
+
Type11.Array(facetSchema, { minItems: 1 }),
|
|
5961
|
+
Type11.String({
|
|
5962
|
+
description: "Comma-separated facets: metadata, content, map, diagnostics, identity"
|
|
5963
|
+
})
|
|
5964
|
+
], {
|
|
5965
|
+
description: "Facets: metadata, content, map, diagnostics, identity (default content)"
|
|
5966
|
+
})),
|
|
5967
|
+
at: Type11.Optional(Type11.String({
|
|
5968
|
+
description: "Location: line:N[:COLUMN], hash:HASH[:COLUMN], or symbol:NAME"
|
|
5969
|
+
})),
|
|
5970
|
+
end: optionalIntOrString("Last line to include"),
|
|
5971
|
+
limit: optionalIntOrString("Maximum number of lines to include"),
|
|
5972
|
+
language: Type11.Optional(Type11.String({ description: "Language override" })),
|
|
5973
|
+
visionMode: Type11.Optional(Type11.Union([
|
|
5974
|
+
Type11.Literal("none"),
|
|
5975
|
+
Type11.Literal("all"),
|
|
5976
|
+
Type11.Literal("ocr"),
|
|
5977
|
+
Type11.Literal("caption"),
|
|
5978
|
+
Type11.Literal("objects")
|
|
5979
|
+
], { description: "Standalone image mode: none, all, caption, objects, or ocr" })),
|
|
5980
|
+
visionLevel: Type11.Optional(Type11.Union([Type11.Literal("low"), Type11.Literal("medium"), Type11.Literal("high")], { description: "Vision inference level: low (default), medium, or high" }))
|
|
5981
|
+
});
|
|
5982
|
+
function hasDigestAnchors(value) {
|
|
5983
|
+
if (!value || typeof value !== "object")
|
|
5984
|
+
return false;
|
|
5985
|
+
const record = value;
|
|
5986
|
+
if (Array.isArray(record.hashlines) && record.hashlines.length > 0)
|
|
5987
|
+
return true;
|
|
5988
|
+
return Object.values(record).some(hasDigestAnchors);
|
|
5989
|
+
}
|
|
5990
|
+
function parseSelect(select) {
|
|
5991
|
+
if (select === undefined)
|
|
5992
|
+
return;
|
|
5993
|
+
if (Array.isArray(select)) {
|
|
5994
|
+
const facets = select;
|
|
5995
|
+
if (facets.length === 0)
|
|
5996
|
+
throw new Error("select must include at least one facet");
|
|
5997
|
+
for (const facet of facets) {
|
|
5998
|
+
if (!DIGEST_FACETS.includes(facet)) {
|
|
5999
|
+
throw new Error(`unknown digest facet \`${facet}\`; expected ${DIGEST_FACETS.join(", ")}`);
|
|
6000
|
+
}
|
|
6001
|
+
}
|
|
6002
|
+
return facets;
|
|
6003
|
+
}
|
|
6004
|
+
if (typeof select !== "string") {
|
|
6005
|
+
throw new Error("select must be a facet, facet list, or comma-separated facets");
|
|
6006
|
+
}
|
|
6007
|
+
if (DIGEST_FACETS.includes(select)) {
|
|
6008
|
+
return select;
|
|
6009
|
+
}
|
|
6010
|
+
const parts = select.split(",").map((part) => part.trim()).filter((part) => part.length > 0);
|
|
6011
|
+
if (parts.length === 0)
|
|
6012
|
+
throw new Error("select must include at least one facet");
|
|
6013
|
+
for (const part of parts) {
|
|
6014
|
+
if (!DIGEST_FACETS.includes(part)) {
|
|
6015
|
+
throw new Error(`unknown digest facet \`${part}\`; expected ${DIGEST_FACETS.join(", ")}`);
|
|
6016
|
+
}
|
|
6017
|
+
}
|
|
6018
|
+
return parts;
|
|
6019
|
+
}
|
|
6020
|
+
function selectedFacets(select) {
|
|
6021
|
+
if (select === undefined)
|
|
6022
|
+
return ["content"];
|
|
6023
|
+
return Array.isArray(select) ? select : [select];
|
|
6024
|
+
}
|
|
6025
|
+
function summarizeEnvelope(envelope, facets) {
|
|
6026
|
+
if (!envelope || typeof envelope !== "object")
|
|
6027
|
+
return `digested ${facets.join(",")}`;
|
|
6028
|
+
const record = envelope;
|
|
6029
|
+
const parts = [];
|
|
6030
|
+
for (const facet of facets) {
|
|
6031
|
+
if (facet === "metadata" || record[facet] !== undefined)
|
|
6032
|
+
parts.push(facet);
|
|
6033
|
+
}
|
|
6034
|
+
if (record.metadata !== undefined && !parts.includes("metadata"))
|
|
6035
|
+
parts.unshift("metadata");
|
|
6036
|
+
return `digested ${parts.join(",") || "metadata"}`;
|
|
6037
|
+
}
|
|
6038
|
+
async function executeDigest(opts) {
|
|
6039
|
+
const { params, signal, cwd, onFileAnchored } = opts;
|
|
7141
6040
|
const p = params;
|
|
7142
|
-
|
|
7143
|
-
let content;
|
|
6041
|
+
let select;
|
|
7144
6042
|
try {
|
|
7145
|
-
|
|
6043
|
+
select = parseSelect(p.select);
|
|
7146
6044
|
} catch (err) {
|
|
7147
|
-
|
|
7148
|
-
|
|
6045
|
+
return buildToolErrorResult("digest", "invalid-parameter", err?.message ?? String(err), {
|
|
6046
|
+
path: p.path
|
|
6047
|
+
});
|
|
6048
|
+
}
|
|
6049
|
+
const end = coerceObviousBase10Int(p.end, "end");
|
|
6050
|
+
if (!end.ok || end.value !== undefined && end.value < 1) {
|
|
6051
|
+
const message = end.ok ? `Invalid end: expected a positive integer, received ${end.value}.` : end.message;
|
|
6052
|
+
return buildToolErrorResult("digest", "invalid-parameter", message, { path: p.path });
|
|
6053
|
+
}
|
|
6054
|
+
const limit = coerceObviousBase10Int(p.limit, "limit");
|
|
6055
|
+
if (!limit.ok || limit.value !== undefined && limit.value < 1) {
|
|
6056
|
+
const message = limit.ok ? `Invalid limit: expected a positive integer, received ${limit.value}.` : limit.message;
|
|
6057
|
+
return buildToolErrorResult("digest", "invalid-parameter", message, { path: p.path });
|
|
6058
|
+
}
|
|
6059
|
+
const at = typeof p.at === "string" ? p.at.trim() : undefined;
|
|
6060
|
+
if (p.at !== undefined && !at) {
|
|
6061
|
+
return buildToolErrorResult("digest", "invalid-parameter", "Invalid at: expected a non-empty location.", {
|
|
6062
|
+
path: p.path
|
|
6063
|
+
});
|
|
6064
|
+
}
|
|
6065
|
+
if (p.visionLevel !== undefined && p.visionMode === undefined) {
|
|
6066
|
+
return buildToolErrorResult("digest", "invalid-params-combo", "visionLevel requires visionMode.", {
|
|
6067
|
+
path: p.path
|
|
6068
|
+
});
|
|
7149
6069
|
}
|
|
6070
|
+
const filePath = resolveToCwd(p.path, cwd);
|
|
6071
|
+
const facets = selectedFacets(select);
|
|
6072
|
+
const vision = p.visionMode !== undefined && p.visionMode !== "none";
|
|
7150
6073
|
try {
|
|
7151
|
-
const
|
|
7152
|
-
|
|
7153
|
-
|
|
6074
|
+
const envelope = await readSeekDigestNative(filePath, {
|
|
6075
|
+
select,
|
|
6076
|
+
at,
|
|
6077
|
+
end: end.value,
|
|
6078
|
+
limit: limit.value,
|
|
6079
|
+
language: p.language,
|
|
6080
|
+
visionMode: p.visionMode,
|
|
6081
|
+
visionLevel: p.visionLevel,
|
|
6082
|
+
signal,
|
|
6083
|
+
vision
|
|
6084
|
+
});
|
|
6085
|
+
if (hasDigestAnchors(envelope))
|
|
6086
|
+
onFileAnchored?.(filePath);
|
|
6087
|
+
const text = JSON.stringify(envelope);
|
|
7154
6088
|
return {
|
|
7155
|
-
content: [{ type: "text", text
|
|
7156
|
-
`) }],
|
|
6089
|
+
content: [{ type: "text", text }],
|
|
7157
6090
|
details: {
|
|
7158
|
-
readSeekValue: {
|
|
6091
|
+
readSeekValue: {
|
|
6092
|
+
tool: "digest",
|
|
6093
|
+
ok: true,
|
|
6094
|
+
path: filePath,
|
|
6095
|
+
select: facets,
|
|
6096
|
+
envelope
|
|
6097
|
+
}
|
|
7159
6098
|
}
|
|
7160
6099
|
};
|
|
7161
6100
|
} catch (err) {
|
|
7162
6101
|
const failure = classifyReadSeekFailure(err);
|
|
7163
|
-
return buildToolErrorResult("
|
|
6102
|
+
return buildToolErrorResult("digest", failure.code, failure.message, failure.hint ? { hint: failure.hint } : {});
|
|
7164
6103
|
}
|
|
7165
6104
|
}
|
|
7166
|
-
function
|
|
6105
|
+
function registerDigestTool(pi, options = {}) {
|
|
7167
6106
|
return registerReadSeekTool(pi, {
|
|
7168
|
-
name: "
|
|
7169
|
-
label: "
|
|
7170
|
-
description:
|
|
7171
|
-
promptSnippet:
|
|
7172
|
-
promptGuidelines:
|
|
7173
|
-
parameters:
|
|
6107
|
+
name: "readSeek_digest",
|
|
6108
|
+
label: "Digest",
|
|
6109
|
+
description: DIGEST_PROMPT_METADATA.description,
|
|
6110
|
+
promptSnippet: DIGEST_PROMPT_METADATA.promptSnippet,
|
|
6111
|
+
promptGuidelines: DIGEST_PROMPT_METADATA.promptGuidelines,
|
|
6112
|
+
parameters: digestSchema,
|
|
7174
6113
|
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
7175
|
-
return
|
|
6114
|
+
return executeDigest({ params, signal, cwd: ctx.cwd, onFileAnchored: options.onFileAnchored });
|
|
7176
6115
|
},
|
|
7177
6116
|
renderCall(args, theme, ...rest) {
|
|
7178
6117
|
const context = rest[0] ?? {};
|
|
7179
6118
|
const cwd = context.cwd ?? process.cwd();
|
|
7180
6119
|
const displayPath = typeof args?.path === "string" ? args.path : "?";
|
|
7181
|
-
|
|
7182
|
-
|
|
6120
|
+
let text = theme.fg("toolTitle", theme.bold("digest"));
|
|
6121
|
+
text += ` ${linkToolPath(theme.fg("accent", displayPath), displayPath, cwd)}`;
|
|
6122
|
+
if (args?.select !== undefined) {
|
|
6123
|
+
const select = Array.isArray(args.select) ? args.select.join(",") : String(args.select);
|
|
6124
|
+
text += theme.fg("dim", ` [${select}]`);
|
|
6125
|
+
}
|
|
6126
|
+
if (typeof args?.at === "string" && args.at)
|
|
6127
|
+
text += theme.fg("dim", ` @ ${args.at}`);
|
|
6128
|
+
return new Text8(clampLineToWidth(text, context.width), 0, 0);
|
|
7183
6129
|
},
|
|
7184
|
-
renderResult(result,
|
|
7185
|
-
const { isPartial, isError, expanded, width } = resolveRenderResultContext(
|
|
6130
|
+
renderResult(result, options2, theme, ...rest) {
|
|
6131
|
+
const { isPartial, isError, expanded, width } = resolveRenderResultContext(options2, rest);
|
|
7186
6132
|
if (isPartial)
|
|
7187
|
-
return renderPendingResult("pending
|
|
6133
|
+
return renderPendingResult("pending digest", width, theme);
|
|
7188
6134
|
const textContent = result.content?.[0]?.type === "text" ? result.content[0].text : "";
|
|
7189
6135
|
if (isError || result.isError) {
|
|
7190
|
-
return renderErrorResult(textContent, { expanded, width, fallback: "
|
|
6136
|
+
return renderErrorResult(textContent, { expanded, width, fallback: "digest failed", theme });
|
|
7191
6137
|
}
|
|
7192
|
-
const
|
|
7193
|
-
const
|
|
7194
|
-
|
|
7195
|
-
|
|
7196
|
-
style: issueCount === 0 ? "success" : "warning"
|
|
7197
|
-
});
|
|
6138
|
+
const value = result.details?.readSeekValue;
|
|
6139
|
+
const facets = Array.isArray(value?.select) ? value.select : ["content"];
|
|
6140
|
+
const label = summarizeEnvelope(value?.envelope, facets);
|
|
6141
|
+
let text = summaryLine(label, { hidden: !!textContent && !expanded, theme, style: "success" });
|
|
7198
6142
|
if (expanded && textContent)
|
|
7199
6143
|
text += `
|
|
7200
6144
|
${textContent}`;
|
|
7201
|
-
return new
|
|
6145
|
+
return new Text8(clampLinesToWidth(text.split(`
|
|
7202
6146
|
`), width).join(`
|
|
7203
6147
|
`), 0, 0);
|
|
7204
6148
|
}
|
|
@@ -7211,8 +6155,8 @@ import {
|
|
|
7211
6155
|
DEFAULT_MAX_LINES as DEFAULT_MAX_LINES5,
|
|
7212
6156
|
truncateHead as truncateHead5
|
|
7213
6157
|
} from "@earendil-works/pi-coding-agent";
|
|
7214
|
-
import { Text as
|
|
7215
|
-
import { Type as
|
|
6158
|
+
import { Text as Text9 } from "@earendil-works/pi-tui";
|
|
6159
|
+
import { Type as Type12 } from "@sinclair/typebox";
|
|
7216
6160
|
var NODE_KINDS = [
|
|
7217
6161
|
"artifact",
|
|
7218
6162
|
"footer",
|
|
@@ -7227,7 +6171,7 @@ var NODE_KINDS = [
|
|
|
7227
6171
|
];
|
|
7228
6172
|
var VIEW_PROMPT_METADATA = defineToolPromptMetadata({
|
|
7229
6173
|
promptUrl: new URL("../prompts/view.md", import.meta.url),
|
|
7230
|
-
promptSnippet: "
|
|
6174
|
+
promptSnippet: "Digest a document's structure or selected content"
|
|
7231
6175
|
});
|
|
7232
6176
|
async function executeView(opts) {
|
|
7233
6177
|
const { params, signal, cwd } = opts;
|
|
@@ -7248,6 +6192,12 @@ async function executeView(opts) {
|
|
|
7248
6192
|
path: input.path
|
|
7249
6193
|
});
|
|
7250
6194
|
}
|
|
6195
|
+
if (input.outline && (input.visionMode !== undefined || input.visionLevel !== undefined)) {
|
|
6196
|
+
return buildToolErrorResult("view", "invalid-params-combo", "Cannot combine outline with visionMode or visionLevel.", { path: input.path });
|
|
6197
|
+
}
|
|
6198
|
+
if (input.visionLevel !== undefined && input.visionMode === undefined) {
|
|
6199
|
+
return buildToolErrorResult("view", "invalid-params-combo", "visionLevel requires visionMode.", { path: input.path });
|
|
6200
|
+
}
|
|
7251
6201
|
const filePath = resolveToCwd(input.path, cwd);
|
|
7252
6202
|
try {
|
|
7253
6203
|
const output = await readSeekView(filePath, {
|
|
@@ -7256,6 +6206,8 @@ async function executeView(opts) {
|
|
|
7256
6206
|
kind: input.kind,
|
|
7257
6207
|
depth: depth.value,
|
|
7258
6208
|
outline: input.outline,
|
|
6209
|
+
visionMode: input.visionMode,
|
|
6210
|
+
visionLevel: input.visionLevel,
|
|
7259
6211
|
signal
|
|
7260
6212
|
});
|
|
7261
6213
|
const truncation = truncateHead5(output, {
|
|
@@ -7286,15 +6238,26 @@ function registerViewTool(pi) {
|
|
|
7286
6238
|
description: VIEW_PROMPT_METADATA.description,
|
|
7287
6239
|
promptSnippet: VIEW_PROMPT_METADATA.promptSnippet,
|
|
7288
6240
|
promptGuidelines: VIEW_PROMPT_METADATA.promptGuidelines,
|
|
7289
|
-
parameters:
|
|
6241
|
+
parameters: Type12.Object({
|
|
7290
6242
|
path: filePathParam(),
|
|
7291
|
-
node:
|
|
7292
|
-
page: optionalIntOrString("One-based
|
|
7293
|
-
kind:
|
|
6243
|
+
node: Type12.Optional(Type12.String({ description: "Node ID to use as the view root" })),
|
|
6244
|
+
page: optionalIntOrString("One-based source page"),
|
|
6245
|
+
kind: Type12.Optional(Type12.Union(NODE_KINDS.map((kind) => Type12.Literal(kind)), {
|
|
7294
6246
|
description: "Node kind filter"
|
|
7295
6247
|
})),
|
|
7296
6248
|
depth: optionalIntOrString("Maximum depth below selected roots"),
|
|
7297
|
-
|
|
6249
|
+
visionMode: Type12.Optional(Type12.Union([
|
|
6250
|
+
Type12.Literal("all"),
|
|
6251
|
+
Type12.Literal("ocr"),
|
|
6252
|
+
Type12.Literal("caption"),
|
|
6253
|
+
Type12.Literal("objects")
|
|
6254
|
+
], { description: "Analyze selected assets: all, ocr, caption, or objects" })),
|
|
6255
|
+
visionLevel: Type12.Optional(Type12.Union([
|
|
6256
|
+
Type12.Literal("low"),
|
|
6257
|
+
Type12.Literal("medium"),
|
|
6258
|
+
Type12.Literal("high")
|
|
6259
|
+
], { description: "Vision inference level: low (default), medium, or high" })),
|
|
6260
|
+
outline: Type12.Optional(Type12.Boolean({ description: "Return outline nodes only" }))
|
|
7298
6261
|
}),
|
|
7299
6262
|
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
7300
6263
|
return executeView({ params, signal, cwd: ctx.cwd });
|
|
@@ -7304,7 +6267,7 @@ function registerViewTool(pi) {
|
|
|
7304
6267
|
const cwd = context.cwd ?? process.cwd();
|
|
7305
6268
|
const displayPath = typeof args?.path === "string" ? args.path : "?";
|
|
7306
6269
|
const text = `${theme.fg("toolTitle", theme.bold("view"))} ${linkToolPath(theme.fg("accent", displayPath), displayPath, cwd)}`;
|
|
7307
|
-
return new
|
|
6270
|
+
return new Text9(clampLineToWidth(text, context.width), 0, 0);
|
|
7308
6271
|
},
|
|
7309
6272
|
renderResult(result, options, theme, ...rest) {
|
|
7310
6273
|
const { isPartial, isError, expanded, width } = resolveRenderResultContext(options, rest);
|
|
@@ -7324,7 +6287,7 @@ function registerViewTool(pi) {
|
|
|
7324
6287
|
if (expanded && textContent)
|
|
7325
6288
|
text += `
|
|
7326
6289
|
${textContent}`;
|
|
7327
|
-
return new
|
|
6290
|
+
return new Text9(clampLinesToWidth(text.split(`
|
|
7328
6291
|
`), width).join(`
|
|
7329
6292
|
`), 0, 0);
|
|
7330
6293
|
}
|
|
@@ -7356,10 +6319,9 @@ var READSEEK_TOOL_ENTRIES = [
|
|
|
7356
6319
|
{ builtIn: null, readSeekName: "readSeek_search" },
|
|
7357
6320
|
{ builtIn: null, readSeekName: "readSeek_refs" },
|
|
7358
6321
|
{ builtIn: null, readSeekName: "readSeek_rename" },
|
|
7359
|
-
{ builtIn: null, readSeekName: "readSeek_hover" },
|
|
7360
6322
|
{ builtIn: "write", readSeekName: "readSeek_write" },
|
|
7361
6323
|
{ builtIn: null, readSeekName: "readSeek_def" },
|
|
7362
|
-
{ builtIn: null, readSeekName: "
|
|
6324
|
+
{ builtIn: null, readSeekName: "readSeek_digest" },
|
|
7363
6325
|
{ builtIn: null, readSeekName: "readSeek_view" }
|
|
7364
6326
|
];
|
|
7365
6327
|
function formatSettingsWarning(warning) {
|
|
@@ -7370,7 +6332,7 @@ function editingPolicy(readName, editName, writeName) {
|
|
|
7370
6332
|
"ReadSeek editing policy:",
|
|
7371
6333
|
`- Read first with ${readName} when preparing to edit; ${editName} needs fresh LINE:HASH anchors.`,
|
|
7372
6334
|
`- Use ${editName} for existing files, ${writeName} for whole-file creation or replacement, and readSeek_rename for symbol renames.`,
|
|
7373
|
-
"- Run
|
|
6335
|
+
"- Run readSeek_digest with select diagnostics after source edits for a quick syntax check."
|
|
7374
6336
|
].join(`
|
|
7375
6337
|
`);
|
|
7376
6338
|
}
|
|
@@ -7380,7 +6342,7 @@ function piReadSeekExtension(pi) {
|
|
|
7380
6342
|
const hasFreshAnchors = (absolutePath) => sessionAnchors.hasFreshAnchors(absolutePath);
|
|
7381
6343
|
const forgetAnchors = (absolutePath) => sessionAnchors.forget(absolutePath);
|
|
7382
6344
|
const { settings } = resolveReadSeekJsonSettings();
|
|
7383
|
-
const replacedBuiltIns = new Set(settings.
|
|
6345
|
+
const replacedBuiltIns = new Set(settings.overrideTools ?? []);
|
|
7384
6346
|
const binaryAvailable = readSeekBinaryAvailability().available;
|
|
7385
6347
|
const swap = (builtIn, readSeekName) => binaryAvailable && replacedBuiltIns.has(builtIn) ? builtIn : readSeekName;
|
|
7386
6348
|
const readName = swap("read", "readSeek_read");
|
|
@@ -7399,9 +6361,8 @@ function piReadSeekExtension(pi) {
|
|
|
7399
6361
|
registerSearchTool(pi, { onFileAnchored: markAnchored });
|
|
7400
6362
|
registerRefsTool(pi, { onFileAnchored: markAnchored });
|
|
7401
6363
|
registerRenameTool(pi, { onFileMutated: forgetAnchors });
|
|
7402
|
-
registerHoverTool(pi);
|
|
7403
6364
|
registerDefTool(pi, { onFileAnchored: markAnchored });
|
|
7404
|
-
|
|
6365
|
+
registerDigestTool(pi, { onFileAnchored: markAnchored });
|
|
7405
6366
|
registerViewTool(pi);
|
|
7406
6367
|
registerWriteTool(pi, { onFileAnchored: markAnchored, name: writeName });
|
|
7407
6368
|
pi.on("before_agent_start", (event) => {
|