pi-readseek 0.8.24 → 0.9.1
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 +641 -1682
- 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,7 +828,11 @@ 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
|
|
831
|
+
const cancelable = options.cancelable !== false;
|
|
832
|
+
const child = spawn(readSeekBinaryPath(), args, {
|
|
833
|
+
stdio,
|
|
834
|
+
signal: cancelable ? options.signal : undefined
|
|
835
|
+
});
|
|
1557
836
|
const timeoutMs = options.timeoutMs ?? readSeekTimeoutMs();
|
|
1558
837
|
timeout = setTimeout(() => {
|
|
1559
838
|
child.kill("SIGKILL");
|
|
@@ -1644,10 +923,15 @@ async function readSeekView(filePath, options = {}) {
|
|
|
1644
923
|
args.push("--depth", String(options.depth));
|
|
1645
924
|
if (options.outline)
|
|
1646
925
|
args.push("--outline");
|
|
1647
|
-
|
|
926
|
+
if (options.visionMode !== undefined)
|
|
927
|
+
args.push("--vision-mode", options.visionMode);
|
|
928
|
+
if (options.visionLevel !== undefined)
|
|
929
|
+
args.push("--vision-level", options.visionLevel);
|
|
930
|
+
const runOptions = { signal: options.signal };
|
|
931
|
+
return options.visionMode !== undefined ? runReadSeekVisionRaw(args, runOptions) : runReadSeekRaw(args, runOptions);
|
|
1648
932
|
}
|
|
1649
933
|
var visionInvocationTail = Promise.resolve();
|
|
1650
|
-
async function
|
|
934
|
+
async function runReadSeekVisionRaw(args, options = {}) {
|
|
1651
935
|
let release;
|
|
1652
936
|
const gate = new Promise((resolve) => {
|
|
1653
937
|
release = resolve;
|
|
@@ -1676,7 +960,7 @@ async function runReadSeekVision(args, options = {}) {
|
|
|
1676
960
|
await predecessor;
|
|
1677
961
|
}
|
|
1678
962
|
signal?.throwIfAborted();
|
|
1679
|
-
return await
|
|
963
|
+
return await runReadSeekRaw(args, {
|
|
1680
964
|
...options,
|
|
1681
965
|
timeoutMs: options.timeoutMs ?? resolveReadSeekTimeoutMs() ?? DEFAULT_READSEEK_VISION_TIMEOUT_MS
|
|
1682
966
|
});
|
|
@@ -1684,6 +968,9 @@ async function runReadSeekVision(args, options = {}) {
|
|
|
1684
968
|
release();
|
|
1685
969
|
}
|
|
1686
970
|
}
|
|
971
|
+
async function runReadSeekVision(args, options = {}) {
|
|
972
|
+
return JSON.parse(await runReadSeekVisionRaw(args, options));
|
|
973
|
+
}
|
|
1687
974
|
function requireNumber(value, field) {
|
|
1688
975
|
if (typeof value !== "number" || !Number.isSafeInteger(value))
|
|
1689
976
|
throw new Error(`invalid readseek ${field}: expected safe integer`);
|
|
@@ -1808,39 +1095,6 @@ function parseSearchOutput(value) {
|
|
|
1808
1095
|
})
|
|
1809
1096
|
};
|
|
1810
1097
|
}
|
|
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
1098
|
function optionalString(value, field) {
|
|
1845
1099
|
if (value === undefined || value === null)
|
|
1846
1100
|
return;
|
|
@@ -1870,19 +1124,6 @@ function parseRefsOutput(value) {
|
|
|
1870
1124
|
})
|
|
1871
1125
|
};
|
|
1872
1126
|
}
|
|
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
1127
|
function parseDiagnosticKind(value) {
|
|
1887
1128
|
if (value === "error" || value === "missing")
|
|
1888
1129
|
return value;
|
|
@@ -1909,9 +1150,6 @@ function parseCheckOutput(value) {
|
|
|
1909
1150
|
})
|
|
1910
1151
|
};
|
|
1911
1152
|
}
|
|
1912
|
-
async function readSeekCheck(filePath, content, options = {}) {
|
|
1913
|
-
return parseCheckOutput(await runReadSeek(["check", `stdin:${filePath}`], { signal: options.signal, stdin: content }));
|
|
1914
|
-
}
|
|
1915
1153
|
function parseOcrText(value) {
|
|
1916
1154
|
return optionalString(value, "ocr");
|
|
1917
1155
|
}
|
|
@@ -1933,6 +1171,11 @@ function parseDetectedObjects(value) {
|
|
|
1933
1171
|
};
|
|
1934
1172
|
});
|
|
1935
1173
|
}
|
|
1174
|
+
function parseImageEncoding(value) {
|
|
1175
|
+
if (value === "base64")
|
|
1176
|
+
return value;
|
|
1177
|
+
throw new Error("invalid image encoding");
|
|
1178
|
+
}
|
|
1936
1179
|
function parseDetectOutput(value) {
|
|
1937
1180
|
if (!value || typeof value !== "object")
|
|
1938
1181
|
throw new Error("invalid readseek detect output");
|
|
@@ -1983,20 +1226,228 @@ function parseDetectOutput(value) {
|
|
|
1983
1226
|
}
|
|
1984
1227
|
return { kind: "text", type, file, mime };
|
|
1985
1228
|
}
|
|
1986
|
-
function
|
|
1987
|
-
if (value
|
|
1988
|
-
|
|
1989
|
-
|
|
1229
|
+
function parseDocumentCache(value) {
|
|
1230
|
+
if (!value || typeof value !== "object")
|
|
1231
|
+
throw new Error("invalid readseek document cache");
|
|
1232
|
+
const cache = value;
|
|
1233
|
+
const state = requireString(cache.state, "cache.state");
|
|
1234
|
+
if (state !== "built" && state !== "reused")
|
|
1235
|
+
throw new Error("invalid readseek cache.state");
|
|
1236
|
+
return {
|
|
1237
|
+
content_hash: requireString(cache.content_hash, "cache.content_hash"),
|
|
1238
|
+
state
|
|
1239
|
+
};
|
|
1240
|
+
}
|
|
1241
|
+
function parseIdentifyOutput(value) {
|
|
1242
|
+
if (!value || typeof value !== "object")
|
|
1243
|
+
throw new Error("invalid readseek identify output");
|
|
1244
|
+
const output = value;
|
|
1245
|
+
const identifier = output.identifier;
|
|
1246
|
+
const symbol = output.symbol;
|
|
1247
|
+
return {
|
|
1248
|
+
file: requireString(output.file, "file"),
|
|
1249
|
+
language: requireString(output.language, "language"),
|
|
1250
|
+
engine: optionalString(output.engine, "engine"),
|
|
1251
|
+
line_count: requireNumber(output.line_count, "line_count"),
|
|
1252
|
+
file_hash: requireString(output.file_hash, "file_hash"),
|
|
1253
|
+
line: requireNumber(output.line, "line"),
|
|
1254
|
+
column: requireNumber(output.column, "column"),
|
|
1255
|
+
line_hash: requireString(output.line_hash, "line_hash"),
|
|
1256
|
+
identifier: identifier && typeof identifier === "object" ? {
|
|
1257
|
+
text: requireString(identifier.text, "identifier.text"),
|
|
1258
|
+
start_column: requireNumber(identifier.start_column, "identifier.start_column"),
|
|
1259
|
+
end_column: requireNumber(identifier.end_column, "identifier.end_column"),
|
|
1260
|
+
start_byte: requireNumber(identifier.start_byte, "identifier.start_byte"),
|
|
1261
|
+
end_byte: requireNumber(identifier.end_byte, "identifier.end_byte")
|
|
1262
|
+
} : undefined,
|
|
1263
|
+
symbol: symbol && typeof symbol === "object" ? {
|
|
1264
|
+
name: requireString(symbol.name, "symbol.name"),
|
|
1265
|
+
kind: requireString(symbol.kind, "symbol.kind"),
|
|
1266
|
+
qualified_name: requireString(symbol.qualified_name, "symbol.qualified_name"),
|
|
1267
|
+
start_line: requireNumber(symbol.start_line, "symbol.start_line"),
|
|
1268
|
+
end_line: requireNumber(symbol.end_line, "symbol.end_line")
|
|
1269
|
+
} : undefined
|
|
1270
|
+
};
|
|
1271
|
+
}
|
|
1272
|
+
function parseImageContent(value) {
|
|
1273
|
+
const detection = parseDetectOutput(value);
|
|
1274
|
+
if (detection.kind !== "image")
|
|
1275
|
+
throw new Error("invalid readseek image content");
|
|
1276
|
+
return detection;
|
|
1277
|
+
}
|
|
1278
|
+
function parseTextContent(value) {
|
|
1279
|
+
if (!value || typeof value !== "object")
|
|
1280
|
+
throw new Error("invalid readseek content");
|
|
1281
|
+
const output = value;
|
|
1282
|
+
if (output.hashlines !== undefined && output.start_line === undefined) {
|
|
1283
|
+
const hashlines = parseSearchHashlines(output.hashlines, "content.hashlines");
|
|
1284
|
+
const first = hashlines[0];
|
|
1285
|
+
const last = hashlines[hashlines.length - 1];
|
|
1286
|
+
return {
|
|
1287
|
+
file: requireString(output.file, "file"),
|
|
1288
|
+
language: requireString(output.language, "language"),
|
|
1289
|
+
line_count: requireNumber(output.line_count, "line_count"),
|
|
1290
|
+
file_hash: requireString(output.file_hash, "file_hash"),
|
|
1291
|
+
start_line: first?.line ?? 1,
|
|
1292
|
+
end_line: last?.line ?? 0,
|
|
1293
|
+
hashlines
|
|
1294
|
+
};
|
|
1295
|
+
}
|
|
1296
|
+
return parseReadOutput(value);
|
|
1297
|
+
}
|
|
1298
|
+
function parseDigestEnvelope(value) {
|
|
1299
|
+
if (!value || typeof value !== "object")
|
|
1300
|
+
throw new Error("invalid readseek digest output");
|
|
1301
|
+
const output = value;
|
|
1302
|
+
if (output.metadata === undefined)
|
|
1303
|
+
throw new Error("invalid readseek digest metadata");
|
|
1304
|
+
const result = {
|
|
1305
|
+
metadata: parseDetectOutput(output.metadata)
|
|
1306
|
+
};
|
|
1307
|
+
if (output.cache !== undefined)
|
|
1308
|
+
result.cache = parseDocumentCache(output.cache);
|
|
1309
|
+
if (output.content !== undefined)
|
|
1310
|
+
result.content = output.content;
|
|
1311
|
+
if (output.map !== undefined)
|
|
1312
|
+
result.map = parseMapOutput(output.map);
|
|
1313
|
+
if (output.diagnostics !== undefined)
|
|
1314
|
+
result.diagnostics = parseCheckOutput(output.diagnostics);
|
|
1315
|
+
if (output.identity !== undefined)
|
|
1316
|
+
result.identity = parseIdentifyOutput(output.identity);
|
|
1317
|
+
return result;
|
|
1318
|
+
}
|
|
1319
|
+
function digestSelectArg(select) {
|
|
1320
|
+
if (select === undefined)
|
|
1321
|
+
return "content";
|
|
1322
|
+
return Array.isArray(select) ? select.join(",") : select;
|
|
1323
|
+
}
|
|
1324
|
+
function digestArgs(target, options) {
|
|
1325
|
+
const args = ["digest", target, "--select", digestSelectArg(options.select)];
|
|
1326
|
+
if (options.at !== undefined)
|
|
1327
|
+
args.push("--at", options.at);
|
|
1328
|
+
if (options.end !== undefined)
|
|
1329
|
+
args.push("--end", String(options.end));
|
|
1330
|
+
if (options.limit !== undefined)
|
|
1331
|
+
args.push("--limit", String(options.limit));
|
|
1332
|
+
if (options.language)
|
|
1333
|
+
args.push("--language", options.language);
|
|
1334
|
+
if (options.visionMode !== undefined)
|
|
1335
|
+
args.push("--vision-mode", options.visionMode);
|
|
1336
|
+
if (options.visionLevel !== undefined)
|
|
1337
|
+
args.push("--vision-level", options.visionLevel);
|
|
1338
|
+
if (options.stdin !== undefined) {
|
|
1339
|
+
const stdinName = options.stdinName ?? target;
|
|
1340
|
+
if (target === "-")
|
|
1341
|
+
args.push("--stdin-name", stdinName);
|
|
1342
|
+
}
|
|
1343
|
+
return args;
|
|
1344
|
+
}
|
|
1345
|
+
async function runDigestRaw(target, options = {}) {
|
|
1346
|
+
const args = digestArgs(target, options);
|
|
1347
|
+
const runOptions = {
|
|
1348
|
+
signal: options.signal,
|
|
1349
|
+
stdin: options.stdin,
|
|
1350
|
+
timeoutMs: options.timeoutMs
|
|
1351
|
+
};
|
|
1352
|
+
return options.vision ? await runReadSeekVision(args, runOptions) : await runReadSeek(args, runOptions);
|
|
1353
|
+
}
|
|
1354
|
+
async function readSeekDigestNative(target, options = {}) {
|
|
1355
|
+
return runDigestRaw(target, options);
|
|
1356
|
+
}
|
|
1357
|
+
async function readSeekDigest(target, options = {}) {
|
|
1358
|
+
return parseDigestEnvelope(await runDigestRaw(target, options));
|
|
1359
|
+
}
|
|
1360
|
+
function requireDigestContent(envelope, field) {
|
|
1361
|
+
if (envelope.content === undefined)
|
|
1362
|
+
throw new Error(`invalid readseek ${field}: missing content`);
|
|
1363
|
+
return envelope.content;
|
|
1364
|
+
}
|
|
1365
|
+
function fileMapFromReadSeekOutput(output, filePath, totalBytes) {
|
|
1366
|
+
if (output.language === "unknown" && output.symbols.length === 0)
|
|
1367
|
+
return null;
|
|
1368
|
+
return {
|
|
1369
|
+
path: filePath,
|
|
1370
|
+
totalLines: output.line_count,
|
|
1371
|
+
totalBytes,
|
|
1372
|
+
language: normalizeLanguage(output.language),
|
|
1373
|
+
detailLevel: "full" /* Full */,
|
|
1374
|
+
symbols: symbolsFromReadSeek(output.symbols)
|
|
1375
|
+
};
|
|
1376
|
+
}
|
|
1377
|
+
async function readSeekMap(filePath, totalBytes, options = {}) {
|
|
1378
|
+
const envelope = await readSeekDigest(filePath, { select: "map", signal: options.signal });
|
|
1379
|
+
if (!envelope.map)
|
|
1380
|
+
return null;
|
|
1381
|
+
return fileMapFromReadSeekOutput(envelope.map, filePath, totalBytes);
|
|
1382
|
+
}
|
|
1383
|
+
async function readSeekSearch(target, pattern, options = {}) {
|
|
1384
|
+
const args = ["search", target, pattern];
|
|
1385
|
+
if (options.language)
|
|
1386
|
+
args.push("--language", options.language);
|
|
1387
|
+
appendGitSelection(args, options);
|
|
1388
|
+
return parseSearchOutput(await runReadSeek(args, { signal: options.signal })).results;
|
|
1389
|
+
}
|
|
1390
|
+
async function readSeekRefs(target, name, options = {}) {
|
|
1391
|
+
const args = ["refs", target];
|
|
1392
|
+
if (options.scope) {
|
|
1393
|
+
if (options.line !== undefined)
|
|
1394
|
+
args.push("--at", lineLocationSelector(options.line, options.column));
|
|
1395
|
+
} else {
|
|
1396
|
+
args.push(name);
|
|
1397
|
+
}
|
|
1398
|
+
if (options.language)
|
|
1399
|
+
args.push("--language", options.language);
|
|
1400
|
+
appendGitSelection(args, options);
|
|
1401
|
+
return parseRefsOutput(await runReadSeek(args, { signal: options.signal })).references;
|
|
1402
|
+
}
|
|
1403
|
+
async function readSeekCheck(filePath, content, options = {}) {
|
|
1404
|
+
const envelope = await readSeekDigest("-", {
|
|
1405
|
+
select: "diagnostics",
|
|
1406
|
+
stdin: content,
|
|
1407
|
+
stdinName: filePath,
|
|
1408
|
+
signal: options.signal
|
|
1409
|
+
});
|
|
1410
|
+
if (!envelope.diagnostics)
|
|
1411
|
+
throw new Error("invalid readseek diagnostics: missing diagnostics facet");
|
|
1412
|
+
return envelope.diagnostics;
|
|
1990
1413
|
}
|
|
1991
1414
|
async function readSeekDetect(filePath, options = {}) {
|
|
1992
|
-
|
|
1415
|
+
const envelope = await readSeekDigest(filePath, { select: "metadata", signal: options.signal });
|
|
1416
|
+
return envelope.metadata;
|
|
1417
|
+
}
|
|
1418
|
+
async function readSeekDigestContent(filePath, options = {}) {
|
|
1419
|
+
const envelope = await readSeekDigest(filePath, {
|
|
1420
|
+
select: "content",
|
|
1421
|
+
at: options.startLine !== undefined ? lineLocationSelector(options.startLine) : undefined,
|
|
1422
|
+
end: options.endLine,
|
|
1423
|
+
visionMode: options.visionMode,
|
|
1424
|
+
signal: options.signal,
|
|
1425
|
+
vision: options.vision
|
|
1426
|
+
});
|
|
1427
|
+
if (envelope.metadata.kind === "pdf") {
|
|
1428
|
+
return { metadata: envelope.metadata, cache: envelope.cache };
|
|
1429
|
+
}
|
|
1430
|
+
const content = requireDigestContent(envelope, "content");
|
|
1431
|
+
if (envelope.metadata.kind === "image") {
|
|
1432
|
+
return { metadata: envelope.metadata, image: parseImageContent(content) };
|
|
1433
|
+
}
|
|
1434
|
+
return { metadata: envelope.metadata, text: parseTextContent(content) };
|
|
1993
1435
|
}
|
|
1994
1436
|
async function readSeekImage(filePath, modes, options = {}) {
|
|
1995
1437
|
const requestedModes = modes.length > 1 ? ["all"] : modes;
|
|
1996
|
-
const results = await Promise.allSettled(requestedModes.map(async (mode) =>
|
|
1438
|
+
const results = await Promise.allSettled(requestedModes.map(async (mode) => {
|
|
1439
|
+
const digested = await readSeekDigestContent(filePath, {
|
|
1440
|
+
visionMode: mode,
|
|
1441
|
+
signal: options.signal,
|
|
1442
|
+
vision: true
|
|
1443
|
+
});
|
|
1444
|
+
if (!digested.image)
|
|
1445
|
+
throw new Error(`readseek returned no image analysis for ${filePath}`);
|
|
1446
|
+
return digested.image;
|
|
1447
|
+
}));
|
|
1997
1448
|
let merged;
|
|
1998
1449
|
for (const result of results) {
|
|
1999
|
-
if (result.status !== "fulfilled"
|
|
1450
|
+
if (result.status !== "fulfilled")
|
|
2000
1451
|
continue;
|
|
2001
1452
|
const detection = result.value;
|
|
2002
1453
|
merged = merged === undefined ? detection : {
|
|
@@ -2014,52 +1465,46 @@ async function readSeekImage(filePath, modes, options = {}) {
|
|
|
2014
1465
|
throw new Error(`readseek returned no image analysis for ${filePath}`);
|
|
2015
1466
|
}
|
|
2016
1467
|
async function readSeekPreparedImage(filePath, options = {}) {
|
|
2017
|
-
const
|
|
2018
|
-
|
|
1468
|
+
const digested = await readSeekDigestContent(filePath, {
|
|
1469
|
+
visionMode: "none",
|
|
1470
|
+
signal: options.signal
|
|
1471
|
+
});
|
|
1472
|
+
const output = digested.image;
|
|
1473
|
+
if (!output || output.encoding !== "base64" || output.data === undefined || output.mime === undefined) {
|
|
2019
1474
|
throw new Error(`readseek returned no prepared image for ${filePath}`);
|
|
2020
1475
|
}
|
|
2021
1476
|
return { mime: output.mime, encoding: output.encoding, data: output.data };
|
|
2022
1477
|
}
|
|
2023
|
-
function
|
|
2024
|
-
if (!value || typeof value !== "object")
|
|
2025
|
-
throw new Error("invalid readseek PDF image");
|
|
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) {
|
|
1478
|
+
function parseEditOutput(value) {
|
|
2045
1479
|
if (!value || typeof value !== "object")
|
|
2046
|
-
throw new Error("invalid readseek
|
|
1480
|
+
throw new Error("invalid readseek edit output");
|
|
2047
1481
|
const output = value;
|
|
2048
|
-
if (output.format !== "pdf" || !Array.isArray(output.images))
|
|
2049
|
-
throw new Error("invalid readseek PDF output");
|
|
2050
1482
|
return {
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
1483
|
+
file: requireString(output.file, "file"),
|
|
1484
|
+
before_hash: requireString(output.before_hash, "before_hash"),
|
|
1485
|
+
file_hash: requireString(output.file_hash, "file_hash"),
|
|
1486
|
+
plan_hash: requireString(output.plan_hash, "plan_hash"),
|
|
1487
|
+
applied: requireBoolean(output.applied, "applied"),
|
|
1488
|
+
changed: requireBoolean(output.changed, "changed"),
|
|
1489
|
+
edit_count: requireNumber(output.edit_count, "edit_count"),
|
|
1490
|
+
before_content: optionalString(output.before_content, "before_content"),
|
|
1491
|
+
content: optionalString(output.content, "content")
|
|
2055
1492
|
};
|
|
2056
1493
|
}
|
|
2057
|
-
async function
|
|
2058
|
-
const
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
1494
|
+
async function readSeekEdit(filePath, edits, options = {}) {
|
|
1495
|
+
const args = ["edit", filePath];
|
|
1496
|
+
if (options.apply)
|
|
1497
|
+
args.push("--apply");
|
|
1498
|
+
if (options.planHash)
|
|
1499
|
+
args.push("--plan-hash", options.planHash);
|
|
1500
|
+
if (options.language)
|
|
1501
|
+
args.push("--language", options.language);
|
|
1502
|
+
return parseEditOutput(await runReadSeek(args, {
|
|
1503
|
+
signal: options.signal,
|
|
1504
|
+
stdin: JSON.stringify({ edits }),
|
|
1505
|
+
timeoutMs: options.timeoutMs,
|
|
1506
|
+
cancelable: options.cancelable
|
|
1507
|
+
}));
|
|
2063
1508
|
}
|
|
2064
1509
|
function parseRenameConflicts(value, field) {
|
|
2065
1510
|
if (value === undefined)
|
|
@@ -2131,10 +1576,7 @@ function parseRenameOutput(value) {
|
|
|
2131
1576
|
};
|
|
2132
1577
|
}
|
|
2133
1578
|
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);
|
|
1579
|
+
const args = ["rename", filePath, "--at", lineLocationSelector(options.line, options.column), "--to", options.to];
|
|
2138
1580
|
if (options.apply)
|
|
2139
1581
|
args.push("--apply");
|
|
2140
1582
|
if (options.workspace)
|
|
@@ -2144,46 +1586,6 @@ async function readSeekRename(filePath, options) {
|
|
|
2144
1586
|
appendGitSelection(args, options);
|
|
2145
1587
|
return parseRenameOutput(await runReadSeek(args, { signal: options.signal }));
|
|
2146
1588
|
}
|
|
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
1589
|
function parseDefCompact(value) {
|
|
2188
1590
|
if (!value || typeof value !== "object")
|
|
2189
1591
|
throw new Error("invalid readseek def output");
|
|
@@ -2871,9 +2273,6 @@ function formatReadCallText(args) {
|
|
|
2871
2273
|
if (typeof args?.symbol === "string" && args.symbol) {
|
|
2872
2274
|
return { path: rawPath, suffix: `→ symbol ${args.symbol}` };
|
|
2873
2275
|
}
|
|
2874
|
-
if (args?.map === true) {
|
|
2875
|
-
return { path: rawPath, suffix: "+ map" };
|
|
2876
|
-
}
|
|
2877
2276
|
if (typeof args?.offset === "number") {
|
|
2878
2277
|
const offset = args.offset;
|
|
2879
2278
|
const limit = typeof args?.limit === "number" ? args.limit : undefined;
|
|
@@ -3162,9 +2561,6 @@ function optionalIntOrString(description) {
|
|
|
3162
2561
|
function filePathParam() {
|
|
3163
2562
|
return Type.String({ description: "Absolute or relative file path" });
|
|
3164
2563
|
}
|
|
3165
|
-
function mapParam() {
|
|
3166
|
-
return Type.Optional(Type.Boolean({ description: "Append structural map" }));
|
|
3167
|
-
}
|
|
3168
2564
|
function registerReadSeekTool(pi, tool) {
|
|
3169
2565
|
pi.registerTool(tool);
|
|
3170
2566
|
return tool;
|
|
@@ -3211,50 +2607,9 @@ ${lines.join(`
|
|
|
3211
2607
|
return "No image analysis returned.";
|
|
3212
2608
|
return "Image caption returned no text.";
|
|
3213
2609
|
}
|
|
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]`;
|
|
2610
|
+
function documentRedirectMessage(path2, cache) {
|
|
2611
|
+
const cacheNote = cache ? ` Index ${cache.state} (hash ${cache.content_hash}).` : "";
|
|
2612
|
+
return `Document content is not available via readSeek_read for ${path2}.${cacheNote} Use readSeek_view to digest structure or selected content.`;
|
|
3258
2613
|
}
|
|
3259
2614
|
function skippedImageFile(path2) {
|
|
3260
2615
|
return {
|
|
@@ -3282,25 +2637,14 @@ async function executeRead(opts) {
|
|
|
3282
2637
|
const message = `Invalid offset: expected a positive integer, received ${offset.value}.`;
|
|
3283
2638
|
return buildToolErrorResult("read", "invalid-offset", message, { path: rawParams.path });
|
|
3284
2639
|
}
|
|
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
2640
|
const rawSymbol = typeof rawParams.symbol === "string" && rawParams.symbol.trim().length > 0 ? rawParams.symbol.trim() : undefined;
|
|
3294
2641
|
const rawBundle = typeof rawParams.bundle === "string" ? rawParams.bundle.trim() : undefined;
|
|
3295
|
-
const requestedMapViaBundle = rawBundle === "map" || rawBundle === "local" && rawSymbol === undefined && (rawParams.map ?? true);
|
|
3296
2642
|
const p = {
|
|
3297
2643
|
...rawParams,
|
|
3298
2644
|
offset: offset.value,
|
|
3299
2645
|
limit: limit.value,
|
|
3300
|
-
page: page.value,
|
|
3301
2646
|
symbol: rawSymbol,
|
|
3302
|
-
|
|
3303
|
-
bundle: requestedMapViaBundle ? undefined : rawBundle
|
|
2647
|
+
bundle: rawBundle
|
|
3304
2648
|
};
|
|
3305
2649
|
const rawPath = p.path.replace(/^@/, "");
|
|
3306
2650
|
const absolutePath = resolveToCwd(rawPath, cwd);
|
|
@@ -3324,14 +2668,6 @@ async function executeRead(opts) {
|
|
|
3324
2668
|
const message = 'Cannot use bundle without symbol. Use read({ path, symbol, bundle: "local" }).';
|
|
3325
2669
|
return buildToolErrorResult("read", "invalid-params-combo", message, { path: rawParams.path });
|
|
3326
2670
|
}
|
|
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
2671
|
throwIfAborted(signal);
|
|
3336
2672
|
const ext = rawPath.split(".").pop()?.toLowerCase() ?? "";
|
|
3337
2673
|
let rawBuffer;
|
|
@@ -3345,27 +2681,31 @@ async function executeRead(opts) {
|
|
|
3345
2681
|
}
|
|
3346
2682
|
const hasBinaryContent = looksLikeBinary(rawBuffer);
|
|
3347
2683
|
const hasPdfHeader = rawBuffer.subarray(0, 1024).includes(Buffer.from("%PDF-"));
|
|
3348
|
-
|
|
2684
|
+
const maybeVisual = hasBinaryContent || ext === "pdf" || hasPdfHeader;
|
|
2685
|
+
let earlyDigest;
|
|
2686
|
+
if (maybeVisual) {
|
|
3349
2687
|
let detection;
|
|
3350
2688
|
try {
|
|
3351
|
-
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
path: rawParams.path
|
|
2689
|
+
earlyDigest = await readSeekDigestContent(absolutePath, {
|
|
2690
|
+
...ext === "pdf" || hasPdfHeader ? {} : { visionMode: "none" },
|
|
2691
|
+
signal
|
|
3355
2692
|
});
|
|
2693
|
+
detection = earlyDigest.metadata;
|
|
2694
|
+
} catch {
|
|
2695
|
+
try {
|
|
2696
|
+
detection = await readSeekDetect(absolutePath, { signal });
|
|
2697
|
+
} catch {
|
|
2698
|
+
return buildToolErrorResult("read", "read-error", `Visual file detection failed for ${absolutePath}.`, {
|
|
2699
|
+
path: rawParams.path
|
|
2700
|
+
});
|
|
2701
|
+
}
|
|
3356
2702
|
}
|
|
3357
|
-
if (
|
|
3358
|
-
return buildToolErrorResult("read", "
|
|
3359
|
-
path: rawParams.path
|
|
3360
|
-
});
|
|
2703
|
+
if (detection.kind === "pdf") {
|
|
2704
|
+
return buildToolErrorResult("read", "document-use-view", documentRedirectMessage(rawParams.path, earlyDigest?.cache), { path: rawParams.path });
|
|
3361
2705
|
}
|
|
3362
|
-
if (detection.kind === "image"
|
|
2706
|
+
if (detection.kind === "image") {
|
|
3363
2707
|
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.";
|
|
2708
|
+
const message = "Cannot combine offset/limit with image reads.";
|
|
3369
2709
|
return buildToolErrorResult("read", "invalid-params-combo", message, { path: rawParams.path });
|
|
3370
2710
|
}
|
|
3371
2711
|
const imageMode = resolveReadSeekImageMode();
|
|
@@ -3375,38 +2715,14 @@ async function executeRead(opts) {
|
|
|
3375
2715
|
path: rawParams.path
|
|
3376
2716
|
});
|
|
3377
2717
|
}
|
|
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
2718
|
if (imageMode === "off" || p.image === undefined) {
|
|
3403
2719
|
return skippedImageFile(rawParams.path);
|
|
3404
2720
|
}
|
|
3405
2721
|
if (useNativeVision || p.image === "none") {
|
|
3406
2722
|
try {
|
|
3407
|
-
const
|
|
2723
|
+
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
2724
|
return succeed({
|
|
3409
|
-
content: [{ type: "image", data:
|
|
2725
|
+
content: [{ type: "image", data: prepared.data, mimeType: prepared.mime }],
|
|
3410
2726
|
details: {}
|
|
3411
2727
|
});
|
|
3412
2728
|
} catch (error) {
|
|
@@ -3436,10 +2752,6 @@ async function executeRead(opts) {
|
|
|
3436
2752
|
});
|
|
3437
2753
|
}
|
|
3438
2754
|
}
|
|
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
2755
|
throwIfAborted(signal);
|
|
3444
2756
|
const rawText = rawBuffer.toString("utf-8");
|
|
3445
2757
|
const normalized = normalizeToLF(stripBom(rawText).text);
|
|
@@ -3562,7 +2874,11 @@ async function executeRead(opts) {
|
|
|
3562
2874
|
throwIfAborted(signal);
|
|
3563
2875
|
let readSeekOutput;
|
|
3564
2876
|
try {
|
|
3565
|
-
|
|
2877
|
+
const digested = total === 0 ? await readSeekDigestContent(absolutePath, { signal }) : await readSeekDigestContent(absolutePath, { startLine, endLine: endIdx, signal });
|
|
2878
|
+
if (!digested.text) {
|
|
2879
|
+
throw new Error(`readseek returned no text content for ${absolutePath}`);
|
|
2880
|
+
}
|
|
2881
|
+
readSeekOutput = digested.text;
|
|
3566
2882
|
} catch (err) {
|
|
3567
2883
|
const detail = err?.message ? ` — ${err.message}` : "";
|
|
3568
2884
|
const message = `readseek failed while reading ${rawPath}${detail}`;
|
|
@@ -3584,7 +2900,7 @@ async function executeRead(opts) {
|
|
|
3584
2900
|
const selected = readSeekLines.map((line) => line.raw);
|
|
3585
2901
|
const formatted = renderReadSeekLines(readSeekLines);
|
|
3586
2902
|
const truncation = truncateHead2(formatted, { maxLines: DEFAULT_MAX_LINES3, maxBytes: DEFAULT_MAX_BYTES3 });
|
|
3587
|
-
const shouldAppendMap = !!
|
|
2903
|
+
const shouldAppendMap = !!truncation.truncated && !p.offset && !p.limit && !symbolMatch;
|
|
3588
2904
|
let appendedMap = false;
|
|
3589
2905
|
let mapText = null;
|
|
3590
2906
|
if (shouldAppendMap) {
|
|
@@ -3597,6 +2913,16 @@ async function executeRead(opts) {
|
|
|
3597
2913
|
}
|
|
3598
2914
|
} catch {}
|
|
3599
2915
|
}
|
|
2916
|
+
let currentBuffer;
|
|
2917
|
+
try {
|
|
2918
|
+
currentBuffer = await fsReadFile(absolutePath);
|
|
2919
|
+
} catch (err) {
|
|
2920
|
+
const { code, message } = formatFsError(err, "read-error");
|
|
2921
|
+
return buildToolErrorResult("read", code, message, { path: rawParams.path });
|
|
2922
|
+
}
|
|
2923
|
+
if (!currentBuffer.equals(rawBuffer)) {
|
|
2924
|
+
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 });
|
|
2925
|
+
}
|
|
3600
2926
|
if (symbolWarning) {
|
|
3601
2927
|
structuredWarnings.push(buildReadSeekWarning("symbol-warning", symbolWarning.trim()));
|
|
3602
2928
|
}
|
|
@@ -3632,7 +2958,7 @@ async function executeRead(opts) {
|
|
|
3632
2958
|
endLine: symbolMatch.endLine
|
|
3633
2959
|
} : null,
|
|
3634
2960
|
map: {
|
|
3635
|
-
requested:
|
|
2961
|
+
requested: false,
|
|
3636
2962
|
appended: appendedMap,
|
|
3637
2963
|
text: mapText
|
|
3638
2964
|
},
|
|
@@ -3660,13 +2986,13 @@ function registerReadTool(pi, options = {}) {
|
|
|
3660
2986
|
const imageModes = imageMode === "auto" ? ["none", "all", "ocr", "caption", "objects"] : ["all", "ocr", "caption", "objects"];
|
|
3661
2987
|
const promptMetadata = defineToolPromptMetadata({
|
|
3662
2988
|
promptUrl: new URL("../prompts/read.md", import.meta.url),
|
|
3663
|
-
promptSnippet: imageMode === "off" ? "Read anchored text
|
|
2989
|
+
promptSnippet: imageMode === "off" ? "Read anchored text or symbols" : "Read anchored text, symbols, or images",
|
|
3664
2990
|
registeredName: name
|
|
3665
2991
|
});
|
|
3666
2992
|
const tool = registerReadSeekTool(pi, {
|
|
3667
2993
|
name,
|
|
3668
2994
|
label: "Read",
|
|
3669
|
-
description: imageMode === "off" ? "Read anchored text by range
|
|
2995
|
+
description: imageMode === "off" ? "Read anchored text by range or symbol. Images and documents are disabled; use readSeek_view for documents." : promptMetadata.description,
|
|
3670
2996
|
promptSnippet: promptMetadata.promptSnippet,
|
|
3671
2997
|
promptGuidelines: [
|
|
3672
2998
|
...promptMetadata.promptGuidelines,
|
|
@@ -3676,15 +3002,13 @@ function registerReadTool(pi, options = {}) {
|
|
|
3676
3002
|
path: filePathParam(),
|
|
3677
3003
|
offset: optionalIntOrString("One-based starting line"),
|
|
3678
3004
|
limit: optionalIntOrString("Maximum lines to return"),
|
|
3679
|
-
page: optionalIntOrString("One-based PDF page; defaults to 1"),
|
|
3680
3005
|
symbol: Type2.Optional(Type2.String({ description: "Symbol name to read" })),
|
|
3681
|
-
map: mapParam(),
|
|
3682
3006
|
bundle: Type2.Optional(Type2.Literal("local", {
|
|
3683
3007
|
description: "Include related symbols from the same file; requires symbol"
|
|
3684
3008
|
})),
|
|
3685
3009
|
...imageMode === "off" ? {} : {
|
|
3686
3010
|
image: Type2.Optional(Type2.Union(imageModes.map((mode) => Type2.Literal(mode)), {
|
|
3687
|
-
description: `Standalone
|
|
3011
|
+
description: `Standalone image mode: ${imageModes.join(", ")}. Required for standalone images.${imageMode === "auto" ? " For standalone images, none skips local analysis." : ""}`
|
|
3688
3012
|
}))
|
|
3689
3013
|
}
|
|
3690
3014
|
}),
|
|
@@ -3773,7 +3097,7 @@ ${textContent}`;
|
|
|
3773
3097
|
}
|
|
3774
3098
|
|
|
3775
3099
|
// src/edit.ts
|
|
3776
|
-
import { readFile as fsReadFile2
|
|
3100
|
+
import { readFile as fsReadFile2 } from "node:fs/promises";
|
|
3777
3101
|
import { createPatch } from "diff";
|
|
3778
3102
|
import { withFileMutationQueue } from "@earendil-works/pi-coding-agent";
|
|
3779
3103
|
import { Type as Type3 } from "@sinclair/typebox";
|
|
@@ -4056,77 +3380,12 @@ async function validateSyntaxRegression(input, options = {}) {
|
|
|
4056
3380
|
};
|
|
4057
3381
|
}
|
|
4058
3382
|
|
|
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
|
-
|
|
3383
|
+
// src/syntax-validate-mode.ts
|
|
3384
|
+
var DEFAULT = "warn";
|
|
3385
|
+
function resolveSyntaxValidateMode(opts) {
|
|
3386
|
+
return opts.syntaxValidate ?? resolveReadSeekSyntaxValidation() ?? DEFAULT;
|
|
3387
|
+
}
|
|
3388
|
+
|
|
4130
3389
|
// src/pending-diff-preview.ts
|
|
4131
3390
|
import { existsSync as existsSync2, readFileSync as readFileSync4, realpathSync, statSync as statSync3 } from "node:fs";
|
|
4132
3391
|
import { dirname, isAbsolute as isAbsolute2, relative as relative2, resolve, sep } from "node:path";
|
|
@@ -4188,57 +3447,6 @@ function buildData(filePath, previousContent, nextContent, existed, headerLabel)
|
|
|
4188
3447
|
function getEdits(input) {
|
|
4189
3448
|
return Array.isArray(input.edits) ? input.edits : [];
|
|
4190
3449
|
}
|
|
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
3450
|
function buildPendingWritePreviewData(input, cwd) {
|
|
4243
3451
|
if (typeof input.content !== "string")
|
|
4244
3452
|
return skip("missing content");
|
|
@@ -4262,50 +3470,16 @@ async function buildPendingEditPreviewData(input, cwd) {
|
|
|
4262
3470
|
const previous = readUtf8File(resolved.path);
|
|
4263
3471
|
if (previous.type === "skip")
|
|
4264
3472
|
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;
|
|
3473
|
+
try {
|
|
3474
|
+
const plan = await readSeekEdit(resolved.path, edits, {
|
|
3475
|
+
language: typeof input.language === "string" ? input.language : undefined
|
|
3476
|
+
});
|
|
3477
|
+
if (typeof plan.content !== "string")
|
|
3478
|
+
return skip("edit plan did not include content");
|
|
3479
|
+
return buildData(resolved.path, previous.content, plan.content, true, "pending edit");
|
|
3480
|
+
} catch (err) {
|
|
3481
|
+
return skip(`edit projection failed: ${err?.message ?? String(err)}`);
|
|
4307
3482
|
}
|
|
4308
|
-
return buildData(resolved.path, previous.content, next, true, "pending edit");
|
|
4309
3483
|
}
|
|
4310
3484
|
function buildEditPreviewKey(input) {
|
|
4311
3485
|
if (typeof input.path !== "string")
|
|
@@ -4313,7 +3487,7 @@ function buildEditPreviewKey(input) {
|
|
|
4313
3487
|
const edits = getEdits(input);
|
|
4314
3488
|
if (edits.length === 0)
|
|
4315
3489
|
return;
|
|
4316
|
-
return JSON.stringify({ path: input.path, edits });
|
|
3490
|
+
return JSON.stringify({ path: input.path, edits, language: input.language });
|
|
4317
3491
|
}
|
|
4318
3492
|
function buildWritePreviewKey(input) {
|
|
4319
3493
|
if (typeof input.path !== "string" || typeof input.content !== "string")
|
|
@@ -4796,26 +3970,25 @@ var hashlineEditItemSchema = Type3.Object({
|
|
|
4796
3970
|
set_line: Type3.Optional(Type3.Object({
|
|
4797
3971
|
anchor: Type3.String({ description: "Fresh LINE:HASH anchor for the line to replace" }),
|
|
4798
3972
|
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":"..."}}' })),
|
|
3973
|
+
}, { additionalProperties: false, description: 'Replace one line: {"set_line":{"anchor":"LINE:HASH","new_text":"..."}}' })),
|
|
4800
3974
|
replace_lines: Type3.Optional(Type3.Object({
|
|
4801
3975
|
start_anchor: Type3.String({ description: "Fresh LINE:HASH anchor for the first line in the range" }),
|
|
4802
3976
|
end_anchor: Type3.String({ description: "Fresh LINE:HASH anchor for the last line in the range" }),
|
|
4803
3977
|
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":"..."}}' })),
|
|
3978
|
+
}, { additionalProperties: false, description: 'Replace a range: {"replace_lines":{"start_anchor":"LINE:HASH","end_anchor":"LINE:HASH","new_text":"..."}}' })),
|
|
4805
3979
|
insert_after: Type3.Optional(Type3.Object({
|
|
4806
3980
|
anchor: Type3.String({ description: "Fresh LINE:HASH anchor for the line after which to insert" }),
|
|
4807
3981
|
new_text: Type3.String({ description: "Text to insert after the anchored line" })
|
|
4808
|
-
}, { description: 'Insert text: {"insert_after":{"anchor":"LINE:HASH","new_text":"..."}}' })),
|
|
3982
|
+
}, { additionalProperties: false, description: 'Insert text: {"insert_after":{"anchor":"LINE:HASH","new_text":"..."}}' })),
|
|
4809
3983
|
replace: Type3.Optional(Type3.Object({
|
|
4810
3984
|
old_text: Type3.String({ description: "Exact text to find" }),
|
|
4811
3985
|
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":"..."}}' })),
|
|
3986
|
+
all: Type3.Optional(Type3.Boolean({ description: "Replace every exact match" }))
|
|
3987
|
+
}, { additionalProperties: false, description: 'Replace text: {"replace":{"old_text":"...","new_text":"..."}}' })),
|
|
4815
3988
|
replace_symbol: Type3.Optional(Type3.Object({
|
|
4816
3989
|
symbol: Type3.String({ description: "Mapped symbol name" }),
|
|
4817
3990
|
new_body: Type3.String({ description: "Complete replacement body for the symbol" })
|
|
4818
|
-
}, { description: 'Replace a symbol: {"replace_symbol":{"symbol":"name","new_body":"..."}}' }))
|
|
3991
|
+
}, { additionalProperties: false, description: 'Replace a symbol: {"replace_symbol":{"symbol":"name","new_body":"..."}}' }))
|
|
4819
3992
|
}, {
|
|
4820
3993
|
additionalProperties: false,
|
|
4821
3994
|
minProperties: 1,
|
|
@@ -4824,13 +3997,15 @@ var hashlineEditItemSchema = Type3.Object({
|
|
|
4824
3997
|
});
|
|
4825
3998
|
var hashlineEditSchema = Type3.Object({
|
|
4826
3999
|
path: filePathParam(),
|
|
4827
|
-
|
|
4000
|
+
language: Type3.Optional(Type3.String({ description: "Language override for symbol replacement" })),
|
|
4001
|
+
edits: Type3.Array(hashlineEditItemSchema, {
|
|
4002
|
+
minItems: 1,
|
|
4828
4003
|
description: "Use set_line, replace_lines, insert_after, replace_symbol, or replace"
|
|
4829
|
-
})
|
|
4004
|
+
}),
|
|
4830
4005
|
postEditVerify: Type3.Optional(Type3.Boolean({
|
|
4831
4006
|
description: "Read back and verify persisted content"
|
|
4832
4007
|
}))
|
|
4833
|
-
}, { additionalProperties:
|
|
4008
|
+
}, { additionalProperties: false });
|
|
4834
4009
|
function buildEditError(path2, code, message, hint, errorDetails) {
|
|
4835
4010
|
return {
|
|
4836
4011
|
content: [{ type: "text", text: message }],
|
|
@@ -4854,9 +4029,7 @@ function mapEditFileError(err, filePath, _displayPath, _phase) {
|
|
|
4854
4029
|
}
|
|
4855
4030
|
async function executeEdit(opts) {
|
|
4856
4031
|
const { params, signal, cwd, wasReadInSession, onFileMutated, syntaxValidate } = opts;
|
|
4857
|
-
await ensureHashInit();
|
|
4858
4032
|
const parsed = params;
|
|
4859
|
-
const input = params;
|
|
4860
4033
|
const rawPath = parsed.path;
|
|
4861
4034
|
const path2 = rawPath.replace(/^@/, "");
|
|
4862
4035
|
const absolutePath = resolveToCwd(path2, cwd);
|
|
@@ -4865,345 +4038,105 @@ async function executeEdit(opts) {
|
|
|
4865
4038
|
return await withFileMutationQueue(absolutePath, async () => {
|
|
4866
4039
|
throwIfAborted(signal);
|
|
4867
4040
|
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 } }].");
|
|
4041
|
+
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.`;
|
|
4042
|
+
return buildEditError(absolutePath, "file-not-read", `You must get fresh anchors for ${absolutePath} before editing it. ${hint}`, hint);
|
|
4878
4043
|
}
|
|
4879
4044
|
const edits = parsed.edits ?? [];
|
|
4880
4045
|
if (!edits.length) {
|
|
4881
4046
|
return buildEditError(absolutePath, "invalid-edit-variant", "No edits provided.");
|
|
4882
4047
|
}
|
|
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 {}
|
|
4048
|
+
const variants = new Set(["set_line", "replace_lines", "insert_after", "replace", "replace_symbol"]);
|
|
4049
|
+
for (let index = 0;index < edits.length; index++) {
|
|
4050
|
+
const edit = edits[index];
|
|
4051
|
+
const keys = edit && typeof edit === "object" && !Array.isArray(edit) ? Object.keys(edit) : [];
|
|
4052
|
+
if (keys.length !== 1 || !variants.has(keys[0])) {
|
|
4053
|
+
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
4054
|
}
|
|
4949
4055
|
}
|
|
4950
|
-
let
|
|
4951
|
-
const replaceSymbolRanges = [];
|
|
4952
|
-
const rsProbeResults = [];
|
|
4056
|
+
let plan;
|
|
4953
4057
|
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
|
-
}
|
|
4058
|
+
plan = await readSeekEdit(absolutePath, edits, { signal, language: parsed.language });
|
|
4968
4059
|
} catch (err) {
|
|
4969
4060
|
throwIfAborted(signal);
|
|
4970
4061
|
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
|
-
}
|
|
4062
|
+
const code = failure.message.includes("stale anchor") ? "hash-mismatch" : failure.code;
|
|
4063
|
+
return buildEditError(absolutePath, code, failure.message, failure.hint);
|
|
4981
4064
|
}
|
|
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;
|
|
4065
|
+
if (typeof plan.before_content !== "string" || typeof plan.content !== "string") {
|
|
4066
|
+
return buildEditError(absolutePath, "invalid-output", "readseek edit plan did not include before_content and content");
|
|
5081
4067
|
}
|
|
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);
|
|
4068
|
+
const original = plan.before_content;
|
|
4069
|
+
const result = plan.content;
|
|
4070
|
+
const originalForSyntax = normalizeToLF(stripBom(original).text);
|
|
4071
|
+
const resultForSyntax = normalizeToLF(stripBom(result).text);
|
|
4072
|
+
if (!plan.changed) {
|
|
4073
|
+
return buildEditError(absolutePath, "no-op", `No changes made to ${path2}. The edits produced identical content.`);
|
|
5125
4074
|
}
|
|
5126
|
-
throwIfAborted(signal);
|
|
5127
4075
|
const syntaxMode = resolveSyntaxValidateMode({ syntaxValidate });
|
|
5128
4076
|
let syntaxWarning;
|
|
5129
4077
|
if (syntaxMode !== "off") {
|
|
5130
4078
|
const regression = await validateSyntaxRegression({
|
|
5131
4079
|
filePath: absolutePath,
|
|
5132
|
-
before:
|
|
5133
|
-
after:
|
|
4080
|
+
before: originalForSyntax,
|
|
4081
|
+
after: resultForSyntax
|
|
5134
4082
|
}, { signal });
|
|
5135
4083
|
if (regression) {
|
|
5136
|
-
const
|
|
5137
|
-
const message = `syntax-regression: lines ${lines}`;
|
|
4084
|
+
const message = `syntax-regression: lines ${regression.errorLines.join(", ")}`;
|
|
5138
4085
|
if (syntaxMode === "block") {
|
|
5139
4086
|
return buildEditError(absolutePath, "syntax-regression", message);
|
|
5140
4087
|
}
|
|
5141
4088
|
syntaxWarning = message;
|
|
5142
4089
|
}
|
|
5143
4090
|
}
|
|
5144
|
-
|
|
4091
|
+
throwIfAborted(signal);
|
|
4092
|
+
let applied;
|
|
5145
4093
|
try {
|
|
5146
|
-
await
|
|
4094
|
+
applied = await readSeekEdit(absolutePath, edits, {
|
|
4095
|
+
apply: true,
|
|
4096
|
+
planHash: plan.plan_hash,
|
|
4097
|
+
language: parsed.language,
|
|
4098
|
+
cancelable: false
|
|
4099
|
+
});
|
|
5147
4100
|
} catch (err) {
|
|
5148
|
-
|
|
4101
|
+
throwIfAborted(signal);
|
|
4102
|
+
const failure = classifyReadSeekFailure(err);
|
|
4103
|
+
return buildEditError(absolutePath, failure.code, failure.message, failure.hint);
|
|
4104
|
+
}
|
|
4105
|
+
if (!applied.applied) {
|
|
4106
|
+
return buildEditError(absolutePath, "invalid-output", "readseek did not confirm that the edit was applied");
|
|
5149
4107
|
}
|
|
5150
4108
|
onFileMutated?.(absolutePath);
|
|
5151
|
-
if (
|
|
4109
|
+
if (parsed.postEditVerify === true) {
|
|
5152
4110
|
let verifiedContent;
|
|
5153
4111
|
try {
|
|
5154
|
-
|
|
5155
|
-
verifiedContent = verified;
|
|
4112
|
+
verifiedContent = await fsReadFile2(absolutePath, "utf-8");
|
|
5156
4113
|
} catch (err) {
|
|
5157
|
-
|
|
5158
|
-
return buildEditError(absolutePath, "post-edit-verification-read-failed", message, undefined, {
|
|
5159
|
-
fsCode: err?.code,
|
|
5160
|
-
fsMessage: err?.message
|
|
5161
|
-
});
|
|
4114
|
+
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
4115
|
}
|
|
5163
|
-
if (verifiedContent !==
|
|
5164
|
-
|
|
5165
|
-
return buildEditError(absolutePath, "post-edit-verification-mismatch", message, undefined, {
|
|
5166
|
-
expectedLength: writeContent.length,
|
|
5167
|
-
actualLength: verifiedContent.length
|
|
5168
|
-
});
|
|
4116
|
+
if (verifiedContent !== plan.content) {
|
|
4117
|
+
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
4118
|
}
|
|
5170
4119
|
}
|
|
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
|
-
}));
|
|
4120
|
+
const diffResult = generateCompactOrFullDiff(original, result);
|
|
4121
|
+
const patch = createPatch(path2, original, result);
|
|
5178
4122
|
const diffData = buildDiffData({
|
|
5179
4123
|
path: absolutePath,
|
|
5180
|
-
oldContent:
|
|
4124
|
+
oldContent: original,
|
|
5181
4125
|
newContent: result,
|
|
5182
|
-
diff: diffResult.diff
|
|
5183
|
-
...blockRanges.length ? { blockRanges } : {}
|
|
4126
|
+
diff: diffResult.diff
|
|
5184
4127
|
});
|
|
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
|
-
};
|
|
4128
|
+
const warnings = syntaxWarning ? [syntaxWarning] : [];
|
|
4129
|
+
const classification = classifyEdit(original, result);
|
|
4130
|
+
const semanticSummary = { classification: classification.classification };
|
|
5198
4131
|
const builtOutput = buildEditOutput({
|
|
5199
4132
|
path: absolutePath,
|
|
5200
4133
|
displayPath: path2,
|
|
5201
4134
|
diff: diffResult.diff,
|
|
5202
4135
|
patch,
|
|
5203
4136
|
diffData,
|
|
5204
|
-
firstChangedLine:
|
|
4137
|
+
firstChangedLine: diffResult.firstChangedLine,
|
|
5205
4138
|
warnings,
|
|
5206
|
-
noopEdits:
|
|
4139
|
+
noopEdits: [],
|
|
5207
4140
|
edits,
|
|
5208
4141
|
semanticSummary
|
|
5209
4142
|
});
|
|
@@ -5213,7 +4146,7 @@ Your edits were normalized back to the original content. Ensure your replacement
|
|
|
5213
4146
|
diff: diffResult.diff,
|
|
5214
4147
|
patch: builtOutput.patch,
|
|
5215
4148
|
diffData,
|
|
5216
|
-
firstChangedLine:
|
|
4149
|
+
firstChangedLine: diffResult.firstChangedLine,
|
|
5217
4150
|
readSeekValue: builtOutput.readSeekValue
|
|
5218
4151
|
}
|
|
5219
4152
|
};
|
|
@@ -6627,120 +5560,12 @@ ${linkedPathLine(theme, file.file, display, cwd, ` (${file.edits.length} edits)`
|
|
|
6627
5560
|
});
|
|
6628
5561
|
}
|
|
6629
5562
|
|
|
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
5563
|
// src/write.ts
|
|
6739
|
-
import { mkdir, readFile
|
|
5564
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
6740
5565
|
import { dirname as dirname2, relative as relative3 } from "node:path";
|
|
6741
5566
|
import { withFileMutationQueue as withFileMutationQueue2, truncateHead as truncateHead4 } from "@earendil-works/pi-coding-agent";
|
|
6742
|
-
import { Text as
|
|
6743
|
-
import { Type as
|
|
5567
|
+
import { Text as Text7 } from "@earendil-works/pi-tui";
|
|
5568
|
+
import { Type as Type9 } from "@sinclair/typebox";
|
|
6744
5569
|
var WRITE_PENDING_PREVIEW_STATE_KEY = "hashline-write-pending-preview";
|
|
6745
5570
|
var CONTENT_PREVIEW_MAX_LINES = 200;
|
|
6746
5571
|
function formatContentPreviewLines(content, theme) {
|
|
@@ -6780,7 +5605,7 @@ function isToolErrorResult(result) {
|
|
|
6780
5605
|
}
|
|
6781
5606
|
async function readPreviousTextForDiff(filePath) {
|
|
6782
5607
|
try {
|
|
6783
|
-
const previous = await
|
|
5608
|
+
const previous = await readFile(filePath);
|
|
6784
5609
|
return {
|
|
6785
5610
|
content: looksLikeBinary(previous) ? "" : previous.toString("utf-8"),
|
|
6786
5611
|
existed: true
|
|
@@ -6818,7 +5643,7 @@ function buildWriteFsErrorResult(err, absolutePath) {
|
|
|
6818
5643
|
}
|
|
6819
5644
|
async function executeWrite(opts) {
|
|
6820
5645
|
await ensureHashInit();
|
|
6821
|
-
const { path: filePath, content,
|
|
5646
|
+
const { path: filePath, content, cwd } = opts;
|
|
6822
5647
|
const warnings = [];
|
|
6823
5648
|
const readSeekWarnings = [];
|
|
6824
5649
|
if (hasBareCarriageReturn(content)) {
|
|
@@ -6844,8 +5669,15 @@ async function executeWrite(opts) {
|
|
|
6844
5669
|
const { content: previousContent, existed: existedBeforeWrite } = await readPreviousTextForDiff(filePath);
|
|
6845
5670
|
await mkdir(dirname2(filePath), { recursive: true });
|
|
6846
5671
|
await writeFile(filePath, content, "utf-8");
|
|
6847
|
-
const
|
|
5672
|
+
const normalizedContent = stripBom(content).text;
|
|
5673
|
+
const rawLines = normalizedContent === "" ? [] : normalizedContent.split(`
|
|
6848
5674
|
`);
|
|
5675
|
+
if (rawLines[rawLines.length - 1] === "")
|
|
5676
|
+
rawLines.pop();
|
|
5677
|
+
for (let i = 0;i < rawLines.length; i++) {
|
|
5678
|
+
if (rawLines[i].endsWith("\r"))
|
|
5679
|
+
rawLines[i] = rawLines[i].slice(0, -1);
|
|
5680
|
+
}
|
|
6849
5681
|
const readSeekLines = [];
|
|
6850
5682
|
const displayLines = [];
|
|
6851
5683
|
for (let i = 0;i < rawLines.length; i++) {
|
|
@@ -6867,21 +5699,6 @@ async function executeWrite(opts) {
|
|
|
6867
5699
|
[… output truncated at 50 KB — full anchors in readSeekValue]`;
|
|
6868
5700
|
}
|
|
6869
5701
|
}
|
|
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
5702
|
const displayPath = cwd ? relative3(cwd, filePath) || filePath : filePath;
|
|
6886
5703
|
const normalizedPrevious = normalizeToLF(previousContent);
|
|
6887
5704
|
const normalizedNext = normalizeToLF(content);
|
|
@@ -6904,8 +5721,7 @@ async function executeWrite(opts) {
|
|
|
6904
5721
|
lines: readSeekLines,
|
|
6905
5722
|
warnings: readSeekWarnings,
|
|
6906
5723
|
diff: diffResult.diff,
|
|
6907
|
-
diffData
|
|
6908
|
-
...requestMap ? { map: { appended: mapAppended } } : {}
|
|
5724
|
+
diffData
|
|
6909
5725
|
}
|
|
6910
5726
|
};
|
|
6911
5727
|
}
|
|
@@ -6922,10 +5738,9 @@ function registerWriteTool(pi, options = {}) {
|
|
|
6922
5738
|
description: promptMetadata.description,
|
|
6923
5739
|
promptSnippet: promptMetadata.promptSnippet,
|
|
6924
5740
|
promptGuidelines: promptMetadata.promptGuidelines,
|
|
6925
|
-
parameters:
|
|
5741
|
+
parameters: Type9.Object({
|
|
6926
5742
|
path: filePathParam(),
|
|
6927
|
-
content:
|
|
6928
|
-
map: mapParam()
|
|
5743
|
+
content: Type9.String({ description: "Complete text file content" })
|
|
6929
5744
|
}),
|
|
6930
5745
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
6931
5746
|
const cwd = ctx?.cwd ?? process.cwd();
|
|
@@ -6937,7 +5752,6 @@ function registerWriteTool(pi, options = {}) {
|
|
|
6937
5752
|
result = await executeWrite({
|
|
6938
5753
|
path: absolutePath,
|
|
6939
5754
|
content: params.content,
|
|
6940
|
-
map: params.map,
|
|
6941
5755
|
cwd
|
|
6942
5756
|
});
|
|
6943
5757
|
} catch (err) {
|
|
@@ -7007,7 +5821,7 @@ function registerWriteTool(pi, options = {}) {
|
|
|
7007
5821
|
`);
|
|
7008
5822
|
lines.push(...formatContentPreviewLines(content, theme));
|
|
7009
5823
|
}
|
|
7010
|
-
return new
|
|
5824
|
+
return new Text7(clampLinesToWidth(lines, width).join(`
|
|
7011
5825
|
`), 0, 0);
|
|
7012
5826
|
}
|
|
7013
5827
|
const hasExpandableDiff = !!diffData;
|
|
@@ -7016,7 +5830,7 @@ function registerWriteTool(pi, options = {}) {
|
|
|
7016
5830
|
return upsertDiffComponent(context.lastComponent, { prefixLines: text.split(`
|
|
7017
5831
|
`), diffData, theme, expanded: true });
|
|
7018
5832
|
}
|
|
7019
|
-
return new
|
|
5833
|
+
return new Text7(clampLinesToWidth(text.split(`
|
|
7020
5834
|
`), width).join(`
|
|
7021
5835
|
`), 0, 0);
|
|
7022
5836
|
}
|
|
@@ -7025,7 +5839,7 @@ function registerWriteTool(pi, options = {}) {
|
|
|
7025
5839
|
}
|
|
7026
5840
|
|
|
7027
5841
|
// src/def.ts
|
|
7028
|
-
import { Type as
|
|
5842
|
+
import { Type as Type10 } from "@sinclair/typebox";
|
|
7029
5843
|
import path6 from "node:path";
|
|
7030
5844
|
var DEF_PROMPT_METADATA = defineToolPromptMetadata({
|
|
7031
5845
|
promptUrl: new URL("../prompts/def.md", import.meta.url),
|
|
@@ -7101,8 +5915,8 @@ function registerDefTool(pi, options = {}) {
|
|
|
7101
5915
|
description: DEF_PROMPT_METADATA.description,
|
|
7102
5916
|
promptSnippet: DEF_PROMPT_METADATA.promptSnippet,
|
|
7103
5917
|
promptGuidelines: DEF_PROMPT_METADATA.promptGuidelines,
|
|
7104
|
-
parameters:
|
|
7105
|
-
name:
|
|
5918
|
+
parameters: Type10.Object({
|
|
5919
|
+
name: Type10.String({ description: "Qualified or unqualified symbol name" }),
|
|
7106
5920
|
path: searchPathParam(),
|
|
7107
5921
|
lang: langParam(),
|
|
7108
5922
|
...readSeekGitSearchParams()
|
|
@@ -7128,77 +5942,205 @@ function registerDefTool(pi, options = {}) {
|
|
|
7128
5942
|
});
|
|
7129
5943
|
}
|
|
7130
5944
|
|
|
7131
|
-
// src/
|
|
7132
|
-
import {
|
|
7133
|
-
import {
|
|
7134
|
-
|
|
7135
|
-
var
|
|
7136
|
-
promptUrl: new URL("../prompts/
|
|
7137
|
-
promptSnippet: "
|
|
5945
|
+
// src/digest.ts
|
|
5946
|
+
import { Text as Text8 } from "@earendil-works/pi-tui";
|
|
5947
|
+
import { Type as Type11 } from "@sinclair/typebox";
|
|
5948
|
+
var DIGEST_FACETS = ["metadata", "content", "map", "diagnostics", "identity"];
|
|
5949
|
+
var DIGEST_PROMPT_METADATA = defineToolPromptMetadata({
|
|
5950
|
+
promptUrl: new URL("../prompts/digest.md", import.meta.url),
|
|
5951
|
+
promptSnippet: "Digest source facets as a native CLI envelope"
|
|
7138
5952
|
});
|
|
7139
|
-
|
|
7140
|
-
|
|
5953
|
+
var facetSchema = Type11.Union(DIGEST_FACETS.map((facet) => Type11.Literal(facet)));
|
|
5954
|
+
var digestSchema = Type11.Object({
|
|
5955
|
+
path: filePathParam(),
|
|
5956
|
+
select: Type11.Optional(Type11.Union([
|
|
5957
|
+
facetSchema,
|
|
5958
|
+
Type11.Array(facetSchema, { minItems: 1 }),
|
|
5959
|
+
Type11.String({
|
|
5960
|
+
description: "Comma-separated facets: metadata, content, map, diagnostics, identity"
|
|
5961
|
+
})
|
|
5962
|
+
], {
|
|
5963
|
+
description: "Facets: metadata, content, map, diagnostics, identity (default content)"
|
|
5964
|
+
})),
|
|
5965
|
+
at: Type11.Optional(Type11.String({
|
|
5966
|
+
description: "Location: line:N[:COLUMN], hash:HASH[:COLUMN], or symbol:NAME"
|
|
5967
|
+
})),
|
|
5968
|
+
end: optionalIntOrString("Last line to include"),
|
|
5969
|
+
limit: optionalIntOrString("Maximum number of lines to include"),
|
|
5970
|
+
language: Type11.Optional(Type11.String({ description: "Language override" })),
|
|
5971
|
+
visionMode: Type11.Optional(Type11.Union([
|
|
5972
|
+
Type11.Literal("none"),
|
|
5973
|
+
Type11.Literal("all"),
|
|
5974
|
+
Type11.Literal("ocr"),
|
|
5975
|
+
Type11.Literal("caption"),
|
|
5976
|
+
Type11.Literal("objects")
|
|
5977
|
+
], { description: "Standalone image mode: none, all, caption, objects, or ocr" })),
|
|
5978
|
+
visionLevel: Type11.Optional(Type11.Union([Type11.Literal("low"), Type11.Literal("medium"), Type11.Literal("high")], { description: "Vision inference level: low (default), medium, or high" }))
|
|
5979
|
+
});
|
|
5980
|
+
function hasDigestAnchors(value) {
|
|
5981
|
+
if (!value || typeof value !== "object")
|
|
5982
|
+
return false;
|
|
5983
|
+
const record = value;
|
|
5984
|
+
if (Array.isArray(record.hashlines) && record.hashlines.length > 0)
|
|
5985
|
+
return true;
|
|
5986
|
+
return Object.values(record).some(hasDigestAnchors);
|
|
5987
|
+
}
|
|
5988
|
+
function parseSelect(select) {
|
|
5989
|
+
if (select === undefined)
|
|
5990
|
+
return;
|
|
5991
|
+
if (Array.isArray(select)) {
|
|
5992
|
+
const facets = select;
|
|
5993
|
+
if (facets.length === 0)
|
|
5994
|
+
throw new Error("select must include at least one facet");
|
|
5995
|
+
for (const facet of facets) {
|
|
5996
|
+
if (!DIGEST_FACETS.includes(facet)) {
|
|
5997
|
+
throw new Error(`unknown digest facet \`${facet}\`; expected ${DIGEST_FACETS.join(", ")}`);
|
|
5998
|
+
}
|
|
5999
|
+
}
|
|
6000
|
+
return facets;
|
|
6001
|
+
}
|
|
6002
|
+
if (typeof select !== "string") {
|
|
6003
|
+
throw new Error("select must be a facet, facet list, or comma-separated facets");
|
|
6004
|
+
}
|
|
6005
|
+
if (DIGEST_FACETS.includes(select)) {
|
|
6006
|
+
return select;
|
|
6007
|
+
}
|
|
6008
|
+
const parts = select.split(",").map((part) => part.trim()).filter((part) => part.length > 0);
|
|
6009
|
+
if (parts.length === 0)
|
|
6010
|
+
throw new Error("select must include at least one facet");
|
|
6011
|
+
for (const part of parts) {
|
|
6012
|
+
if (!DIGEST_FACETS.includes(part)) {
|
|
6013
|
+
throw new Error(`unknown digest facet \`${part}\`; expected ${DIGEST_FACETS.join(", ")}`);
|
|
6014
|
+
}
|
|
6015
|
+
}
|
|
6016
|
+
return parts;
|
|
6017
|
+
}
|
|
6018
|
+
function selectedFacets(select) {
|
|
6019
|
+
if (select === undefined)
|
|
6020
|
+
return ["content"];
|
|
6021
|
+
return Array.isArray(select) ? select : [select];
|
|
6022
|
+
}
|
|
6023
|
+
function summarizeEnvelope(envelope, facets) {
|
|
6024
|
+
if (!envelope || typeof envelope !== "object")
|
|
6025
|
+
return `digested ${facets.join(",")}`;
|
|
6026
|
+
const record = envelope;
|
|
6027
|
+
const parts = [];
|
|
6028
|
+
for (const facet of facets) {
|
|
6029
|
+
if (facet === "metadata" || record[facet] !== undefined)
|
|
6030
|
+
parts.push(facet);
|
|
6031
|
+
}
|
|
6032
|
+
if (record.metadata !== undefined && !parts.includes("metadata"))
|
|
6033
|
+
parts.unshift("metadata");
|
|
6034
|
+
return `digested ${parts.join(",") || "metadata"}`;
|
|
6035
|
+
}
|
|
6036
|
+
async function executeDigest(opts) {
|
|
6037
|
+
const { params, signal, cwd, onFileAnchored } = opts;
|
|
7141
6038
|
const p = params;
|
|
7142
|
-
|
|
7143
|
-
let content;
|
|
6039
|
+
let select;
|
|
7144
6040
|
try {
|
|
7145
|
-
|
|
6041
|
+
select = parseSelect(p.select);
|
|
7146
6042
|
} catch (err) {
|
|
7147
|
-
|
|
7148
|
-
|
|
6043
|
+
return buildToolErrorResult("digest", "invalid-parameter", err?.message ?? String(err), {
|
|
6044
|
+
path: p.path
|
|
6045
|
+
});
|
|
6046
|
+
}
|
|
6047
|
+
const end = coerceObviousBase10Int(p.end, "end");
|
|
6048
|
+
if (!end.ok || end.value !== undefined && end.value < 1) {
|
|
6049
|
+
const message = end.ok ? `Invalid end: expected a positive integer, received ${end.value}.` : end.message;
|
|
6050
|
+
return buildToolErrorResult("digest", "invalid-parameter", message, { path: p.path });
|
|
6051
|
+
}
|
|
6052
|
+
const limit = coerceObviousBase10Int(p.limit, "limit");
|
|
6053
|
+
if (!limit.ok || limit.value !== undefined && limit.value < 1) {
|
|
6054
|
+
const message = limit.ok ? `Invalid limit: expected a positive integer, received ${limit.value}.` : limit.message;
|
|
6055
|
+
return buildToolErrorResult("digest", "invalid-parameter", message, { path: p.path });
|
|
6056
|
+
}
|
|
6057
|
+
const at = typeof p.at === "string" ? p.at.trim() : undefined;
|
|
6058
|
+
if (p.at !== undefined && !at) {
|
|
6059
|
+
return buildToolErrorResult("digest", "invalid-parameter", "Invalid at: expected a non-empty location.", {
|
|
6060
|
+
path: p.path
|
|
6061
|
+
});
|
|
6062
|
+
}
|
|
6063
|
+
if (p.visionLevel !== undefined && p.visionMode === undefined) {
|
|
6064
|
+
return buildToolErrorResult("digest", "invalid-params-combo", "visionLevel requires visionMode.", {
|
|
6065
|
+
path: p.path
|
|
6066
|
+
});
|
|
7149
6067
|
}
|
|
6068
|
+
const filePath = resolveToCwd(p.path, cwd);
|
|
6069
|
+
const facets = selectedFacets(select);
|
|
6070
|
+
const vision = p.visionMode !== undefined && p.visionMode !== "none";
|
|
7150
6071
|
try {
|
|
7151
|
-
const
|
|
7152
|
-
|
|
7153
|
-
|
|
6072
|
+
const envelope = await readSeekDigestNative(filePath, {
|
|
6073
|
+
select,
|
|
6074
|
+
at,
|
|
6075
|
+
end: end.value,
|
|
6076
|
+
limit: limit.value,
|
|
6077
|
+
language: p.language,
|
|
6078
|
+
visionMode: p.visionMode,
|
|
6079
|
+
visionLevel: p.visionLevel,
|
|
6080
|
+
signal,
|
|
6081
|
+
vision
|
|
6082
|
+
});
|
|
6083
|
+
if (hasDigestAnchors(envelope))
|
|
6084
|
+
onFileAnchored?.(filePath);
|
|
6085
|
+
const text = JSON.stringify(envelope);
|
|
7154
6086
|
return {
|
|
7155
|
-
content: [{ type: "text", text
|
|
7156
|
-
`) }],
|
|
6087
|
+
content: [{ type: "text", text }],
|
|
7157
6088
|
details: {
|
|
7158
|
-
readSeekValue: {
|
|
6089
|
+
readSeekValue: {
|
|
6090
|
+
tool: "digest",
|
|
6091
|
+
ok: true,
|
|
6092
|
+
path: filePath,
|
|
6093
|
+
select: facets,
|
|
6094
|
+
envelope
|
|
6095
|
+
}
|
|
7159
6096
|
}
|
|
7160
6097
|
};
|
|
7161
6098
|
} catch (err) {
|
|
7162
6099
|
const failure = classifyReadSeekFailure(err);
|
|
7163
|
-
return buildToolErrorResult("
|
|
6100
|
+
return buildToolErrorResult("digest", failure.code, failure.message, failure.hint ? { hint: failure.hint } : {});
|
|
7164
6101
|
}
|
|
7165
6102
|
}
|
|
7166
|
-
function
|
|
6103
|
+
function registerDigestTool(pi, options = {}) {
|
|
7167
6104
|
return registerReadSeekTool(pi, {
|
|
7168
|
-
name: "
|
|
7169
|
-
label: "
|
|
7170
|
-
description:
|
|
7171
|
-
promptSnippet:
|
|
7172
|
-
promptGuidelines:
|
|
7173
|
-
parameters:
|
|
6105
|
+
name: "readSeek_digest",
|
|
6106
|
+
label: "Digest",
|
|
6107
|
+
description: DIGEST_PROMPT_METADATA.description,
|
|
6108
|
+
promptSnippet: DIGEST_PROMPT_METADATA.promptSnippet,
|
|
6109
|
+
promptGuidelines: DIGEST_PROMPT_METADATA.promptGuidelines,
|
|
6110
|
+
parameters: digestSchema,
|
|
7174
6111
|
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
7175
|
-
return
|
|
6112
|
+
return executeDigest({ params, signal, cwd: ctx.cwd, onFileAnchored: options.onFileAnchored });
|
|
7176
6113
|
},
|
|
7177
6114
|
renderCall(args, theme, ...rest) {
|
|
7178
6115
|
const context = rest[0] ?? {};
|
|
7179
6116
|
const cwd = context.cwd ?? process.cwd();
|
|
7180
6117
|
const displayPath = typeof args?.path === "string" ? args.path : "?";
|
|
7181
|
-
|
|
7182
|
-
|
|
6118
|
+
let text = theme.fg("toolTitle", theme.bold("digest"));
|
|
6119
|
+
text += ` ${linkToolPath(theme.fg("accent", displayPath), displayPath, cwd)}`;
|
|
6120
|
+
if (args?.select !== undefined) {
|
|
6121
|
+
const select = Array.isArray(args.select) ? args.select.join(",") : String(args.select);
|
|
6122
|
+
text += theme.fg("dim", ` [${select}]`);
|
|
6123
|
+
}
|
|
6124
|
+
if (typeof args?.at === "string" && args.at)
|
|
6125
|
+
text += theme.fg("dim", ` @ ${args.at}`);
|
|
6126
|
+
return new Text8(clampLineToWidth(text, context.width), 0, 0);
|
|
7183
6127
|
},
|
|
7184
|
-
renderResult(result,
|
|
7185
|
-
const { isPartial, isError, expanded, width } = resolveRenderResultContext(
|
|
6128
|
+
renderResult(result, options2, theme, ...rest) {
|
|
6129
|
+
const { isPartial, isError, expanded, width } = resolveRenderResultContext(options2, rest);
|
|
7186
6130
|
if (isPartial)
|
|
7187
|
-
return renderPendingResult("pending
|
|
6131
|
+
return renderPendingResult("pending digest", width, theme);
|
|
7188
6132
|
const textContent = result.content?.[0]?.type === "text" ? result.content[0].text : "";
|
|
7189
6133
|
if (isError || result.isError) {
|
|
7190
|
-
return renderErrorResult(textContent, { expanded, width, fallback: "
|
|
6134
|
+
return renderErrorResult(textContent, { expanded, width, fallback: "digest failed", theme });
|
|
7191
6135
|
}
|
|
7192
|
-
const
|
|
7193
|
-
const
|
|
7194
|
-
|
|
7195
|
-
|
|
7196
|
-
style: issueCount === 0 ? "success" : "warning"
|
|
7197
|
-
});
|
|
6136
|
+
const value = result.details?.readSeekValue;
|
|
6137
|
+
const facets = Array.isArray(value?.select) ? value.select : ["content"];
|
|
6138
|
+
const label = summarizeEnvelope(value?.envelope, facets);
|
|
6139
|
+
let text = summaryLine(label, { hidden: !!textContent && !expanded, theme, style: "success" });
|
|
7198
6140
|
if (expanded && textContent)
|
|
7199
6141
|
text += `
|
|
7200
6142
|
${textContent}`;
|
|
7201
|
-
return new
|
|
6143
|
+
return new Text8(clampLinesToWidth(text.split(`
|
|
7202
6144
|
`), width).join(`
|
|
7203
6145
|
`), 0, 0);
|
|
7204
6146
|
}
|
|
@@ -7211,8 +6153,8 @@ import {
|
|
|
7211
6153
|
DEFAULT_MAX_LINES as DEFAULT_MAX_LINES5,
|
|
7212
6154
|
truncateHead as truncateHead5
|
|
7213
6155
|
} from "@earendil-works/pi-coding-agent";
|
|
7214
|
-
import { Text as
|
|
7215
|
-
import { Type as
|
|
6156
|
+
import { Text as Text9 } from "@earendil-works/pi-tui";
|
|
6157
|
+
import { Type as Type12 } from "@sinclair/typebox";
|
|
7216
6158
|
var NODE_KINDS = [
|
|
7217
6159
|
"artifact",
|
|
7218
6160
|
"footer",
|
|
@@ -7227,7 +6169,7 @@ var NODE_KINDS = [
|
|
|
7227
6169
|
];
|
|
7228
6170
|
var VIEW_PROMPT_METADATA = defineToolPromptMetadata({
|
|
7229
6171
|
promptUrl: new URL("../prompts/view.md", import.meta.url),
|
|
7230
|
-
promptSnippet: "
|
|
6172
|
+
promptSnippet: "Digest a document's structure or selected content"
|
|
7231
6173
|
});
|
|
7232
6174
|
async function executeView(opts) {
|
|
7233
6175
|
const { params, signal, cwd } = opts;
|
|
@@ -7248,6 +6190,12 @@ async function executeView(opts) {
|
|
|
7248
6190
|
path: input.path
|
|
7249
6191
|
});
|
|
7250
6192
|
}
|
|
6193
|
+
if (input.outline && (input.visionMode !== undefined || input.visionLevel !== undefined)) {
|
|
6194
|
+
return buildToolErrorResult("view", "invalid-params-combo", "Cannot combine outline with visionMode or visionLevel.", { path: input.path });
|
|
6195
|
+
}
|
|
6196
|
+
if (input.visionLevel !== undefined && input.visionMode === undefined) {
|
|
6197
|
+
return buildToolErrorResult("view", "invalid-params-combo", "visionLevel requires visionMode.", { path: input.path });
|
|
6198
|
+
}
|
|
7251
6199
|
const filePath = resolveToCwd(input.path, cwd);
|
|
7252
6200
|
try {
|
|
7253
6201
|
const output = await readSeekView(filePath, {
|
|
@@ -7256,6 +6204,8 @@ async function executeView(opts) {
|
|
|
7256
6204
|
kind: input.kind,
|
|
7257
6205
|
depth: depth.value,
|
|
7258
6206
|
outline: input.outline,
|
|
6207
|
+
visionMode: input.visionMode,
|
|
6208
|
+
visionLevel: input.visionLevel,
|
|
7259
6209
|
signal
|
|
7260
6210
|
});
|
|
7261
6211
|
const truncation = truncateHead5(output, {
|
|
@@ -7286,15 +6236,26 @@ function registerViewTool(pi) {
|
|
|
7286
6236
|
description: VIEW_PROMPT_METADATA.description,
|
|
7287
6237
|
promptSnippet: VIEW_PROMPT_METADATA.promptSnippet,
|
|
7288
6238
|
promptGuidelines: VIEW_PROMPT_METADATA.promptGuidelines,
|
|
7289
|
-
parameters:
|
|
6239
|
+
parameters: Type12.Object({
|
|
7290
6240
|
path: filePathParam(),
|
|
7291
|
-
node:
|
|
7292
|
-
page: optionalIntOrString("One-based
|
|
7293
|
-
kind:
|
|
6241
|
+
node: Type12.Optional(Type12.String({ description: "Node ID to use as the view root" })),
|
|
6242
|
+
page: optionalIntOrString("One-based source page"),
|
|
6243
|
+
kind: Type12.Optional(Type12.Union(NODE_KINDS.map((kind) => Type12.Literal(kind)), {
|
|
7294
6244
|
description: "Node kind filter"
|
|
7295
6245
|
})),
|
|
7296
6246
|
depth: optionalIntOrString("Maximum depth below selected roots"),
|
|
7297
|
-
|
|
6247
|
+
visionMode: Type12.Optional(Type12.Union([
|
|
6248
|
+
Type12.Literal("all"),
|
|
6249
|
+
Type12.Literal("ocr"),
|
|
6250
|
+
Type12.Literal("caption"),
|
|
6251
|
+
Type12.Literal("objects")
|
|
6252
|
+
], { description: "Analyze selected assets: all, ocr, caption, or objects" })),
|
|
6253
|
+
visionLevel: Type12.Optional(Type12.Union([
|
|
6254
|
+
Type12.Literal("low"),
|
|
6255
|
+
Type12.Literal("medium"),
|
|
6256
|
+
Type12.Literal("high")
|
|
6257
|
+
], { description: "Vision inference level: low (default), medium, or high" })),
|
|
6258
|
+
outline: Type12.Optional(Type12.Boolean({ description: "Return outline nodes only" }))
|
|
7298
6259
|
}),
|
|
7299
6260
|
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
7300
6261
|
return executeView({ params, signal, cwd: ctx.cwd });
|
|
@@ -7304,7 +6265,7 @@ function registerViewTool(pi) {
|
|
|
7304
6265
|
const cwd = context.cwd ?? process.cwd();
|
|
7305
6266
|
const displayPath = typeof args?.path === "string" ? args.path : "?";
|
|
7306
6267
|
const text = `${theme.fg("toolTitle", theme.bold("view"))} ${linkToolPath(theme.fg("accent", displayPath), displayPath, cwd)}`;
|
|
7307
|
-
return new
|
|
6268
|
+
return new Text9(clampLineToWidth(text, context.width), 0, 0);
|
|
7308
6269
|
},
|
|
7309
6270
|
renderResult(result, options, theme, ...rest) {
|
|
7310
6271
|
const { isPartial, isError, expanded, width } = resolveRenderResultContext(options, rest);
|
|
@@ -7324,7 +6285,7 @@ function registerViewTool(pi) {
|
|
|
7324
6285
|
if (expanded && textContent)
|
|
7325
6286
|
text += `
|
|
7326
6287
|
${textContent}`;
|
|
7327
|
-
return new
|
|
6288
|
+
return new Text9(clampLinesToWidth(text.split(`
|
|
7328
6289
|
`), width).join(`
|
|
7329
6290
|
`), 0, 0);
|
|
7330
6291
|
}
|
|
@@ -7356,10 +6317,9 @@ var READSEEK_TOOL_ENTRIES = [
|
|
|
7356
6317
|
{ builtIn: null, readSeekName: "readSeek_search" },
|
|
7357
6318
|
{ builtIn: null, readSeekName: "readSeek_refs" },
|
|
7358
6319
|
{ builtIn: null, readSeekName: "readSeek_rename" },
|
|
7359
|
-
{ builtIn: null, readSeekName: "readSeek_hover" },
|
|
7360
6320
|
{ builtIn: "write", readSeekName: "readSeek_write" },
|
|
7361
6321
|
{ builtIn: null, readSeekName: "readSeek_def" },
|
|
7362
|
-
{ builtIn: null, readSeekName: "
|
|
6322
|
+
{ builtIn: null, readSeekName: "readSeek_digest" },
|
|
7363
6323
|
{ builtIn: null, readSeekName: "readSeek_view" }
|
|
7364
6324
|
];
|
|
7365
6325
|
function formatSettingsWarning(warning) {
|
|
@@ -7370,7 +6330,7 @@ function editingPolicy(readName, editName, writeName) {
|
|
|
7370
6330
|
"ReadSeek editing policy:",
|
|
7371
6331
|
`- Read first with ${readName} when preparing to edit; ${editName} needs fresh LINE:HASH anchors.`,
|
|
7372
6332
|
`- Use ${editName} for existing files, ${writeName} for whole-file creation or replacement, and readSeek_rename for symbol renames.`,
|
|
7373
|
-
"- Run
|
|
6333
|
+
"- Run readSeek_digest with select diagnostics after source edits for a quick syntax check."
|
|
7374
6334
|
].join(`
|
|
7375
6335
|
`);
|
|
7376
6336
|
}
|
|
@@ -7380,7 +6340,7 @@ function piReadSeekExtension(pi) {
|
|
|
7380
6340
|
const hasFreshAnchors = (absolutePath) => sessionAnchors.hasFreshAnchors(absolutePath);
|
|
7381
6341
|
const forgetAnchors = (absolutePath) => sessionAnchors.forget(absolutePath);
|
|
7382
6342
|
const { settings } = resolveReadSeekJsonSettings();
|
|
7383
|
-
const replacedBuiltIns = new Set(settings.
|
|
6343
|
+
const replacedBuiltIns = new Set(settings.overrideTools ?? []);
|
|
7384
6344
|
const binaryAvailable = readSeekBinaryAvailability().available;
|
|
7385
6345
|
const swap = (builtIn, readSeekName) => binaryAvailable && replacedBuiltIns.has(builtIn) ? builtIn : readSeekName;
|
|
7386
6346
|
const readName = swap("read", "readSeek_read");
|
|
@@ -7399,9 +6359,8 @@ function piReadSeekExtension(pi) {
|
|
|
7399
6359
|
registerSearchTool(pi, { onFileAnchored: markAnchored });
|
|
7400
6360
|
registerRefsTool(pi, { onFileAnchored: markAnchored });
|
|
7401
6361
|
registerRenameTool(pi, { onFileMutated: forgetAnchors });
|
|
7402
|
-
registerHoverTool(pi);
|
|
7403
6362
|
registerDefTool(pi, { onFileAnchored: markAnchored });
|
|
7404
|
-
|
|
6363
|
+
registerDigestTool(pi, { onFileAnchored: markAnchored });
|
|
7405
6364
|
registerViewTool(pi);
|
|
7406
6365
|
registerWriteTool(pi, { onFileAnchored: markAnchored, name: writeName });
|
|
7407
6366
|
pi.on("before_agent_start", (event) => {
|