pi-readseek 0.8.24 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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 map or symbol to inspect large files without reading them in full."
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 = ["replacedTools", "imageMode", "syntaxValidation", "timeoutMs", "grep", "display"];
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 readReplacedTools(raw, source, warnings) {
1242
- if (!("replacedTools" in raw))
511
+ function readOverrideTools(raw, source, warnings) {
512
+ if (!("overrideTools" in raw))
1243
513
  return;
1244
- const value = raw.replacedTools;
514
+ const value = raw.overrideTools;
1245
515
  if (!Array.isArray(value)) {
1246
- warnings.push(invalid(source, "readseek.replacedTools"));
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.replacedTools[${index}]`));
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 replacedTools = readReplacedTools(section, source, warnings);
1300
- if (replacedTools !== undefined)
1301
- settings.replacedTools = replacedTools;
569
+ const overrideTools = readOverrideTools(section, source, warnings);
570
+ if (overrideTools !== undefined)
571
+ settings.overrideTools = overrideTools;
1302
572
  const imageMode = readImageMode(section, source, warnings);
1303
573
  if (imageMode !== undefined)
1304
574
  settings.imageMode = imageMode;
@@ -1398,6 +668,9 @@ function appendGitSelection(args, options) {
1398
668
  if (selection.length > 0)
1399
669
  args.push("--git", selection.join(","));
1400
670
  }
671
+ function lineLocationSelector(line, column) {
672
+ return column === undefined ? `line:${line}` : `line:${line}:${column}`;
673
+ }
1401
674
  function normalizeLanguage(language) {
1402
675
  return language === "java" ? "Java" : language;
1403
676
  }
@@ -1458,6 +731,8 @@ function readSeekPlatform() {
1458
731
  return `${process.platform}-${process.arch}`;
1459
732
  }
1460
733
  function readSeekBinaryPath() {
734
+ if (process.env.READSEEK_BIN)
735
+ return process.env.READSEEK_BIN;
1461
736
  const platform = readSeekPlatform();
1462
737
  const platformPackage = READSEEK_PLATFORM_PACKAGES[platform];
1463
738
  if (!platformPackage) {
@@ -1553,13 +828,19 @@ async function spawnReadSeekRaw(args, options = {}) {
1553
828
  };
1554
829
  const stdin = options.stdin;
1555
830
  const stdio = [stdin === undefined ? "ignore" : "pipe", "pipe", "pipe"];
1556
- const child = spawn(readSeekBinaryPath(), args, { stdio, signal: options.signal });
1557
- const timeoutMs = options.timeoutMs ?? readSeekTimeoutMs();
1558
- timeout = setTimeout(() => {
1559
- child.kill("SIGKILL");
1560
- fail(new Error(`readseek timed out after ${timeoutMs} ms`));
1561
- }, timeoutMs);
1562
- timeout.unref?.();
831
+ const cancelable = options.cancelable !== false;
832
+ const child = spawn(readSeekBinaryPath(), args, {
833
+ stdio,
834
+ signal: cancelable ? options.signal : undefined
835
+ });
836
+ if (cancelable) {
837
+ const timeoutMs = options.timeoutMs ?? readSeekTimeoutMs();
838
+ timeout = setTimeout(() => {
839
+ child.kill("SIGKILL");
840
+ fail(new Error(`readseek timed out after ${timeoutMs} ms`));
841
+ }, timeoutMs);
842
+ timeout.unref?.();
843
+ }
1563
844
  const childStdout = child.stdout;
1564
845
  const childStderr = child.stderr;
1565
846
  const childStdin = child.stdin;
@@ -1644,10 +925,15 @@ async function readSeekView(filePath, options = {}) {
1644
925
  args.push("--depth", String(options.depth));
1645
926
  if (options.outline)
1646
927
  args.push("--outline");
1647
- return runReadSeekRaw(args, { signal: options.signal });
928
+ if (options.visionMode !== undefined)
929
+ args.push("--vision-mode", options.visionMode);
930
+ if (options.visionLevel !== undefined)
931
+ args.push("--vision-level", options.visionLevel);
932
+ const runOptions = { signal: options.signal };
933
+ return options.visionMode !== undefined ? runReadSeekVisionRaw(args, runOptions) : runReadSeekRaw(args, runOptions);
1648
934
  }
1649
935
  var visionInvocationTail = Promise.resolve();
1650
- async function runReadSeekVision(args, options = {}) {
936
+ async function runReadSeekVisionRaw(args, options = {}) {
1651
937
  let release;
1652
938
  const gate = new Promise((resolve) => {
1653
939
  release = resolve;
@@ -1676,7 +962,7 @@ async function runReadSeekVision(args, options = {}) {
1676
962
  await predecessor;
1677
963
  }
1678
964
  signal?.throwIfAborted();
1679
- return await runReadSeek(args, {
965
+ return await runReadSeekRaw(args, {
1680
966
  ...options,
1681
967
  timeoutMs: options.timeoutMs ?? resolveReadSeekTimeoutMs() ?? DEFAULT_READSEEK_VISION_TIMEOUT_MS
1682
968
  });
@@ -1684,6 +970,9 @@ async function runReadSeekVision(args, options = {}) {
1684
970
  release();
1685
971
  }
1686
972
  }
973
+ async function runReadSeekVision(args, options = {}) {
974
+ return JSON.parse(await runReadSeekVisionRaw(args, options));
975
+ }
1687
976
  function requireNumber(value, field) {
1688
977
  if (typeof value !== "number" || !Number.isSafeInteger(value))
1689
978
  throw new Error(`invalid readseek ${field}: expected safe integer`);
@@ -1808,39 +1097,6 @@ function parseSearchOutput(value) {
1808
1097
  })
1809
1098
  };
1810
1099
  }
1811
- async function readSeekRead(filePath, startLine, endLine, options = {}) {
1812
- const args = ["read", startLine === undefined ? filePath : `${filePath}:${startLine}`];
1813
- if (endLine !== undefined)
1814
- args.push("--end", String(endLine));
1815
- return parseReadOutput(await runReadSeek(args, { signal: options.signal }));
1816
- }
1817
- function fileMapFromReadSeekOutput(output, filePath, totalBytes) {
1818
- if (output.language === "unknown" && output.symbols.length === 0)
1819
- return null;
1820
- return {
1821
- path: filePath,
1822
- totalLines: output.line_count,
1823
- totalBytes,
1824
- language: normalizeLanguage(output.language),
1825
- detailLevel: "full" /* Full */,
1826
- symbols: symbolsFromReadSeek(output.symbols)
1827
- };
1828
- }
1829
- async function readSeekMap(filePath, totalBytes, options = {}) {
1830
- const output = parseMapOutput(await runReadSeek(["map", filePath], { signal: options.signal }));
1831
- return fileMapFromReadSeekOutput(output, filePath, totalBytes);
1832
- }
1833
- async function readSeekSearch(target, pattern, options = {}) {
1834
- const args = ["search", target, pattern];
1835
- if (options.language)
1836
- args.push("--language", options.language);
1837
- appendGitSelection(args, options);
1838
- return parseSearchOutput(await runReadSeek(args, { signal: options.signal })).results;
1839
- }
1840
- async function readSeekMapContent(filePath, content, options = {}) {
1841
- const output = parseMapOutput(await runReadSeek(["map", `stdin:${filePath}`], { signal: options.signal, stdin: content }));
1842
- return fileMapFromReadSeekOutput(output, filePath, Buffer.byteLength(content, "utf8"));
1843
- }
1844
1100
  function optionalString(value, field) {
1845
1101
  if (value === undefined || value === null)
1846
1102
  return;
@@ -1870,19 +1126,6 @@ function parseRefsOutput(value) {
1870
1126
  })
1871
1127
  };
1872
1128
  }
1873
- async function readSeekRefs(target, name, options = {}) {
1874
- const args = ["refs", target, name];
1875
- if (options.scope)
1876
- args.push("--scope");
1877
- if (options.line !== undefined)
1878
- args.push("--line", String(options.line));
1879
- if (options.column !== undefined)
1880
- args.push("--column", String(options.column));
1881
- if (options.language)
1882
- args.push("--language", options.language);
1883
- appendGitSelection(args, options);
1884
- return parseRefsOutput(await runReadSeek(args, { signal: options.signal })).references;
1885
- }
1886
1129
  function parseDiagnosticKind(value) {
1887
1130
  if (value === "error" || value === "missing")
1888
1131
  return value;
@@ -1909,9 +1152,6 @@ function parseCheckOutput(value) {
1909
1152
  })
1910
1153
  };
1911
1154
  }
1912
- async function readSeekCheck(filePath, content, options = {}) {
1913
- return parseCheckOutput(await runReadSeek(["check", `stdin:${filePath}`], { signal: options.signal, stdin: content }));
1914
- }
1915
1155
  function parseOcrText(value) {
1916
1156
  return optionalString(value, "ocr");
1917
1157
  }
@@ -1933,6 +1173,11 @@ function parseDetectedObjects(value) {
1933
1173
  };
1934
1174
  });
1935
1175
  }
1176
+ function parseImageEncoding(value) {
1177
+ if (value === "base64")
1178
+ return value;
1179
+ throw new Error("invalid image encoding");
1180
+ }
1936
1181
  function parseDetectOutput(value) {
1937
1182
  if (!value || typeof value !== "object")
1938
1183
  throw new Error("invalid readseek detect output");
@@ -1983,20 +1228,228 @@ function parseDetectOutput(value) {
1983
1228
  }
1984
1229
  return { kind: "text", type, file, mime };
1985
1230
  }
1986
- function parseImageEncoding(value) {
1987
- if (value === "base64")
1988
- return value;
1989
- throw new Error("invalid image encoding");
1231
+ function parseDocumentCache(value) {
1232
+ if (!value || typeof value !== "object")
1233
+ throw new Error("invalid readseek document cache");
1234
+ const cache = value;
1235
+ const state = requireString(cache.state, "cache.state");
1236
+ if (state !== "built" && state !== "reused")
1237
+ throw new Error("invalid readseek cache.state");
1238
+ return {
1239
+ content_hash: requireString(cache.content_hash, "cache.content_hash"),
1240
+ state
1241
+ };
1242
+ }
1243
+ function parseIdentifyOutput(value) {
1244
+ if (!value || typeof value !== "object")
1245
+ throw new Error("invalid readseek identify output");
1246
+ const output = value;
1247
+ const identifier = output.identifier;
1248
+ const symbol = output.symbol;
1249
+ return {
1250
+ file: requireString(output.file, "file"),
1251
+ language: requireString(output.language, "language"),
1252
+ engine: optionalString(output.engine, "engine"),
1253
+ line_count: requireNumber(output.line_count, "line_count"),
1254
+ file_hash: requireString(output.file_hash, "file_hash"),
1255
+ line: requireNumber(output.line, "line"),
1256
+ column: requireNumber(output.column, "column"),
1257
+ line_hash: requireString(output.line_hash, "line_hash"),
1258
+ identifier: identifier && typeof identifier === "object" ? {
1259
+ text: requireString(identifier.text, "identifier.text"),
1260
+ start_column: requireNumber(identifier.start_column, "identifier.start_column"),
1261
+ end_column: requireNumber(identifier.end_column, "identifier.end_column"),
1262
+ start_byte: requireNumber(identifier.start_byte, "identifier.start_byte"),
1263
+ end_byte: requireNumber(identifier.end_byte, "identifier.end_byte")
1264
+ } : undefined,
1265
+ symbol: symbol && typeof symbol === "object" ? {
1266
+ name: requireString(symbol.name, "symbol.name"),
1267
+ kind: requireString(symbol.kind, "symbol.kind"),
1268
+ qualified_name: requireString(symbol.qualified_name, "symbol.qualified_name"),
1269
+ start_line: requireNumber(symbol.start_line, "symbol.start_line"),
1270
+ end_line: requireNumber(symbol.end_line, "symbol.end_line")
1271
+ } : undefined
1272
+ };
1273
+ }
1274
+ function parseImageContent(value) {
1275
+ const detection = parseDetectOutput(value);
1276
+ if (detection.kind !== "image")
1277
+ throw new Error("invalid readseek image content");
1278
+ return detection;
1279
+ }
1280
+ function parseTextContent(value) {
1281
+ if (!value || typeof value !== "object")
1282
+ throw new Error("invalid readseek content");
1283
+ const output = value;
1284
+ if (output.hashlines !== undefined && output.start_line === undefined) {
1285
+ const hashlines = parseSearchHashlines(output.hashlines, "content.hashlines");
1286
+ const first = hashlines[0];
1287
+ const last = hashlines[hashlines.length - 1];
1288
+ return {
1289
+ file: requireString(output.file, "file"),
1290
+ language: requireString(output.language, "language"),
1291
+ line_count: requireNumber(output.line_count, "line_count"),
1292
+ file_hash: requireString(output.file_hash, "file_hash"),
1293
+ start_line: first?.line ?? 1,
1294
+ end_line: last?.line ?? 0,
1295
+ hashlines
1296
+ };
1297
+ }
1298
+ return parseReadOutput(value);
1299
+ }
1300
+ function parseDigestEnvelope(value) {
1301
+ if (!value || typeof value !== "object")
1302
+ throw new Error("invalid readseek digest output");
1303
+ const output = value;
1304
+ if (output.metadata === undefined)
1305
+ throw new Error("invalid readseek digest metadata");
1306
+ const result = {
1307
+ metadata: parseDetectOutput(output.metadata)
1308
+ };
1309
+ if (output.cache !== undefined)
1310
+ result.cache = parseDocumentCache(output.cache);
1311
+ if (output.content !== undefined)
1312
+ result.content = output.content;
1313
+ if (output.map !== undefined)
1314
+ result.map = parseMapOutput(output.map);
1315
+ if (output.diagnostics !== undefined)
1316
+ result.diagnostics = parseCheckOutput(output.diagnostics);
1317
+ if (output.identity !== undefined)
1318
+ result.identity = parseIdentifyOutput(output.identity);
1319
+ return result;
1320
+ }
1321
+ function digestSelectArg(select) {
1322
+ if (select === undefined)
1323
+ return "content";
1324
+ return Array.isArray(select) ? select.join(",") : select;
1325
+ }
1326
+ function digestArgs(target, options) {
1327
+ const args = ["digest", target, "--select", digestSelectArg(options.select)];
1328
+ if (options.at !== undefined)
1329
+ args.push("--at", options.at);
1330
+ if (options.end !== undefined)
1331
+ args.push("--end", String(options.end));
1332
+ if (options.limit !== undefined)
1333
+ args.push("--limit", String(options.limit));
1334
+ if (options.language)
1335
+ args.push("--language", options.language);
1336
+ if (options.visionMode !== undefined)
1337
+ args.push("--vision-mode", options.visionMode);
1338
+ if (options.visionLevel !== undefined)
1339
+ args.push("--vision-level", options.visionLevel);
1340
+ if (options.stdin !== undefined) {
1341
+ const stdinName = options.stdinName ?? target;
1342
+ if (target === "-")
1343
+ args.push("--stdin-name", stdinName);
1344
+ }
1345
+ return args;
1346
+ }
1347
+ async function runDigestRaw(target, options = {}) {
1348
+ const args = digestArgs(target, options);
1349
+ const runOptions = {
1350
+ signal: options.signal,
1351
+ stdin: options.stdin,
1352
+ timeoutMs: options.timeoutMs
1353
+ };
1354
+ return options.vision ? await runReadSeekVision(args, runOptions) : await runReadSeek(args, runOptions);
1355
+ }
1356
+ async function readSeekDigestNative(target, options = {}) {
1357
+ return runDigestRaw(target, options);
1358
+ }
1359
+ async function readSeekDigest(target, options = {}) {
1360
+ return parseDigestEnvelope(await runDigestRaw(target, options));
1361
+ }
1362
+ function requireDigestContent(envelope, field) {
1363
+ if (envelope.content === undefined)
1364
+ throw new Error(`invalid readseek ${field}: missing content`);
1365
+ return envelope.content;
1366
+ }
1367
+ function fileMapFromReadSeekOutput(output, filePath, totalBytes) {
1368
+ if (output.language === "unknown" && output.symbols.length === 0)
1369
+ return null;
1370
+ return {
1371
+ path: filePath,
1372
+ totalLines: output.line_count,
1373
+ totalBytes,
1374
+ language: normalizeLanguage(output.language),
1375
+ detailLevel: "full" /* Full */,
1376
+ symbols: symbolsFromReadSeek(output.symbols)
1377
+ };
1378
+ }
1379
+ async function readSeekMap(filePath, totalBytes, options = {}) {
1380
+ const envelope = await readSeekDigest(filePath, { select: "map", signal: options.signal });
1381
+ if (!envelope.map)
1382
+ return null;
1383
+ return fileMapFromReadSeekOutput(envelope.map, filePath, totalBytes);
1384
+ }
1385
+ async function readSeekSearch(target, pattern, options = {}) {
1386
+ const args = ["search", target, pattern];
1387
+ if (options.language)
1388
+ args.push("--language", options.language);
1389
+ appendGitSelection(args, options);
1390
+ return parseSearchOutput(await runReadSeek(args, { signal: options.signal })).results;
1391
+ }
1392
+ async function readSeekRefs(target, name, options = {}) {
1393
+ const args = ["refs", target];
1394
+ if (options.scope) {
1395
+ if (options.line !== undefined)
1396
+ args.push("--at", lineLocationSelector(options.line, options.column));
1397
+ } else {
1398
+ args.push(name);
1399
+ }
1400
+ if (options.language)
1401
+ args.push("--language", options.language);
1402
+ appendGitSelection(args, options);
1403
+ return parseRefsOutput(await runReadSeek(args, { signal: options.signal })).references;
1404
+ }
1405
+ async function readSeekCheck(filePath, content, options = {}) {
1406
+ const envelope = await readSeekDigest("-", {
1407
+ select: "diagnostics",
1408
+ stdin: content,
1409
+ stdinName: filePath,
1410
+ signal: options.signal
1411
+ });
1412
+ if (!envelope.diagnostics)
1413
+ throw new Error("invalid readseek diagnostics: missing diagnostics facet");
1414
+ return envelope.diagnostics;
1990
1415
  }
1991
1416
  async function readSeekDetect(filePath, options = {}) {
1992
- return parseDetectOutput(await runReadSeek(["detect", filePath], { signal: options.signal }));
1417
+ const envelope = await readSeekDigest(filePath, { select: "metadata", signal: options.signal });
1418
+ return envelope.metadata;
1419
+ }
1420
+ async function readSeekDigestContent(filePath, options = {}) {
1421
+ const envelope = await readSeekDigest(filePath, {
1422
+ select: "content",
1423
+ at: options.startLine !== undefined ? lineLocationSelector(options.startLine) : undefined,
1424
+ end: options.endLine,
1425
+ visionMode: options.visionMode,
1426
+ signal: options.signal,
1427
+ vision: options.vision
1428
+ });
1429
+ if (envelope.metadata.kind === "pdf") {
1430
+ return { metadata: envelope.metadata, cache: envelope.cache };
1431
+ }
1432
+ const content = requireDigestContent(envelope, "content");
1433
+ if (envelope.metadata.kind === "image") {
1434
+ return { metadata: envelope.metadata, image: parseImageContent(content) };
1435
+ }
1436
+ return { metadata: envelope.metadata, text: parseTextContent(content) };
1993
1437
  }
1994
1438
  async function readSeekImage(filePath, modes, options = {}) {
1995
1439
  const requestedModes = modes.length > 1 ? ["all"] : modes;
1996
- const results = await Promise.allSettled(requestedModes.map(async (mode) => parseDetectOutput(await runReadSeekVision(["read", "--vision-mode", mode, filePath], { signal: options.signal }))));
1440
+ const results = await Promise.allSettled(requestedModes.map(async (mode) => {
1441
+ const digested = await readSeekDigestContent(filePath, {
1442
+ visionMode: mode,
1443
+ signal: options.signal,
1444
+ vision: true
1445
+ });
1446
+ if (!digested.image)
1447
+ throw new Error(`readseek returned no image analysis for ${filePath}`);
1448
+ return digested.image;
1449
+ }));
1997
1450
  let merged;
1998
1451
  for (const result of results) {
1999
- if (result.status !== "fulfilled" || result.value.kind !== "image")
1452
+ if (result.status !== "fulfilled")
2000
1453
  continue;
2001
1454
  const detection = result.value;
2002
1455
  merged = merged === undefined ? detection : {
@@ -2014,52 +1467,46 @@ async function readSeekImage(filePath, modes, options = {}) {
2014
1467
  throw new Error(`readseek returned no image analysis for ${filePath}`);
2015
1468
  }
2016
1469
  async function readSeekPreparedImage(filePath, options = {}) {
2017
- const output = parseDetectOutput(await runReadSeek(["read", "--vision-mode", "none", filePath], { signal: options.signal }));
2018
- if (output.kind !== "image" || output.encoding !== "base64" || output.data === undefined || output.mime === undefined) {
1470
+ const digested = await readSeekDigestContent(filePath, {
1471
+ visionMode: "none",
1472
+ signal: options.signal
1473
+ });
1474
+ const output = digested.image;
1475
+ if (!output || output.encoding !== "base64" || output.data === undefined || output.mime === undefined) {
2019
1476
  throw new Error(`readseek returned no prepared image for ${filePath}`);
2020
1477
  }
2021
1478
  return { mime: output.mime, encoding: output.encoding, data: output.data };
2022
1479
  }
2023
- function parsePdfImage(value) {
1480
+ function parseEditOutput(value) {
2024
1481
  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) {
2045
- if (!value || typeof value !== "object")
2046
- throw new Error("invalid readseek PDF output");
1482
+ throw new Error("invalid readseek edit output");
2047
1483
  const output = value;
2048
- if (output.format !== "pdf" || !Array.isArray(output.images))
2049
- throw new Error("invalid readseek PDF output");
2050
1484
  return {
2051
- format: output.format,
2052
- pages: requireNumber(output.pages, "PDF pages"),
2053
- markdown: requireString(output.markdown, "PDF markdown"),
2054
- images: output.images.map(parsePdfImage)
1485
+ file: requireString(output.file, "file"),
1486
+ before_hash: requireString(output.before_hash, "before_hash"),
1487
+ file_hash: requireString(output.file_hash, "file_hash"),
1488
+ plan_hash: requireString(output.plan_hash, "plan_hash"),
1489
+ applied: requireBoolean(output.applied, "applied"),
1490
+ changed: requireBoolean(output.changed, "changed"),
1491
+ edit_count: requireNumber(output.edit_count, "edit_count"),
1492
+ before_content: optionalString(output.before_content, "before_content"),
1493
+ content: optionalString(output.content, "content")
2055
1494
  };
2056
1495
  }
2057
- async function readSeekPdf(filePath, mode, options = {}) {
2058
- const run = mode === "none" ? runReadSeek : runReadSeekVision;
2059
- const args = ["read", "--vision-mode", mode, filePath];
2060
- if (options.page !== undefined)
2061
- args.push("--page", String(options.page));
2062
- return parsePdfOutput(await run(args, { signal: options.signal }));
1496
+ async function readSeekEdit(filePath, edits, options = {}) {
1497
+ const args = ["edit", filePath];
1498
+ if (options.apply)
1499
+ args.push("--apply");
1500
+ if (options.planHash)
1501
+ args.push("--plan-hash", options.planHash);
1502
+ if (options.language)
1503
+ args.push("--language", options.language);
1504
+ return parseEditOutput(await runReadSeek(args, {
1505
+ signal: options.signal,
1506
+ stdin: JSON.stringify({ edits }),
1507
+ timeoutMs: options.timeoutMs,
1508
+ cancelable: options.cancelable
1509
+ }));
2063
1510
  }
2064
1511
  function parseRenameConflicts(value, field) {
2065
1512
  if (value === undefined)
@@ -2131,10 +1578,7 @@ function parseRenameOutput(value) {
2131
1578
  };
2132
1579
  }
2133
1580
  async function readSeekRename(filePath, options) {
2134
- const args = ["rename", filePath, "--line", String(options.line)];
2135
- if (options.column !== undefined)
2136
- args.push("--column", String(options.column));
2137
- args.push("--to", options.to);
1581
+ const args = ["rename", filePath, "--at", lineLocationSelector(options.line, options.column), "--to", options.to];
2138
1582
  if (options.apply)
2139
1583
  args.push("--apply");
2140
1584
  if (options.workspace)
@@ -2144,46 +1588,6 @@ async function readSeekRename(filePath, options) {
2144
1588
  appendGitSelection(args, options);
2145
1589
  return parseRenameOutput(await runReadSeek(args, { signal: options.signal }));
2146
1590
  }
2147
- function parseIdentifyOutput(value) {
2148
- if (!value || typeof value !== "object")
2149
- throw new Error("invalid readseek identify output");
2150
- const output = value;
2151
- const identifier = output.identifier;
2152
- const symbol = output.symbol;
2153
- return {
2154
- file: requireString(output.file, "file"),
2155
- language: requireString(output.language, "language"),
2156
- engine: optionalString(output.engine, "engine"),
2157
- line_count: requireNumber(output.line_count, "line_count"),
2158
- file_hash: requireString(output.file_hash, "file_hash"),
2159
- line: requireNumber(output.line, "line"),
2160
- column: requireNumber(output.column, "column"),
2161
- line_hash: requireString(output.line_hash, "line_hash"),
2162
- identifier: identifier && typeof identifier === "object" ? {
2163
- text: requireString(identifier.text, "identifier.text"),
2164
- start_column: requireNumber(identifier.start_column, "identifier.start_column"),
2165
- end_column: requireNumber(identifier.end_column, "identifier.end_column"),
2166
- start_byte: requireNumber(identifier.start_byte, "identifier.start_byte"),
2167
- end_byte: requireNumber(identifier.end_byte, "identifier.end_byte")
2168
- } : undefined,
2169
- symbol: symbol && typeof symbol === "object" ? {
2170
- name: requireString(symbol.name, "symbol.name"),
2171
- kind: requireString(symbol.kind, "symbol.kind"),
2172
- qualified_name: requireString(symbol.qualified_name, "symbol.qualified_name"),
2173
- start_line: requireNumber(symbol.start_line, "symbol.start_line"),
2174
- end_line: requireNumber(symbol.end_line, "symbol.end_line")
2175
- } : undefined
2176
- };
2177
- }
2178
- async function readSeekIdentify(filePath, content, options = {}) {
2179
- const target = options.line === undefined ? `stdin:${filePath}` : `stdin:${filePath}:${options.line}`;
2180
- const args = ["identify", target];
2181
- if (options.column !== undefined)
2182
- args.push("--column", String(options.column));
2183
- if (options.language)
2184
- args.push("--language", options.language);
2185
- return parseIdentifyOutput(await runReadSeek(args, { signal: options.signal, stdin: content }));
2186
- }
2187
1591
  function parseDefCompact(value) {
2188
1592
  if (!value || typeof value !== "object")
2189
1593
  throw new Error("invalid readseek def output");
@@ -2871,9 +2275,6 @@ function formatReadCallText(args) {
2871
2275
  if (typeof args?.symbol === "string" && args.symbol) {
2872
2276
  return { path: rawPath, suffix: `→ symbol ${args.symbol}` };
2873
2277
  }
2874
- if (args?.map === true) {
2875
- return { path: rawPath, suffix: "+ map" };
2876
- }
2877
2278
  if (typeof args?.offset === "number") {
2878
2279
  const offset = args.offset;
2879
2280
  const limit = typeof args?.limit === "number" ? args.limit : undefined;
@@ -3162,9 +2563,6 @@ function optionalIntOrString(description) {
3162
2563
  function filePathParam() {
3163
2564
  return Type.String({ description: "Absolute or relative file path" });
3164
2565
  }
3165
- function mapParam() {
3166
- return Type.Optional(Type.Boolean({ description: "Append structural map" }));
3167
- }
3168
2566
  function registerReadSeekTool(pi, tool) {
3169
2567
  pi.registerTool(tool);
3170
2568
  return tool;
@@ -3211,50 +2609,9 @@ ${lines.join(`
3211
2609
  return "No image analysis returned.";
3212
2610
  return "Image caption returned no text.";
3213
2611
  }
3214
- function formatPdfAnalysis(pdf) {
3215
- const sections = [pdf.markdown.trimEnd()];
3216
- for (const image of pdf.images) {
3217
- const details = [];
3218
- const ocr = image.ocr?.trim();
3219
- if (ocr)
3220
- details.push(`OCR text:
3221
- ${ocr}`);
3222
- const caption = image.caption?.trim();
3223
- if (caption)
3224
- details.push(`Caption:
3225
- ${caption}`);
3226
- if (image.objects?.length) {
3227
- const lines = image.objects.map((object) => `- ${object.label} [${object.bbox.join(", ")}]`);
3228
- details.push(`Detected objects:
3229
- ${lines.join(`
3230
- `)}`);
3231
- }
3232
- if (details.length > 0)
3233
- sections.push(`PDF page ${image.page} image:
3234
- ${details.join(`
3235
-
3236
- `)}`);
3237
- }
3238
- return sections.filter(Boolean).join(`
3239
-
3240
- `);
3241
- }
3242
- function preparedPdfContent(pdf) {
3243
- const content = [{ type: "text", text: truncateDocumentText(pdf.markdown) }];
3244
- for (const image of pdf.images) {
3245
- if (image.encoding !== "base64" || image.data === undefined)
3246
- continue;
3247
- content.push({ type: "text", text: `[PDF page ${image.page} image]` });
3248
- content.push({ type: "image", data: image.data, mimeType: image.mime });
3249
- }
3250
- return content;
3251
- }
3252
- function truncateDocumentText(text) {
3253
- const truncation = truncateHead2(text, { maxLines: DEFAULT_MAX_LINES3, maxBytes: DEFAULT_MAX_BYTES3 });
3254
- if (!truncation.truncated)
3255
- return truncation.content;
3256
- return `${truncation.content}
3257
- [… document output truncated; use readSeek_view with page or node selectors]`;
2612
+ function documentRedirectMessage(path2, cache) {
2613
+ const cacheNote = cache ? ` Index ${cache.state} (hash ${cache.content_hash}).` : "";
2614
+ return `Document content is not available via readSeek_read for ${path2}.${cacheNote} Use readSeek_view to digest structure or selected content.`;
3258
2615
  }
3259
2616
  function skippedImageFile(path2) {
3260
2617
  return {
@@ -3282,25 +2639,14 @@ async function executeRead(opts) {
3282
2639
  const message = `Invalid offset: expected a positive integer, received ${offset.value}.`;
3283
2640
  return buildToolErrorResult("read", "invalid-offset", message, { path: rawParams.path });
3284
2641
  }
3285
- const page = coerceObviousBase10Int(rawParams.page, "page");
3286
- if (!page.ok) {
3287
- return buildToolErrorResult("read", "invalid-page", page.message, { path: rawParams.path });
3288
- }
3289
- if (page.value !== undefined && page.value < 1) {
3290
- const message = `Invalid page: expected a positive integer, received ${page.value}.`;
3291
- return buildToolErrorResult("read", "invalid-page", message, { path: rawParams.path });
3292
- }
3293
2642
  const rawSymbol = typeof rawParams.symbol === "string" && rawParams.symbol.trim().length > 0 ? rawParams.symbol.trim() : undefined;
3294
2643
  const rawBundle = typeof rawParams.bundle === "string" ? rawParams.bundle.trim() : undefined;
3295
- const requestedMapViaBundle = rawBundle === "map" || rawBundle === "local" && rawSymbol === undefined && (rawParams.map ?? true);
3296
2644
  const p = {
3297
2645
  ...rawParams,
3298
2646
  offset: offset.value,
3299
2647
  limit: limit.value,
3300
- page: page.value,
3301
2648
  symbol: rawSymbol,
3302
- map: rawParams.map === true || requestedMapViaBundle,
3303
- bundle: requestedMapViaBundle ? undefined : rawBundle
2649
+ bundle: rawBundle
3304
2650
  };
3305
2651
  const rawPath = p.path.replace(/^@/, "");
3306
2652
  const absolutePath = resolveToCwd(rawPath, cwd);
@@ -3324,14 +2670,6 @@ async function executeRead(opts) {
3324
2670
  const message = 'Cannot use bundle without symbol. Use read({ path, symbol, bundle: "local" }).';
3325
2671
  return buildToolErrorResult("read", "invalid-params-combo", message, { path: rawParams.path });
3326
2672
  }
3327
- if (p.bundle && p.map) {
3328
- const message = "Cannot combine bundle with map. Use one or the other.";
3329
- return buildToolErrorResult("read", "invalid-params-combo", message, { path: rawParams.path });
3330
- }
3331
- if (p.map && p.symbol) {
3332
- const message = "Cannot combine map with symbol. Use one or the other.";
3333
- return buildToolErrorResult("read", "invalid-params-combo", message, { path: rawParams.path });
3334
- }
3335
2673
  throwIfAborted(signal);
3336
2674
  const ext = rawPath.split(".").pop()?.toLowerCase() ?? "";
3337
2675
  let rawBuffer;
@@ -3345,27 +2683,31 @@ async function executeRead(opts) {
3345
2683
  }
3346
2684
  const hasBinaryContent = looksLikeBinary(rawBuffer);
3347
2685
  const hasPdfHeader = rawBuffer.subarray(0, 1024).includes(Buffer.from("%PDF-"));
3348
- if (hasBinaryContent || ext === "pdf" || hasPdfHeader) {
2686
+ const maybeVisual = hasBinaryContent || ext === "pdf" || hasPdfHeader;
2687
+ let earlyDigest;
2688
+ if (maybeVisual) {
3349
2689
  let detection;
3350
2690
  try {
3351
- detection = await readSeekDetect(absolutePath, { signal });
3352
- } catch {
3353
- return buildToolErrorResult("read", "read-error", `Visual file detection failed for ${absolutePath}.`, {
3354
- path: rawParams.path
2691
+ earlyDigest = await readSeekDigestContent(absolutePath, {
2692
+ ...ext === "pdf" || hasPdfHeader ? {} : { visionMode: "none" },
2693
+ signal
3355
2694
  });
2695
+ detection = earlyDigest.metadata;
2696
+ } catch {
2697
+ try {
2698
+ detection = await readSeekDetect(absolutePath, { signal });
2699
+ } catch {
2700
+ return buildToolErrorResult("read", "read-error", `Visual file detection failed for ${absolutePath}.`, {
2701
+ path: rawParams.path
2702
+ });
2703
+ }
3356
2704
  }
3357
- if (!detection) {
3358
- return buildToolErrorResult("read", "read-error", `Visual file detection returned no data for ${absolutePath}.`, {
3359
- path: rawParams.path
3360
- });
2705
+ if (detection.kind === "pdf") {
2706
+ return buildToolErrorResult("read", "document-use-view", documentRedirectMessage(rawParams.path, earlyDigest?.cache), { path: rawParams.path });
3361
2707
  }
3362
- if (detection.kind === "image" || detection.kind === "pdf") {
2708
+ if (detection.kind === "image") {
3363
2709
  if (p.offset !== undefined || p.limit !== undefined) {
3364
- const message = "Cannot combine offset/limit with image or PDF reads.";
3365
- return buildToolErrorResult("read", "invalid-params-combo", message, { path: rawParams.path });
3366
- }
3367
- if (detection.kind !== "pdf" && p.page !== undefined) {
3368
- const message = "The page parameter applies to PDFs only.";
2710
+ const message = "Cannot combine offset/limit with image reads.";
3369
2711
  return buildToolErrorResult("read", "invalid-params-combo", message, { path: rawParams.path });
3370
2712
  }
3371
2713
  const imageMode = resolveReadSeekImageMode();
@@ -3375,38 +2717,14 @@ async function executeRead(opts) {
3375
2717
  path: rawParams.path
3376
2718
  });
3377
2719
  }
3378
- if (detection.kind === "pdf") {
3379
- let pdfImageMode = "none";
3380
- if (imageMode !== "off" && p.image !== undefined && p.image !== "none" && !useNativeVision) {
3381
- pdfImageMode = p.image;
3382
- }
3383
- try {
3384
- const pdf = await readSeekPdf(absolutePath, pdfImageMode, { page: p.page ?? 1, signal });
3385
- if (pdfImageMode !== "none") {
3386
- return succeed({
3387
- content: [{ type: "text", text: truncateDocumentText(formatPdfAnalysis(pdf)) }],
3388
- details: {}
3389
- });
3390
- }
3391
- return succeed({
3392
- content: useNativeVision ? preparedPdfContent(pdf) : [{ type: "text", text: truncateDocumentText(pdf.markdown) }],
3393
- details: {}
3394
- });
3395
- } catch (error) {
3396
- const message = error instanceof Error ? error.message : String(error);
3397
- return buildToolErrorResult("read", "read-error", `PDF reading unavailable: ${message}`, {
3398
- path: rawParams.path
3399
- });
3400
- }
3401
- }
3402
2720
  if (imageMode === "off" || p.image === undefined) {
3403
2721
  return skippedImageFile(rawParams.path);
3404
2722
  }
3405
2723
  if (useNativeVision || p.image === "none") {
3406
2724
  try {
3407
- const image = await readSeekPreparedImage(absolutePath, { signal });
2725
+ const prepared = earlyDigest?.image && earlyDigest.image.encoding === "base64" && earlyDigest.image.data !== undefined && earlyDigest.image.mime !== undefined ? { mime: earlyDigest.image.mime, encoding: earlyDigest.image.encoding, data: earlyDigest.image.data } : await readSeekPreparedImage(absolutePath, { signal });
3408
2726
  return succeed({
3409
- content: [{ type: "image", data: image.data, mimeType: image.mime }],
2727
+ content: [{ type: "image", data: prepared.data, mimeType: prepared.mime }],
3410
2728
  details: {}
3411
2729
  });
3412
2730
  } catch (error) {
@@ -3436,10 +2754,6 @@ async function executeRead(opts) {
3436
2754
  });
3437
2755
  }
3438
2756
  }
3439
- if (p.page !== undefined) {
3440
- const message = "The page parameter applies to PDFs only.";
3441
- return buildToolErrorResult("read", "invalid-params-combo", message, { path: rawParams.path });
3442
- }
3443
2757
  throwIfAborted(signal);
3444
2758
  const rawText = rawBuffer.toString("utf-8");
3445
2759
  const normalized = normalizeToLF(stripBom(rawText).text);
@@ -3562,7 +2876,11 @@ async function executeRead(opts) {
3562
2876
  throwIfAborted(signal);
3563
2877
  let readSeekOutput;
3564
2878
  try {
3565
- readSeekOutput = total === 0 ? await readSeekRead(absolutePath, undefined, undefined, { signal }) : await readSeekRead(absolutePath, startLine, endIdx, { signal });
2879
+ const digested = total === 0 ? await readSeekDigestContent(absolutePath, { signal }) : await readSeekDigestContent(absolutePath, { startLine, endLine: endIdx, signal });
2880
+ if (!digested.text) {
2881
+ throw new Error(`readseek returned no text content for ${absolutePath}`);
2882
+ }
2883
+ readSeekOutput = digested.text;
3566
2884
  } catch (err) {
3567
2885
  const detail = err?.message ? ` — ${err.message}` : "";
3568
2886
  const message = `readseek failed while reading ${rawPath}${detail}`;
@@ -3584,7 +2902,7 @@ async function executeRead(opts) {
3584
2902
  const selected = readSeekLines.map((line) => line.raw);
3585
2903
  const formatted = renderReadSeekLines(readSeekLines);
3586
2904
  const truncation = truncateHead2(formatted, { maxLines: DEFAULT_MAX_LINES3, maxBytes: DEFAULT_MAX_BYTES3 });
3587
- const shouldAppendMap = !!p.map || !!truncation.truncated && !p.offset && !p.limit && !symbolMatch;
2905
+ const shouldAppendMap = !!truncation.truncated && !p.offset && !p.limit && !symbolMatch;
3588
2906
  let appendedMap = false;
3589
2907
  let mapText = null;
3590
2908
  if (shouldAppendMap) {
@@ -3597,6 +2915,16 @@ async function executeRead(opts) {
3597
2915
  }
3598
2916
  } catch {}
3599
2917
  }
2918
+ let currentBuffer;
2919
+ try {
2920
+ currentBuffer = await fsReadFile(absolutePath);
2921
+ } catch (err) {
2922
+ const { code, message } = formatFsError(err, "read-error");
2923
+ return buildToolErrorResult("read", code, message, { path: rawParams.path });
2924
+ }
2925
+ if (!currentBuffer.equals(rawBuffer)) {
2926
+ return buildToolErrorResult("read", "file-changed-during-read", `File changed while it was being read: ${rawPath}. Re-read it to obtain a consistent snapshot.`, { path: rawParams.path });
2927
+ }
3600
2928
  if (symbolWarning) {
3601
2929
  structuredWarnings.push(buildReadSeekWarning("symbol-warning", symbolWarning.trim()));
3602
2930
  }
@@ -3632,7 +2960,7 @@ async function executeRead(opts) {
3632
2960
  endLine: symbolMatch.endLine
3633
2961
  } : null,
3634
2962
  map: {
3635
- requested: !!p.map,
2963
+ requested: false,
3636
2964
  appended: appendedMap,
3637
2965
  text: mapText
3638
2966
  },
@@ -3660,13 +2988,13 @@ function registerReadTool(pi, options = {}) {
3660
2988
  const imageModes = imageMode === "auto" ? ["none", "all", "ocr", "caption", "objects"] : ["all", "ocr", "caption", "objects"];
3661
2989
  const promptMetadata = defineToolPromptMetadata({
3662
2990
  promptUrl: new URL("../prompts/read.md", import.meta.url),
3663
- promptSnippet: imageMode === "off" ? "Read anchored text, symbols, maps, or PDF text" : "Read anchored text, symbols, maps, images, or PDFs",
2991
+ promptSnippet: imageMode === "off" ? "Read anchored text or symbols" : "Read anchored text, symbols, or images",
3664
2992
  registeredName: name
3665
2993
  });
3666
2994
  const tool = registerReadSeekTool(pi, {
3667
2995
  name,
3668
2996
  label: "Read",
3669
- description: imageMode === "off" ? "Read anchored text by range, symbol, or map. Images are disabled; PDFs still return text." : promptMetadata.description,
2997
+ description: imageMode === "off" ? "Read anchored text by range or symbol. Images and documents are disabled; use readSeek_view for documents." : promptMetadata.description,
3670
2998
  promptSnippet: promptMetadata.promptSnippet,
3671
2999
  promptGuidelines: [
3672
3000
  ...promptMetadata.promptGuidelines,
@@ -3676,15 +3004,13 @@ function registerReadTool(pi, options = {}) {
3676
3004
  path: filePathParam(),
3677
3005
  offset: optionalIntOrString("One-based starting line"),
3678
3006
  limit: optionalIntOrString("Maximum lines to return"),
3679
- page: optionalIntOrString("One-based PDF page; defaults to 1"),
3680
3007
  symbol: Type2.Optional(Type2.String({ description: "Symbol name to read" })),
3681
- map: mapParam(),
3682
3008
  bundle: Type2.Optional(Type2.Literal("local", {
3683
3009
  description: "Include related symbols from the same file; requires symbol"
3684
3010
  })),
3685
3011
  ...imageMode === "off" ? {} : {
3686
3012
  image: Type2.Optional(Type2.Union(imageModes.map((mode) => Type2.Literal(mode)), {
3687
- description: `Standalone/PDF-embedded image mode: ${imageModes.join(", ")}. Required for standalone images.${imageMode === "auto" ? " For standalone images, none skips local analysis." : ""}`
3013
+ description: `Standalone image mode: ${imageModes.join(", ")}. Required for standalone images.${imageMode === "auto" ? " For standalone images, none skips local analysis." : ""}`
3688
3014
  }))
3689
3015
  }
3690
3016
  }),
@@ -3773,7 +3099,7 @@ ${textContent}`;
3773
3099
  }
3774
3100
 
3775
3101
  // src/edit.ts
3776
- import { readFile as fsReadFile2, writeFile as fsWriteFile } from "node:fs/promises";
3102
+ import { readFile as fsReadFile2 } from "node:fs/promises";
3777
3103
  import { createPatch } from "diff";
3778
3104
  import { withFileMutationQueue } from "@earendil-works/pi-coding-agent";
3779
3105
  import { Type as Type3 } from "@sinclair/typebox";
@@ -4056,77 +3382,12 @@ async function validateSyntaxRegression(input, options = {}) {
4056
3382
  };
4057
3383
  }
4058
3384
 
4059
- // src/syntax-validate-mode.ts
4060
- var DEFAULT = "warn";
4061
- function resolveSyntaxValidateMode(opts) {
4062
- return opts.syntaxValidate ?? resolveReadSeekSyntaxValidation() ?? DEFAULT;
4063
- }
4064
-
4065
- // src/replace-symbol.ts
4066
- function detectIndent(line) {
4067
- return line.match(/^\s*/)?.[0] ?? "";
4068
- }
4069
- function dedent(text) {
4070
- const lines = text.split(`
4071
- `);
4072
- const nonEmpty = lines.filter((l) => l.trim().length);
4073
- if (!nonEmpty.length)
4074
- return text;
4075
- const minIndent = Math.min(...nonEmpty.map((l) => l.match(/^\s*/)?.[0].length ?? 0));
4076
- return lines.map((l) => l.slice(minIndent)).join(`
4077
- `);
4078
- }
4079
- function reindent(text, indent) {
4080
- return text.split(`
4081
- `).map((l) => l.length ? indent + l : l).join(`
4082
- `);
4083
- }
4084
- async function replaceSymbol(input) {
4085
- const map = await readSeekMapContent(input.filePath, input.content);
4086
- if (!map) {
4087
- return {
4088
- type: "unsupported",
4089
- message: `Unsupported file for replace_symbol: readseek could not map '${input.filePath}'. Use anchored edits for this file type.`
4090
- };
4091
- }
4092
- const lookup = findSymbol(map, input.symbol);
4093
- if (lookup.type === "not-found") {
4094
- return { type: "not-found", message: formatNotFound(input.symbol, map) };
4095
- }
4096
- if (lookup.type === "ambiguous") {
4097
- return { type: "ambiguous", message: formatAmbiguous(input.symbol, lookup.candidates) };
4098
- }
4099
- const sym = lookup.symbol;
4100
- const lines = input.content.split(`
4101
- `);
4102
- const sigLine = lines[sym.startLine - 1] ?? "";
4103
- const indent = detectIndent(sigLine);
4104
- const reindented = reindent(dedent(normalizeToLF(input.newBody)), indent);
4105
- const warnings = [];
4106
- const leaf = input.symbol.replace(/@\d+$/, "").split(".").pop() ?? "";
4107
- const declNameRe = /\b(?:(?:export\s+(?:default\s+)?|async\s+|public\s+|private\s+|protected\s+|static\s+)*)(?:function\*?|class|const|let|var|fn|def|method|type|interface|enum)\s+([A-Za-z_$][\w$]*)/s;
4108
- const firstDeclName = reindented.match(declNameRe)?.[1] ?? reindented.match(/^\s*(?:(?:async\s+)?[\w$<>,?\s]+\s+)?([A-Za-z_$][\w$]*)\s*\(/)?.[1];
4109
- if (leaf && firstDeclName && firstDeclName !== leaf) {
4110
- warnings.push(`name-mismatch: expected ${leaf}, got ${firstDeclName}`);
4111
- }
4112
- const before = lines.slice(0, sym.startLine - 1).join(`
4113
- `);
4114
- const after = lines.slice(sym.endLine).join(`
4115
- `);
4116
- const beforePart = before.length ? before + `
4117
- ` : "";
4118
- const afterPart = after.length ? `
4119
- ` + after : "";
4120
- const newContent = beforePart + reindented + afterPart;
4121
- return {
4122
- type: "ok",
4123
- content: newContent,
4124
- replacement: reindented,
4125
- warnings,
4126
- range: { start: sym.startLine, end: sym.endLine }
4127
- };
4128
- }
4129
-
3385
+ // src/syntax-validate-mode.ts
3386
+ var DEFAULT = "warn";
3387
+ function resolveSyntaxValidateMode(opts) {
3388
+ return opts.syntaxValidate ?? resolveReadSeekSyntaxValidation() ?? DEFAULT;
3389
+ }
3390
+
4130
3391
  // src/pending-diff-preview.ts
4131
3392
  import { existsSync as existsSync2, readFileSync as readFileSync4, realpathSync, statSync as statSync3 } from "node:fs";
4132
3393
  import { dirname, isAbsolute as isAbsolute2, relative as relative2, resolve, sep } from "node:path";
@@ -4188,57 +3449,6 @@ function buildData(filePath, previousContent, nextContent, existed, headerLabel)
4188
3449
  function getEdits(input) {
4189
3450
  return Array.isArray(input.edits) ? input.edits : [];
4190
3451
  }
4191
- function isReplaceEdit(edit) {
4192
- if (!edit || typeof edit !== "object" || !("replace" in edit))
4193
- return false;
4194
- const replace = edit.replace;
4195
- return typeof replace?.old_text === "string" && typeof replace?.new_text === "string";
4196
- }
4197
- function isAnchorEdit(edit) {
4198
- return !!edit && typeof edit === "object" && (("set_line" in edit) || ("replace_lines" in edit) || ("insert_after" in edit));
4199
- }
4200
- function applyAnchoredPreview(content, edits) {
4201
- try {
4202
- return { type: "ok", content: applyHashlineEdits(content, edits).content };
4203
- } catch (err) {
4204
- return { type: "skip", reason: `anchor projection failed: ${err?.message ?? String(err)}` };
4205
- }
4206
- }
4207
- function isReplaceSymbolEdit(edit) {
4208
- if (!edit || typeof edit !== "object" || !("replace_symbol" in edit))
4209
- return false;
4210
- const replaceSymbol2 = edit.replace_symbol;
4211
- return typeof replaceSymbol2?.symbol === "string" && typeof replaceSymbol2?.new_body === "string";
4212
- }
4213
- async function applyReplaceSymbolPreview(filePath, content, edit) {
4214
- try {
4215
- if (!edit.replace_symbol.new_body.trim())
4216
- return { type: "skip", reason: "replace_symbol new_body is empty" };
4217
- const probe = await replaceSymbol({
4218
- filePath,
4219
- content,
4220
- symbol: edit.replace_symbol.symbol,
4221
- newBody: edit.replace_symbol.new_body
4222
- });
4223
- if (probe.type !== "ok")
4224
- return { type: "skip", reason: `symbol projection failed: ${probe.message}` };
4225
- return { type: "ok", content: probe.content };
4226
- } catch (err) {
4227
- return { type: "skip", reason: `symbol projection failed: ${err?.message ?? String(err)}` };
4228
- }
4229
- }
4230
- function applyReplacePreview(content, edit) {
4231
- const { old_text, new_text } = edit.replace;
4232
- if (!old_text.length)
4233
- return { type: "skip", reason: "replace old_text is empty" };
4234
- const replacement = replaceText(content, old_text, new_text, {
4235
- all: edit.replace.all ?? false,
4236
- fuzzy: edit.replace.fuzzy ?? false
4237
- });
4238
- if (!replacement.count)
4239
- return { type: "skip", reason: "replace old_text was not found" };
4240
- return { type: "ok", content: replacement.content };
4241
- }
4242
3452
  function buildPendingWritePreviewData(input, cwd) {
4243
3453
  if (typeof input.content !== "string")
4244
3454
  return skip("missing content");
@@ -4262,50 +3472,16 @@ async function buildPendingEditPreviewData(input, cwd) {
4262
3472
  const previous = readUtf8File(resolved.path);
4263
3473
  if (previous.type === "skip")
4264
3474
  return previous;
4265
- let next = normalizeToLF(previous.content);
4266
- const anchorBatch = [];
4267
- for (const edit of edits) {
4268
- if (isReplaceEdit(edit)) {
4269
- if (anchorBatch.length > 0) {
4270
- const anchored = applyAnchoredPreview(next, anchorBatch);
4271
- if (anchored.type === "skip")
4272
- return anchored;
4273
- next = anchored.content;
4274
- anchorBatch.length = 0;
4275
- }
4276
- const projected = applyReplacePreview(next, edit);
4277
- if (projected.type === "skip")
4278
- return projected;
4279
- next = projected.content;
4280
- continue;
4281
- }
4282
- if (isAnchorEdit(edit)) {
4283
- anchorBatch.push(edit);
4284
- continue;
4285
- }
4286
- if (isReplaceSymbolEdit(edit)) {
4287
- if (anchorBatch.length > 0) {
4288
- const anchored = applyAnchoredPreview(next, anchorBatch);
4289
- if (anchored.type === "skip")
4290
- return anchored;
4291
- next = anchored.content;
4292
- anchorBatch.length = 0;
4293
- }
4294
- const projected = await applyReplaceSymbolPreview(resolved.path, next, edit);
4295
- if (projected.type === "skip")
4296
- return projected;
4297
- next = projected.content;
4298
- continue;
4299
- }
4300
- return skip("unsupported edit variant");
4301
- }
4302
- if (anchorBatch.length > 0) {
4303
- const anchored = applyAnchoredPreview(next, anchorBatch);
4304
- if (anchored.type === "skip")
4305
- return anchored;
4306
- next = anchored.content;
3475
+ try {
3476
+ const plan = await readSeekEdit(resolved.path, edits, {
3477
+ language: typeof input.language === "string" ? input.language : undefined
3478
+ });
3479
+ if (typeof plan.content !== "string")
3480
+ return skip("edit plan did not include content");
3481
+ return buildData(resolved.path, previous.content, plan.content, true, "pending edit");
3482
+ } catch (err) {
3483
+ return skip(`edit projection failed: ${err?.message ?? String(err)}`);
4307
3484
  }
4308
- return buildData(resolved.path, previous.content, next, true, "pending edit");
4309
3485
  }
4310
3486
  function buildEditPreviewKey(input) {
4311
3487
  if (typeof input.path !== "string")
@@ -4313,7 +3489,7 @@ function buildEditPreviewKey(input) {
4313
3489
  const edits = getEdits(input);
4314
3490
  if (edits.length === 0)
4315
3491
  return;
4316
- return JSON.stringify({ path: input.path, edits });
3492
+ return JSON.stringify({ path: input.path, edits, language: input.language });
4317
3493
  }
4318
3494
  function buildWritePreviewKey(input) {
4319
3495
  if (typeof input.path !== "string" || typeof input.content !== "string")
@@ -4796,26 +3972,25 @@ var hashlineEditItemSchema = Type3.Object({
4796
3972
  set_line: Type3.Optional(Type3.Object({
4797
3973
  anchor: Type3.String({ description: "Fresh LINE:HASH anchor for the line to replace" }),
4798
3974
  new_text: Type3.String({ description: "Replacement text; use an empty string to delete the line" })
4799
- }, { description: 'Replace one line: {"set_line":{"anchor":"LINE:HASH","new_text":"..."}}' })),
3975
+ }, { additionalProperties: false, description: 'Replace one line: {"set_line":{"anchor":"LINE:HASH","new_text":"..."}}' })),
4800
3976
  replace_lines: Type3.Optional(Type3.Object({
4801
3977
  start_anchor: Type3.String({ description: "Fresh LINE:HASH anchor for the first line in the range" }),
4802
3978
  end_anchor: Type3.String({ description: "Fresh LINE:HASH anchor for the last line in the range" }),
4803
3979
  new_text: Type3.String({ description: "Replacement text; use an empty string to delete the range" })
4804
- }, { description: 'Replace a range: {"replace_lines":{"start_anchor":"LINE:HASH","end_anchor":"LINE:HASH","new_text":"..."}}' })),
3980
+ }, { additionalProperties: false, description: 'Replace a range: {"replace_lines":{"start_anchor":"LINE:HASH","end_anchor":"LINE:HASH","new_text":"..."}}' })),
4805
3981
  insert_after: Type3.Optional(Type3.Object({
4806
3982
  anchor: Type3.String({ description: "Fresh LINE:HASH anchor for the line after which to insert" }),
4807
3983
  new_text: Type3.String({ description: "Text to insert after the anchored line" })
4808
- }, { description: 'Insert text: {"insert_after":{"anchor":"LINE:HASH","new_text":"..."}}' })),
3984
+ }, { additionalProperties: false, description: 'Insert text: {"insert_after":{"anchor":"LINE:HASH","new_text":"..."}}' })),
4809
3985
  replace: Type3.Optional(Type3.Object({
4810
3986
  old_text: Type3.String({ description: "Exact text to find" }),
4811
3987
  new_text: Type3.String({ description: "Replacement text" }),
4812
- all: Type3.Optional(Type3.Boolean({ description: "Replace every exact match" })),
4813
- fuzzy: Type3.Optional(Type3.Boolean({ description: "Allow unverified fuzzy matching when exact text is absent" }))
4814
- }, { description: 'Replace text: {"replace":{"old_text":"...","new_text":"..."}}' })),
3988
+ all: Type3.Optional(Type3.Boolean({ description: "Replace every exact match" }))
3989
+ }, { additionalProperties: false, description: 'Replace text: {"replace":{"old_text":"...","new_text":"..."}}' })),
4815
3990
  replace_symbol: Type3.Optional(Type3.Object({
4816
3991
  symbol: Type3.String({ description: "Mapped symbol name" }),
4817
3992
  new_body: Type3.String({ description: "Complete replacement body for the symbol" })
4818
- }, { description: 'Replace a symbol: {"replace_symbol":{"symbol":"name","new_body":"..."}}' }))
3993
+ }, { additionalProperties: false, description: 'Replace a symbol: {"replace_symbol":{"symbol":"name","new_body":"..."}}' }))
4819
3994
  }, {
4820
3995
  additionalProperties: false,
4821
3996
  minProperties: 1,
@@ -4824,13 +3999,15 @@ var hashlineEditItemSchema = Type3.Object({
4824
3999
  });
4825
4000
  var hashlineEditSchema = Type3.Object({
4826
4001
  path: filePathParam(),
4827
- edits: Type3.Optional(Type3.Array(hashlineEditItemSchema, {
4002
+ language: Type3.Optional(Type3.String({ description: "Language override for symbol replacement" })),
4003
+ edits: Type3.Array(hashlineEditItemSchema, {
4004
+ minItems: 1,
4828
4005
  description: "Use set_line, replace_lines, insert_after, replace_symbol, or replace"
4829
- })),
4006
+ }),
4830
4007
  postEditVerify: Type3.Optional(Type3.Boolean({
4831
4008
  description: "Read back and verify persisted content"
4832
4009
  }))
4833
- }, { additionalProperties: true });
4010
+ }, { additionalProperties: false });
4834
4011
  function buildEditError(path2, code, message, hint, errorDetails) {
4835
4012
  return {
4836
4013
  content: [{ type: "text", text: message }],
@@ -4854,9 +4031,7 @@ function mapEditFileError(err, filePath, _displayPath, _phase) {
4854
4031
  }
4855
4032
  async function executeEdit(opts) {
4856
4033
  const { params, signal, cwd, wasReadInSession, onFileMutated, syntaxValidate } = opts;
4857
- await ensureHashInit();
4858
4034
  const parsed = params;
4859
- const input = params;
4860
4035
  const rawPath = parsed.path;
4861
4036
  const path2 = rawPath.replace(/^@/, "");
4862
4037
  const absolutePath = resolveToCwd(path2, cwd);
@@ -4865,345 +4040,105 @@ async function executeEdit(opts) {
4865
4040
  return await withFileMutationQueue(absolutePath, async () => {
4866
4041
  throwIfAborted(signal);
4867
4042
  if (wasReadInSession && !wasReadInSession(absolutePath)) {
4868
- const message = [
4869
- `You must get fresh anchors for ${absolutePath} before editing it.`,
4870
- `Call readSeek_read(${JSON.stringify(rawPath)}) first, or use readSeek_grep, readSeek_search, or readSeek_write to produce fresh anchors for this file.`,
4871
- "readSeek_edit requires fresh LINE:HASH anchors from readSeek_read, readSeek_grep, readSeek_search, or readSeek_write so the hashes match the current file contents."
4872
- ].join(" ");
4873
- return buildEditError(absolutePath, "file-not-read", message, `Call readSeek_read(${JSON.stringify(rawPath)}) first, or use readSeek_grep, readSeek_search, or readSeek_write to produce fresh anchors for this file.`);
4874
- }
4875
- const hasTopLevelReplaceInput = typeof input.oldText === "string" || typeof input.newText === "string" || typeof input.old_text === "string" || typeof input.new_text === "string";
4876
- if (hasTopLevelReplaceInput) {
4877
- return buildEditError(absolutePath, "invalid-edit-variant", "Top-level oldText/newText and old_text/new_text are no longer supported. Use edits[0].replace instead.", "Use edits: [{ replace: { old_text, new_text } }].");
4043
+ const hint = `Call readSeek_read(${JSON.stringify(rawPath)}) first, or use readSeek_grep, readSeek_search, or readSeek_write to produce fresh anchors for this file.`;
4044
+ return buildEditError(absolutePath, "file-not-read", `You must get fresh anchors for ${absolutePath} before editing it. ${hint}`, hint);
4878
4045
  }
4879
4046
  const edits = parsed.edits ?? [];
4880
4047
  if (!edits.length) {
4881
4048
  return buildEditError(absolutePath, "invalid-edit-variant", "No edits provided.");
4882
4049
  }
4883
- for (let i = 0;i < edits.length; i++) {
4884
- throwIfAborted(signal);
4885
- const edit = edits[i];
4886
- if (!edit || typeof edit !== "object" || Array.isArray(edit)) {
4887
- const message = `edits[${i}] must be an object containing exactly one edit variant.`;
4888
- return buildEditError(absolutePath, "invalid-edit-variant", message);
4889
- }
4890
- const e = edit;
4891
- if ((("old_text" in e) || ("new_text" in e)) && !("replace" in e)) {
4892
- const message = `edits[${i}] has top-level 'old_text'/'new_text'. Use {replace: {old_text, new_text}} or {set_line}, {replace_lines}, {insert_after}.`;
4893
- return buildEditError(absolutePath, "invalid-edit-variant", message);
4894
- }
4895
- if ("diff" in e) {
4896
- const message = `edits[${i}] contains 'diff' from patch mode. Hashline edit expects one of: {set_line}, {replace_lines}, {insert_after}, {replace}.`;
4897
- return buildEditError(absolutePath, "invalid-edit-variant", message);
4898
- }
4899
- const variantCount = Number("set_line" in e) + Number("replace_lines" in e) + Number("insert_after" in e) + Number("replace" in e) + Number("replace_symbol" in e);
4900
- if (variantCount !== 1 || Object.keys(e).length !== 1) {
4901
- const message = `edits[${i}] must contain exactly one of: 'set_line', 'replace_lines', 'insert_after', 'replace', 'replace_symbol'. Got: [${Object.keys(e).join(", ")}].`;
4902
- return buildEditError(absolutePath, "invalid-edit-variant", message);
4903
- }
4904
- }
4905
- const anchorEdits = edits.filter((e) => ("set_line" in e) || ("replace_lines" in e) || ("insert_after" in e));
4906
- const replaceEdits = edits.filter((e) => ("replace" in e));
4907
- const replaceSymbolEdits = edits.filter((e) => ("replace_symbol" in e));
4908
- for (const rs of replaceSymbolEdits) {
4909
- if (!rs.replace_symbol.new_body.trim()) {
4910
- return buildEditError(absolutePath, "invalid-edit-variant", "replace_symbol.new_body must not be empty or whitespace-only.");
4911
- }
4912
- }
4913
- let rawBuffer;
4914
- try {
4915
- rawBuffer = await fsReadFile2(absolutePath);
4916
- } catch (err) {
4917
- return mapEditFileError(err, absolutePath, path2, "read");
4918
- }
4919
- if (looksLikeBinary(rawBuffer)) {
4920
- const message = `Cannot edit binary file: ${path2}`;
4921
- return buildEditError(absolutePath, "binary-file", message);
4922
- }
4923
- throwIfAborted(signal);
4924
- const raw = rawBuffer.toString("utf-8");
4925
- const { bom, text: content } = stripBom(raw);
4926
- const originalEnding = detectLineEnding(content);
4927
- const originalNormalized = normalizeToLF(content);
4928
- const origLines = originalNormalized.split(`
4929
- `);
4930
- const anchorSnapshots = new Map;
4931
- for (const edit of anchorEdits) {
4932
- const refs = [];
4933
- if ("set_line" in edit)
4934
- refs.push(edit.set_line.anchor);
4935
- else if ("replace_lines" in edit) {
4936
- refs.push(edit.replace_lines.start_anchor, edit.replace_lines.end_anchor);
4937
- } else if ("insert_after" in edit)
4938
- refs.push(edit.insert_after.anchor);
4939
- for (const ref of refs) {
4940
- try {
4941
- const parsed2 = parseLineRef(ref);
4942
- if (parsed2.line >= 1 && parsed2.line <= origLines.length) {
4943
- const lineContent = origLines[parsed2.line - 1];
4944
- const hash = computeLineHash(lineContent);
4945
- anchorSnapshots.set(ref, `${parsed2.line}:${hash}|${escapeControlCharsForDisplay(lineContent)}`);
4946
- }
4947
- } catch {}
4050
+ const variants = new Set(["set_line", "replace_lines", "insert_after", "replace", "replace_symbol"]);
4051
+ for (let index = 0;index < edits.length; index++) {
4052
+ const edit = edits[index];
4053
+ const keys = edit && typeof edit === "object" && !Array.isArray(edit) ? Object.keys(edit) : [];
4054
+ if (keys.length !== 1 || !variants.has(keys[0])) {
4055
+ return buildEditError(absolutePath, "invalid-edit-variant", `edits[${index}] must contain exactly one of: 'set_line', 'replace_lines', 'insert_after', 'replace', 'replace_symbol'. Got: [${keys.join(", ")}].`);
4948
4056
  }
4949
4057
  }
4950
- let preAnchorContent = originalNormalized;
4951
- const replaceSymbolRanges = [];
4952
- const rsProbeResults = [];
4058
+ let plan;
4953
4059
  try {
4954
- for (const rs of replaceSymbolEdits) {
4955
- throwIfAborted(signal);
4956
- const probe = await replaceSymbol({
4957
- filePath: absolutePath,
4958
- content: originalNormalized,
4959
- symbol: rs.replace_symbol.symbol,
4960
- newBody: rs.replace_symbol.new_body
4961
- });
4962
- if (probe.type !== "ok") {
4963
- return buildEditError(absolutePath, "invalid-edit-variant", probe.message);
4964
- }
4965
- rsProbeResults.push(probe);
4966
- replaceSymbolRanges.push(probe.range);
4967
- }
4060
+ plan = await readSeekEdit(absolutePath, edits, { signal, language: parsed.language });
4968
4061
  } catch (err) {
4969
4062
  throwIfAborted(signal);
4970
4063
  const failure = classifyReadSeekFailure(err);
4971
- return buildEditError(absolutePath, failure.code, failure.message, failure.hint);
4972
- }
4973
- const sortedReplaceSymbolRanges = [...replaceSymbolRanges].sort((a, b) => a.start - b.start || a.end - b.end);
4974
- for (let i = 1;i < sortedReplaceSymbolRanges.length; i++) {
4975
- const prev = sortedReplaceSymbolRanges[i - 1];
4976
- const current = sortedReplaceSymbolRanges[i];
4977
- if (current.start <= prev.end) {
4978
- const message = `replace_symbol ranges overlap or duplicate (lines ${prev.start}-${prev.end} and ${current.start}-${current.end}).`;
4979
- return buildEditError(absolutePath, "invalid-edit-variant", message);
4980
- }
4064
+ const code = failure.message.includes("stale anchor") ? "hash-mismatch" : failure.code;
4065
+ return buildEditError(absolutePath, code, failure.message, failure.hint);
4981
4066
  }
4982
- if (replaceSymbolRanges.length > 0) {
4983
- for (const edit of anchorEdits) {
4984
- if ("replace_lines" in edit) {
4985
- let startLine;
4986
- let endLine;
4987
- try {
4988
- startLine = parseLineRef(edit.replace_lines.start_anchor).line;
4989
- endLine = parseLineRef(edit.replace_lines.end_anchor).line;
4990
- } catch {}
4991
- if (startLine !== undefined && endLine !== undefined) {
4992
- const lo = Math.min(startLine, endLine);
4993
- const hi = Math.max(startLine, endLine);
4994
- for (const range of replaceSymbolRanges) {
4995
- if (lo <= range.end && hi >= range.start) {
4996
- const message = `replace_lines range ${lo}-${hi} overlaps a replace_symbol range (lines ${range.start}-${range.end}).`;
4997
- return buildEditError(absolutePath, "invalid-edit-variant", message);
4998
- }
4999
- }
5000
- }
5001
- }
5002
- const refs = [];
5003
- if ("set_line" in edit)
5004
- refs.push(edit.set_line.anchor);
5005
- else if ("replace_lines" in edit) {
5006
- refs.push(edit.replace_lines.start_anchor, edit.replace_lines.end_anchor);
5007
- } else if ("insert_after" in edit)
5008
- refs.push(edit.insert_after.anchor);
5009
- for (const ref of refs) {
5010
- let parsedLine;
5011
- try {
5012
- parsedLine = parseLineRef(ref).line;
5013
- } catch {
5014
- continue;
5015
- }
5016
- for (const range of replaceSymbolRanges) {
5017
- if (parsedLine >= range.start && parsedLine <= range.end) {
5018
- const message = `Anchor at line ${parsedLine} falls inside a replace_symbol range (lines ${range.start}-${range.end}).`;
5019
- return buildEditError(absolutePath, "invalid-edit-variant", message);
5020
- }
5021
- }
5022
- }
5023
- }
5024
- }
5025
- const anchorIntervals = [];
5026
- for (const edit of anchorEdits) {
5027
- try {
5028
- if ("set_line" in edit) {
5029
- const line = parseLineRef(edit.set_line.anchor).line;
5030
- anchorIntervals.push({ lo: line, hi: line });
5031
- } else if ("replace_lines" in edit) {
5032
- const start = parseLineRef(edit.replace_lines.start_anchor).line;
5033
- const end = parseLineRef(edit.replace_lines.end_anchor).line;
5034
- anchorIntervals.push({ lo: Math.min(start, end), hi: Math.max(start, end) });
5035
- }
5036
- } catch {}
5037
- }
5038
- const seenAnchorIntervals = new Set;
5039
- const uniqueAnchorIntervals = [];
5040
- for (const interval of anchorIntervals) {
5041
- const key = `${interval.lo}:${interval.hi}`;
5042
- if (seenAnchorIntervals.has(key))
5043
- continue;
5044
- seenAnchorIntervals.add(key);
5045
- uniqueAnchorIntervals.push(interval);
5046
- }
5047
- uniqueAnchorIntervals.sort((a, b) => a.lo - b.lo || a.hi - b.hi);
5048
- for (let i = 1;i < uniqueAnchorIntervals.length; i++) {
5049
- const prev = uniqueAnchorIntervals[i - 1];
5050
- const current = uniqueAnchorIntervals[i];
5051
- if (current.lo <= prev.hi) {
5052
- const message = `Anchored edits overlap (lines ${prev.lo}-${prev.hi} and ${current.lo}-${current.hi}). Split into separate, non-overlapping edits or re-read for fresh anchors.`;
5053
- return buildEditError(absolutePath, "invalid-edit-variant", message);
5054
- }
5055
- }
5056
- const replaceSymbolWarnings = [];
5057
- if (rsProbeResults.length > 0) {
5058
- const lines = originalNormalized.split(`
5059
- `);
5060
- for (const probe of rsProbeResults) {
5061
- replaceSymbolWarnings.push(...probe.warnings);
5062
- }
5063
- for (const probe of [...rsProbeResults].sort((a, b) => b.range.start - a.range.start)) {
5064
- lines.splice(probe.range.start - 1, probe.range.end - probe.range.start + 1, ...probe.replacement.split(`
5065
- `));
5066
- }
5067
- preAnchorContent = lines.join(`
5068
- `);
5069
- }
5070
- let result = preAnchorContent;
5071
- let anchorResult;
5072
- try {
5073
- anchorResult = applyHashlineEdits(result, anchorEdits, signal);
5074
- } catch (err) {
5075
- if (err instanceof HashlineMismatchError) {
5076
- return buildEditError(absolutePath, "hash-mismatch", err.message, undefined, {
5077
- updatedAnchors: err.updatedAnchors
5078
- });
5079
- }
5080
- throw err;
4067
+ if (typeof plan.before_content !== "string" || typeof plan.content !== "string") {
4068
+ return buildEditError(absolutePath, "invalid-output", "readseek edit plan did not include before_content and content");
5081
4069
  }
5082
- result = anchorResult.content;
5083
- const replaceWarnings = [];
5084
- for (const r of replaceEdits) {
5085
- throwIfAborted(signal);
5086
- if (!r.replace.old_text.length) {
5087
- const message = "replace.old_text must not be empty.";
5088
- return buildEditError(absolutePath, "invalid-edit-variant", message);
5089
- }
5090
- const rep = replaceText(result, r.replace.old_text, r.replace.new_text, {
5091
- all: r.replace.all ?? false,
5092
- fuzzy: r.replace.fuzzy ?? false
5093
- });
5094
- if (!rep.count) {
5095
- const message = `Could not find exact text to replace in ${path2}.`;
5096
- const hint = "Re-read the file and prefer set_line/replace_lines/insert_after for hash-verified edits. " + "The replace variant is exact-only by default because fuzzy fallback is unverified.";
5097
- return buildEditError(absolutePath, "text-not-found", message, hint);
5098
- }
5099
- if (rep.usedFuzzyMatch) {
5100
- replaceWarnings.push("replace used fuzzy matching because exact old_text was not found; re-read the file and prefer set_line/replace_lines/insert_after for hash-verified edits.");
5101
- }
5102
- result = rep.content;
5103
- }
5104
- if (originalNormalized === result) {
5105
- let diagnostic = `No changes made to ${path2}. The edits produced identical content.`;
5106
- if (anchorResult.noopEdits?.length) {
5107
- diagnostic += `
5108
- ` + anchorResult.noopEdits.map((e) => `Edit ${e.editIndex}: replacement for ${e.loc} is identical to current content:
5109
- ${e.loc}| ${escapeControlCharsForDisplay(e.currentContent)}`).join(`
5110
- `);
5111
- diagnostic += `
5112
- Re-read the file to see the current state.`;
5113
- } else {
5114
- const targetLines = [...new Set(anchorSnapshots.values())];
5115
- if (targetLines.length > 0) {
5116
- const preview = targetLines.slice(0, 5).join(`
5117
- `);
5118
- diagnostic += `
5119
- The file currently contains:
5120
- ${preview}
5121
- Your edits were normalized back to the original content. Ensure your replacement changes actual code, not just formatting.`;
5122
- }
5123
- }
5124
- return buildEditError(absolutePath, "no-op", diagnostic);
4070
+ const original = plan.before_content;
4071
+ const result = plan.content;
4072
+ const originalForSyntax = normalizeToLF(stripBom(original).text);
4073
+ const resultForSyntax = normalizeToLF(stripBom(result).text);
4074
+ if (!plan.changed) {
4075
+ return buildEditError(absolutePath, "no-op", `No changes made to ${path2}. The edits produced identical content.`);
5125
4076
  }
5126
- throwIfAborted(signal);
5127
4077
  const syntaxMode = resolveSyntaxValidateMode({ syntaxValidate });
5128
4078
  let syntaxWarning;
5129
4079
  if (syntaxMode !== "off") {
5130
4080
  const regression = await validateSyntaxRegression({
5131
4081
  filePath: absolutePath,
5132
- before: originalNormalized,
5133
- after: result
4082
+ before: originalForSyntax,
4083
+ after: resultForSyntax
5134
4084
  }, { signal });
5135
4085
  if (regression) {
5136
- const lines = regression.errorLines.join(", ");
5137
- const message = `syntax-regression: lines ${lines}`;
4086
+ const message = `syntax-regression: lines ${regression.errorLines.join(", ")}`;
5138
4087
  if (syntaxMode === "block") {
5139
4088
  return buildEditError(absolutePath, "syntax-regression", message);
5140
4089
  }
5141
4090
  syntaxWarning = message;
5142
4091
  }
5143
4092
  }
5144
- const writeContent = bom + restoreLineEndings(result, originalEnding);
4093
+ throwIfAborted(signal);
4094
+ let applied;
5145
4095
  try {
5146
- await fsWriteFile(absolutePath, writeContent, "utf-8");
4096
+ applied = await readSeekEdit(absolutePath, edits, {
4097
+ apply: true,
4098
+ planHash: plan.plan_hash,
4099
+ language: parsed.language,
4100
+ cancelable: false
4101
+ });
5147
4102
  } catch (err) {
5148
- return mapEditFileError(err, absolutePath, path2, "write");
4103
+ throwIfAborted(signal);
4104
+ const failure = classifyReadSeekFailure(err);
4105
+ return buildEditError(absolutePath, failure.code, failure.message, failure.hint);
4106
+ }
4107
+ if (!applied.applied) {
4108
+ return buildEditError(absolutePath, "invalid-output", "readseek did not confirm that the edit was applied");
5149
4109
  }
5150
4110
  onFileMutated?.(absolutePath);
5151
- if (input.postEditVerify === true) {
4111
+ if (parsed.postEditVerify === true) {
5152
4112
  let verifiedContent;
5153
4113
  try {
5154
- const verified = await fsReadFile2(absolutePath, "utf-8");
5155
- verifiedContent = verified;
4114
+ verifiedContent = await fsReadFile2(absolutePath, "utf-8");
5156
4115
  } catch (err) {
5157
- const message = `Edit write completed but post-edit verification failed: could not read ${path2} after writing.`;
5158
- return buildEditError(absolutePath, "post-edit-verification-read-failed", message, undefined, {
5159
- fsCode: err?.code,
5160
- fsMessage: err?.message
5161
- });
4116
+ return buildEditError(absolutePath, "post-edit-verification-read-failed", `Edit write completed but post-edit verification failed: could not read ${path2} after writing.`, undefined, { fsCode: err?.code, fsMessage: err?.message });
5162
4117
  }
5163
- if (verifiedContent !== writeContent) {
5164
- const message = `Edit write completed but post-edit verification did not confirm the intended content for ${path2}. Re-read the file before making follow-up edits.`;
5165
- return buildEditError(absolutePath, "post-edit-verification-mismatch", message, undefined, {
5166
- expectedLength: writeContent.length,
5167
- actualLength: verifiedContent.length
5168
- });
4118
+ if (verifiedContent !== plan.content) {
4119
+ return buildEditError(absolutePath, "post-edit-verification-mismatch", `Edit write completed but post-edit verification did not confirm the intended content for ${path2}. Re-read the file before making follow-up edits.`, undefined, { expectedLength: plan.content.length, actualLength: verifiedContent.length });
5169
4120
  }
5170
4121
  }
5171
- const diffResult = generateCompactOrFullDiff(originalNormalized, result);
5172
- const patch = createPatch(path2, originalNormalized, result);
5173
- const blockRanges = rsProbeResults.map((probe) => ({
5174
- kind: "remove",
5175
- startLine: probe.range.start,
5176
- endLine: probe.range.end
5177
- }));
4122
+ const diffResult = generateCompactOrFullDiff(original, result);
4123
+ const patch = createPatch(path2, original, result);
5178
4124
  const diffData = buildDiffData({
5179
4125
  path: absolutePath,
5180
- oldContent: originalNormalized,
4126
+ oldContent: original,
5181
4127
  newContent: result,
5182
- diff: diffResult.diff,
5183
- ...blockRanges.length ? { blockRanges } : {}
4128
+ diff: diffResult.diff
5184
4129
  });
5185
- const warnings = [];
5186
- if (anchorResult.warnings?.length)
5187
- warnings.push(...anchorResult.warnings);
5188
- if (replaceWarnings.length)
5189
- warnings.push(...replaceWarnings);
5190
- if (replaceSymbolWarnings.length)
5191
- warnings.push(...replaceSymbolWarnings);
5192
- if (syntaxWarning)
5193
- warnings.push(syntaxWarning);
5194
- const internalClassification = classifyEdit(originalNormalized, result);
5195
- const semanticSummary = {
5196
- classification: internalClassification.classification
5197
- };
4130
+ const warnings = syntaxWarning ? [syntaxWarning] : [];
4131
+ const classification = classifyEdit(original, result);
4132
+ const semanticSummary = { classification: classification.classification };
5198
4133
  const builtOutput = buildEditOutput({
5199
4134
  path: absolutePath,
5200
4135
  displayPath: path2,
5201
4136
  diff: diffResult.diff,
5202
4137
  patch,
5203
4138
  diffData,
5204
- firstChangedLine: anchorResult.firstChangedLine ?? diffResult.firstChangedLine,
4139
+ firstChangedLine: diffResult.firstChangedLine,
5205
4140
  warnings,
5206
- noopEdits: anchorResult.noopEdits ?? [],
4141
+ noopEdits: [],
5207
4142
  edits,
5208
4143
  semanticSummary
5209
4144
  });
@@ -5213,7 +4148,7 @@ Your edits were normalized back to the original content. Ensure your replacement
5213
4148
  diff: diffResult.diff,
5214
4149
  patch: builtOutput.patch,
5215
4150
  diffData,
5216
- firstChangedLine: anchorResult.firstChangedLine ?? diffResult.firstChangedLine,
4151
+ firstChangedLine: diffResult.firstChangedLine,
5217
4152
  readSeekValue: builtOutput.readSeekValue
5218
4153
  }
5219
4154
  };
@@ -6627,120 +5562,12 @@ ${linkedPathLine(theme, file.file, display, cwd, ` (${file.edits.length} edits)`
6627
5562
  });
6628
5563
  }
6629
5564
 
6630
- // src/hover.ts
6631
- import { readFile } from "node:fs/promises";
6632
- import { Type as Type9 } from "@sinclair/typebox";
6633
- import { Text as Text7 } from "@earendil-works/pi-tui";
6634
- var HOVER_PROMPT_METADATA = defineToolPromptMetadata({
6635
- promptUrl: new URL("../prompts/hover.md", import.meta.url),
6636
- promptSnippet: "Inspect the symbol at a source location"
6637
- });
6638
- var hoverSchema = Type9.Object({
6639
- path: filePathParam(),
6640
- line: Type9.Integer({ minimum: 1, description: "One-based cursor line" }),
6641
- column: Type9.Optional(Type9.Integer({ minimum: 1, description: "One-based cursor byte column for disambiguation" }))
6642
- });
6643
- async function executeHover(opts) {
6644
- const { params, signal, cwd } = opts;
6645
- const p = params;
6646
- if (!Number.isSafeInteger(p.line) || p.line < 1) {
6647
- return buildToolErrorResult("hover", "invalid-parameter", "hover parameter 'line' must be a positive integer");
6648
- }
6649
- if (p.column !== undefined && (!Number.isSafeInteger(p.column) || p.column < 1)) {
6650
- return buildToolErrorResult("hover", "invalid-parameter", "hover parameter 'column' must be a positive integer");
6651
- }
6652
- const filePath = resolveToCwd(p.path, cwd);
6653
- let content;
6654
- try {
6655
- content = await readFile(filePath, "utf-8");
6656
- } catch (err) {
6657
- const { code, message } = formatFsError(err, "hover-error");
6658
- return buildToolErrorResult("hover", code, message, { path: p.path });
6659
- }
6660
- try {
6661
- const output = await readSeekIdentify(filePath, content, {
6662
- line: p.line,
6663
- column: p.column,
6664
- signal
6665
- });
6666
- const lines = [];
6667
- if (output.identifier) {
6668
- lines.push(`identifier: ${output.identifier.text}`);
6669
- }
6670
- if (output.symbol) {
6671
- lines.push(`symbol: ${output.symbol.name}`);
6672
- lines.push(`kind: ${output.symbol.kind}`);
6673
- lines.push(`qualified: ${output.symbol.qualified_name}`);
6674
- }
6675
- lines.push(`file: ${output.file}`);
6676
- lines.push(`language: ${output.language}`);
6677
- lines.push(`location: ${output.line}:${output.column}`);
6678
- return {
6679
- content: [{ type: "text", text: lines.join(`
6680
- `) }],
6681
- details: {
6682
- readSeekValue: {
6683
- tool: "hover",
6684
- ok: true,
6685
- path: filePath,
6686
- output
6687
- }
6688
- }
6689
- };
6690
- } catch (err) {
6691
- const failure = classifyReadSeekFailure(err);
6692
- return buildToolErrorResult("hover", failure.code, failure.message, failure.hint ? { hint: failure.hint } : {});
6693
- }
6694
- }
6695
- function registerHoverTool(pi) {
6696
- registerReadSeekTool(pi, {
6697
- name: "readSeek_hover",
6698
- label: "Hover",
6699
- description: HOVER_PROMPT_METADATA.description,
6700
- promptSnippet: HOVER_PROMPT_METADATA.promptSnippet,
6701
- promptGuidelines: HOVER_PROMPT_METADATA.promptGuidelines,
6702
- parameters: hoverSchema,
6703
- async execute(_toolCallId, params, signal, _onUpdate, ctx) {
6704
- return executeHover({ params, signal, cwd: ctx.cwd });
6705
- },
6706
- renderCall(args, theme, ...rest) {
6707
- const context = rest[0] ?? {};
6708
- const cwd = context.cwd ?? process.cwd();
6709
- const displayPath = typeof args?.path === "string" ? args.path : "?";
6710
- let text = theme.fg("toolTitle", theme.bold("hover"));
6711
- text += ` ${linkToolPath(theme.fg("accent", displayPath), displayPath, cwd)}`;
6712
- if (args?.line)
6713
- text += theme.fg("dim", `:${args.line}`);
6714
- return new Text7(clampLineToWidth(text, context.width), 0, 0);
6715
- },
6716
- renderResult(result, options, theme, ...rest) {
6717
- const { isPartial, isError, expanded, width } = resolveRenderResultContext(options, rest);
6718
- if (isPartial)
6719
- return renderPendingResult("pending hover", width, theme);
6720
- const content = result.content?.[0];
6721
- const textContent = content?.type === "text" ? content.text : "";
6722
- if (isError || result.isError) {
6723
- return renderErrorResult(textContent, { expanded, width, fallback: "hover failed", theme });
6724
- }
6725
- const output = result.details?.readSeekValue?.output;
6726
- const label = output?.identifier?.text ? `identified ${output.identifier.text}` : output?.symbol?.name ? `symbol ${output.symbol.name}` : "identified cursor";
6727
- let text = summaryLine(label, { hidden: !!textContent && !expanded, theme, style: "success" });
6728
- if (expanded && textContent)
6729
- text += `
6730
- ${textContent}`;
6731
- return new Text7(clampLinesToWidth(text.split(`
6732
- `), width).join(`
6733
- `), 0, 0);
6734
- }
6735
- });
6736
- }
6737
-
6738
5565
  // src/write.ts
6739
- import { mkdir, readFile as readFile2, writeFile } from "node:fs/promises";
5566
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
6740
5567
  import { dirname as dirname2, relative as relative3 } from "node:path";
6741
5568
  import { withFileMutationQueue as withFileMutationQueue2, truncateHead as truncateHead4 } from "@earendil-works/pi-coding-agent";
6742
- import { Text as Text8 } from "@earendil-works/pi-tui";
6743
- import { Type as Type10 } from "@sinclair/typebox";
5569
+ import { Text as Text7 } from "@earendil-works/pi-tui";
5570
+ import { Type as Type9 } from "@sinclair/typebox";
6744
5571
  var WRITE_PENDING_PREVIEW_STATE_KEY = "hashline-write-pending-preview";
6745
5572
  var CONTENT_PREVIEW_MAX_LINES = 200;
6746
5573
  function formatContentPreviewLines(content, theme) {
@@ -6780,7 +5607,7 @@ function isToolErrorResult(result) {
6780
5607
  }
6781
5608
  async function readPreviousTextForDiff(filePath) {
6782
5609
  try {
6783
- const previous = await readFile2(filePath);
5610
+ const previous = await readFile(filePath);
6784
5611
  return {
6785
5612
  content: looksLikeBinary(previous) ? "" : previous.toString("utf-8"),
6786
5613
  existed: true
@@ -6818,7 +5645,7 @@ function buildWriteFsErrorResult(err, absolutePath) {
6818
5645
  }
6819
5646
  async function executeWrite(opts) {
6820
5647
  await ensureHashInit();
6821
- const { path: filePath, content, map: requestMap, cwd } = opts;
5648
+ const { path: filePath, content, cwd } = opts;
6822
5649
  const warnings = [];
6823
5650
  const readSeekWarnings = [];
6824
5651
  if (hasBareCarriageReturn(content)) {
@@ -6844,8 +5671,15 @@ async function executeWrite(opts) {
6844
5671
  const { content: previousContent, existed: existedBeforeWrite } = await readPreviousTextForDiff(filePath);
6845
5672
  await mkdir(dirname2(filePath), { recursive: true });
6846
5673
  await writeFile(filePath, content, "utf-8");
6847
- const rawLines = content.split(`
5674
+ const normalizedContent = stripBom(content).text;
5675
+ const rawLines = normalizedContent === "" ? [] : normalizedContent.split(`
6848
5676
  `);
5677
+ if (rawLines[rawLines.length - 1] === "")
5678
+ rawLines.pop();
5679
+ for (let i = 0;i < rawLines.length; i++) {
5680
+ if (rawLines[i].endsWith("\r"))
5681
+ rawLines[i] = rawLines[i].slice(0, -1);
5682
+ }
6849
5683
  const readSeekLines = [];
6850
5684
  const displayLines = [];
6851
5685
  for (let i = 0;i < rawLines.length; i++) {
@@ -6867,21 +5701,6 @@ async function executeWrite(opts) {
6867
5701
  [… output truncated at 50 KB — full anchors in readSeekValue]`;
6868
5702
  }
6869
5703
  }
6870
- let mapAppended = false;
6871
- if (requestMap) {
6872
- try {
6873
- const fileMap = await getOrGenerateMap(filePath);
6874
- if (fileMap) {
6875
- const mapText = formatFileMapWithBudget(fileMap);
6876
- if (mapText) {
6877
- text += `
6878
-
6879
- ` + mapText;
6880
- mapAppended = true;
6881
- }
6882
- }
6883
- } catch {}
6884
- }
6885
5704
  const displayPath = cwd ? relative3(cwd, filePath) || filePath : filePath;
6886
5705
  const normalizedPrevious = normalizeToLF(previousContent);
6887
5706
  const normalizedNext = normalizeToLF(content);
@@ -6904,8 +5723,7 @@ async function executeWrite(opts) {
6904
5723
  lines: readSeekLines,
6905
5724
  warnings: readSeekWarnings,
6906
5725
  diff: diffResult.diff,
6907
- diffData,
6908
- ...requestMap ? { map: { appended: mapAppended } } : {}
5726
+ diffData
6909
5727
  }
6910
5728
  };
6911
5729
  }
@@ -6922,10 +5740,9 @@ function registerWriteTool(pi, options = {}) {
6922
5740
  description: promptMetadata.description,
6923
5741
  promptSnippet: promptMetadata.promptSnippet,
6924
5742
  promptGuidelines: promptMetadata.promptGuidelines,
6925
- parameters: Type10.Object({
5743
+ parameters: Type9.Object({
6926
5744
  path: filePathParam(),
6927
- content: Type10.String({ description: "Complete text file content" }),
6928
- map: mapParam()
5745
+ content: Type9.String({ description: "Complete text file content" })
6929
5746
  }),
6930
5747
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
6931
5748
  const cwd = ctx?.cwd ?? process.cwd();
@@ -6937,7 +5754,6 @@ function registerWriteTool(pi, options = {}) {
6937
5754
  result = await executeWrite({
6938
5755
  path: absolutePath,
6939
5756
  content: params.content,
6940
- map: params.map,
6941
5757
  cwd
6942
5758
  });
6943
5759
  } catch (err) {
@@ -7007,7 +5823,7 @@ function registerWriteTool(pi, options = {}) {
7007
5823
  `);
7008
5824
  lines.push(...formatContentPreviewLines(content, theme));
7009
5825
  }
7010
- return new Text8(clampLinesToWidth(lines, width).join(`
5826
+ return new Text7(clampLinesToWidth(lines, width).join(`
7011
5827
  `), 0, 0);
7012
5828
  }
7013
5829
  const hasExpandableDiff = !!diffData;
@@ -7016,7 +5832,7 @@ function registerWriteTool(pi, options = {}) {
7016
5832
  return upsertDiffComponent(context.lastComponent, { prefixLines: text.split(`
7017
5833
  `), diffData, theme, expanded: true });
7018
5834
  }
7019
- return new Text8(clampLinesToWidth(text.split(`
5835
+ return new Text7(clampLinesToWidth(text.split(`
7020
5836
  `), width).join(`
7021
5837
  `), 0, 0);
7022
5838
  }
@@ -7025,7 +5841,7 @@ function registerWriteTool(pi, options = {}) {
7025
5841
  }
7026
5842
 
7027
5843
  // src/def.ts
7028
- import { Type as Type11 } from "@sinclair/typebox";
5844
+ import { Type as Type10 } from "@sinclair/typebox";
7029
5845
  import path6 from "node:path";
7030
5846
  var DEF_PROMPT_METADATA = defineToolPromptMetadata({
7031
5847
  promptUrl: new URL("../prompts/def.md", import.meta.url),
@@ -7101,8 +5917,8 @@ function registerDefTool(pi, options = {}) {
7101
5917
  description: DEF_PROMPT_METADATA.description,
7102
5918
  promptSnippet: DEF_PROMPT_METADATA.promptSnippet,
7103
5919
  promptGuidelines: DEF_PROMPT_METADATA.promptGuidelines,
7104
- parameters: Type11.Object({
7105
- name: Type11.String({ description: "Qualified or unqualified symbol name" }),
5920
+ parameters: Type10.Object({
5921
+ name: Type10.String({ description: "Qualified or unqualified symbol name" }),
7106
5922
  path: searchPathParam(),
7107
5923
  lang: langParam(),
7108
5924
  ...readSeekGitSearchParams()
@@ -7128,77 +5944,205 @@ function registerDefTool(pi, options = {}) {
7128
5944
  });
7129
5945
  }
7130
5946
 
7131
- // src/check.ts
7132
- import { readFile as readFile3 } from "node:fs/promises";
7133
- import { Text as Text9 } from "@earendil-works/pi-tui";
7134
- import { Type as Type12 } from "@sinclair/typebox";
7135
- var CHECK_PROMPT_METADATA = defineToolPromptMetadata({
7136
- promptUrl: new URL("../prompts/check.md", import.meta.url),
7137
- promptSnippet: "Check source syntax for parser errors after edits"
5947
+ // src/digest.ts
5948
+ import { Text as Text8 } from "@earendil-works/pi-tui";
5949
+ import { Type as Type11 } from "@sinclair/typebox";
5950
+ var DIGEST_FACETS = ["metadata", "content", "map", "diagnostics", "identity"];
5951
+ var DIGEST_PROMPT_METADATA = defineToolPromptMetadata({
5952
+ promptUrl: new URL("../prompts/digest.md", import.meta.url),
5953
+ promptSnippet: "Digest source facets as a native CLI envelope"
7138
5954
  });
7139
- async function executeCheck(opts) {
7140
- const { params, signal, cwd } = opts;
5955
+ var facetSchema = Type11.Union(DIGEST_FACETS.map((facet) => Type11.Literal(facet)));
5956
+ var digestSchema = Type11.Object({
5957
+ path: filePathParam(),
5958
+ select: Type11.Optional(Type11.Union([
5959
+ facetSchema,
5960
+ Type11.Array(facetSchema, { minItems: 1 }),
5961
+ Type11.String({
5962
+ description: "Comma-separated facets: metadata, content, map, diagnostics, identity"
5963
+ })
5964
+ ], {
5965
+ description: "Facets: metadata, content, map, diagnostics, identity (default content)"
5966
+ })),
5967
+ at: Type11.Optional(Type11.String({
5968
+ description: "Location: line:N[:COLUMN], hash:HASH[:COLUMN], or symbol:NAME"
5969
+ })),
5970
+ end: optionalIntOrString("Last line to include"),
5971
+ limit: optionalIntOrString("Maximum number of lines to include"),
5972
+ language: Type11.Optional(Type11.String({ description: "Language override" })),
5973
+ visionMode: Type11.Optional(Type11.Union([
5974
+ Type11.Literal("none"),
5975
+ Type11.Literal("all"),
5976
+ Type11.Literal("ocr"),
5977
+ Type11.Literal("caption"),
5978
+ Type11.Literal("objects")
5979
+ ], { description: "Standalone image mode: none, all, caption, objects, or ocr" })),
5980
+ visionLevel: Type11.Optional(Type11.Union([Type11.Literal("low"), Type11.Literal("medium"), Type11.Literal("high")], { description: "Vision inference level: low (default), medium, or high" }))
5981
+ });
5982
+ function hasDigestAnchors(value) {
5983
+ if (!value || typeof value !== "object")
5984
+ return false;
5985
+ const record = value;
5986
+ if (Array.isArray(record.hashlines) && record.hashlines.length > 0)
5987
+ return true;
5988
+ return Object.values(record).some(hasDigestAnchors);
5989
+ }
5990
+ function parseSelect(select) {
5991
+ if (select === undefined)
5992
+ return;
5993
+ if (Array.isArray(select)) {
5994
+ const facets = select;
5995
+ if (facets.length === 0)
5996
+ throw new Error("select must include at least one facet");
5997
+ for (const facet of facets) {
5998
+ if (!DIGEST_FACETS.includes(facet)) {
5999
+ throw new Error(`unknown digest facet \`${facet}\`; expected ${DIGEST_FACETS.join(", ")}`);
6000
+ }
6001
+ }
6002
+ return facets;
6003
+ }
6004
+ if (typeof select !== "string") {
6005
+ throw new Error("select must be a facet, facet list, or comma-separated facets");
6006
+ }
6007
+ if (DIGEST_FACETS.includes(select)) {
6008
+ return select;
6009
+ }
6010
+ const parts = select.split(",").map((part) => part.trim()).filter((part) => part.length > 0);
6011
+ if (parts.length === 0)
6012
+ throw new Error("select must include at least one facet");
6013
+ for (const part of parts) {
6014
+ if (!DIGEST_FACETS.includes(part)) {
6015
+ throw new Error(`unknown digest facet \`${part}\`; expected ${DIGEST_FACETS.join(", ")}`);
6016
+ }
6017
+ }
6018
+ return parts;
6019
+ }
6020
+ function selectedFacets(select) {
6021
+ if (select === undefined)
6022
+ return ["content"];
6023
+ return Array.isArray(select) ? select : [select];
6024
+ }
6025
+ function summarizeEnvelope(envelope, facets) {
6026
+ if (!envelope || typeof envelope !== "object")
6027
+ return `digested ${facets.join(",")}`;
6028
+ const record = envelope;
6029
+ const parts = [];
6030
+ for (const facet of facets) {
6031
+ if (facet === "metadata" || record[facet] !== undefined)
6032
+ parts.push(facet);
6033
+ }
6034
+ if (record.metadata !== undefined && !parts.includes("metadata"))
6035
+ parts.unshift("metadata");
6036
+ return `digested ${parts.join(",") || "metadata"}`;
6037
+ }
6038
+ async function executeDigest(opts) {
6039
+ const { params, signal, cwd, onFileAnchored } = opts;
7141
6040
  const p = params;
7142
- const filePath = resolveToCwd(p.path, cwd);
7143
- let content;
6041
+ let select;
7144
6042
  try {
7145
- content = await readFile3(filePath, "utf-8");
6043
+ select = parseSelect(p.select);
7146
6044
  } catch (err) {
7147
- const { code, message } = formatFsError(err, "check-error");
7148
- return buildToolErrorResult("check", code, message, { path: p.path });
6045
+ return buildToolErrorResult("digest", "invalid-parameter", err?.message ?? String(err), {
6046
+ path: p.path
6047
+ });
6048
+ }
6049
+ const end = coerceObviousBase10Int(p.end, "end");
6050
+ if (!end.ok || end.value !== undefined && end.value < 1) {
6051
+ const message = end.ok ? `Invalid end: expected a positive integer, received ${end.value}.` : end.message;
6052
+ return buildToolErrorResult("digest", "invalid-parameter", message, { path: p.path });
6053
+ }
6054
+ const limit = coerceObviousBase10Int(p.limit, "limit");
6055
+ if (!limit.ok || limit.value !== undefined && limit.value < 1) {
6056
+ const message = limit.ok ? `Invalid limit: expected a positive integer, received ${limit.value}.` : limit.message;
6057
+ return buildToolErrorResult("digest", "invalid-parameter", message, { path: p.path });
6058
+ }
6059
+ const at = typeof p.at === "string" ? p.at.trim() : undefined;
6060
+ if (p.at !== undefined && !at) {
6061
+ return buildToolErrorResult("digest", "invalid-parameter", "Invalid at: expected a non-empty location.", {
6062
+ path: p.path
6063
+ });
6064
+ }
6065
+ if (p.visionLevel !== undefined && p.visionMode === undefined) {
6066
+ return buildToolErrorResult("digest", "invalid-params-combo", "visionLevel requires visionMode.", {
6067
+ path: p.path
6068
+ });
7149
6069
  }
6070
+ const filePath = resolveToCwd(p.path, cwd);
6071
+ const facets = selectedFacets(select);
6072
+ const vision = p.visionMode !== undefined && p.visionMode !== "none";
7150
6073
  try {
7151
- const output = await readSeekCheck(filePath, content, { signal });
7152
- const lines = output.diagnostics.map((diagnostic) => `${diagnostic.kind}: lines ${diagnostic.start_line}-${diagnostic.end_line}`);
7153
- const summary = output.errorCount === 0 && output.missingCount === 0 ? "No syntax errors found." : `${output.errorCount} error(s), ${output.missingCount} missing syntax node(s)`;
6074
+ const envelope = await readSeekDigestNative(filePath, {
6075
+ select,
6076
+ at,
6077
+ end: end.value,
6078
+ limit: limit.value,
6079
+ language: p.language,
6080
+ visionMode: p.visionMode,
6081
+ visionLevel: p.visionLevel,
6082
+ signal,
6083
+ vision
6084
+ });
6085
+ if (hasDigestAnchors(envelope))
6086
+ onFileAnchored?.(filePath);
6087
+ const text = JSON.stringify(envelope);
7154
6088
  return {
7155
- content: [{ type: "text", text: [summary, ...lines].join(`
7156
- `) }],
6089
+ content: [{ type: "text", text }],
7157
6090
  details: {
7158
- readSeekValue: { tool: "check", ok: true, path: filePath, output }
6091
+ readSeekValue: {
6092
+ tool: "digest",
6093
+ ok: true,
6094
+ path: filePath,
6095
+ select: facets,
6096
+ envelope
6097
+ }
7159
6098
  }
7160
6099
  };
7161
6100
  } catch (err) {
7162
6101
  const failure = classifyReadSeekFailure(err);
7163
- return buildToolErrorResult("check", failure.code, failure.message, failure.hint ? { hint: failure.hint } : {});
6102
+ return buildToolErrorResult("digest", failure.code, failure.message, failure.hint ? { hint: failure.hint } : {});
7164
6103
  }
7165
6104
  }
7166
- function registerCheckTool(pi) {
6105
+ function registerDigestTool(pi, options = {}) {
7167
6106
  return registerReadSeekTool(pi, {
7168
- name: "readSeek_check",
7169
- label: "Check",
7170
- description: CHECK_PROMPT_METADATA.description,
7171
- promptSnippet: CHECK_PROMPT_METADATA.promptSnippet,
7172
- promptGuidelines: CHECK_PROMPT_METADATA.promptGuidelines,
7173
- parameters: Type12.Object({ path: filePathParam() }),
6107
+ name: "readSeek_digest",
6108
+ label: "Digest",
6109
+ description: DIGEST_PROMPT_METADATA.description,
6110
+ promptSnippet: DIGEST_PROMPT_METADATA.promptSnippet,
6111
+ promptGuidelines: DIGEST_PROMPT_METADATA.promptGuidelines,
6112
+ parameters: digestSchema,
7174
6113
  async execute(_toolCallId, params, signal, _onUpdate, ctx) {
7175
- return executeCheck({ params, signal, cwd: ctx.cwd });
6114
+ return executeDigest({ params, signal, cwd: ctx.cwd, onFileAnchored: options.onFileAnchored });
7176
6115
  },
7177
6116
  renderCall(args, theme, ...rest) {
7178
6117
  const context = rest[0] ?? {};
7179
6118
  const cwd = context.cwd ?? process.cwd();
7180
6119
  const displayPath = typeof args?.path === "string" ? args.path : "?";
7181
- const text = `${theme.fg("toolTitle", theme.bold("check"))} ${linkToolPath(theme.fg("accent", displayPath), displayPath, cwd)}`;
7182
- return new Text9(clampLineToWidth(text, context.width), 0, 0);
6120
+ let text = theme.fg("toolTitle", theme.bold("digest"));
6121
+ text += ` ${linkToolPath(theme.fg("accent", displayPath), displayPath, cwd)}`;
6122
+ if (args?.select !== undefined) {
6123
+ const select = Array.isArray(args.select) ? args.select.join(",") : String(args.select);
6124
+ text += theme.fg("dim", ` [${select}]`);
6125
+ }
6126
+ if (typeof args?.at === "string" && args.at)
6127
+ text += theme.fg("dim", ` @ ${args.at}`);
6128
+ return new Text8(clampLineToWidth(text, context.width), 0, 0);
7183
6129
  },
7184
- renderResult(result, options, theme, ...rest) {
7185
- const { isPartial, isError, expanded, width } = resolveRenderResultContext(options, rest);
6130
+ renderResult(result, options2, theme, ...rest) {
6131
+ const { isPartial, isError, expanded, width } = resolveRenderResultContext(options2, rest);
7186
6132
  if (isPartial)
7187
- return renderPendingResult("pending check", width, theme);
6133
+ return renderPendingResult("pending digest", width, theme);
7188
6134
  const textContent = result.content?.[0]?.type === "text" ? result.content[0].text : "";
7189
6135
  if (isError || result.isError) {
7190
- return renderErrorResult(textContent, { expanded, width, fallback: "check failed", theme });
6136
+ return renderErrorResult(textContent, { expanded, width, fallback: "digest failed", theme });
7191
6137
  }
7192
- const output = result.details?.readSeekValue?.output;
7193
- const issueCount = (output?.errorCount ?? 0) + (output?.missingCount ?? 0);
7194
- let text = summaryLine(issueCount === 0 ? "syntax valid" : `${issueCount} syntax issue(s)`, {
7195
- theme,
7196
- style: issueCount === 0 ? "success" : "warning"
7197
- });
6138
+ const value = result.details?.readSeekValue;
6139
+ const facets = Array.isArray(value?.select) ? value.select : ["content"];
6140
+ const label = summarizeEnvelope(value?.envelope, facets);
6141
+ let text = summaryLine(label, { hidden: !!textContent && !expanded, theme, style: "success" });
7198
6142
  if (expanded && textContent)
7199
6143
  text += `
7200
6144
  ${textContent}`;
7201
- return new Text9(clampLinesToWidth(text.split(`
6145
+ return new Text8(clampLinesToWidth(text.split(`
7202
6146
  `), width).join(`
7203
6147
  `), 0, 0);
7204
6148
  }
@@ -7211,8 +6155,8 @@ import {
7211
6155
  DEFAULT_MAX_LINES as DEFAULT_MAX_LINES5,
7212
6156
  truncateHead as truncateHead5
7213
6157
  } from "@earendil-works/pi-coding-agent";
7214
- import { Text as Text10 } from "@earendil-works/pi-tui";
7215
- import { Type as Type13 } from "@sinclair/typebox";
6158
+ import { Text as Text9 } from "@earendil-works/pi-tui";
6159
+ import { Type as Type12 } from "@sinclair/typebox";
7216
6160
  var NODE_KINDS = [
7217
6161
  "artifact",
7218
6162
  "footer",
@@ -7227,7 +6171,7 @@ var NODE_KINDS = [
7227
6171
  ];
7228
6172
  var VIEW_PROMPT_METADATA = defineToolPromptMetadata({
7229
6173
  promptUrl: new URL("../prompts/view.md", import.meta.url),
7230
- promptSnippet: "Inspect a PDF's structure or selected content"
6174
+ promptSnippet: "Digest a document's structure or selected content"
7231
6175
  });
7232
6176
  async function executeView(opts) {
7233
6177
  const { params, signal, cwd } = opts;
@@ -7248,6 +6192,12 @@ async function executeView(opts) {
7248
6192
  path: input.path
7249
6193
  });
7250
6194
  }
6195
+ if (input.outline && (input.visionMode !== undefined || input.visionLevel !== undefined)) {
6196
+ return buildToolErrorResult("view", "invalid-params-combo", "Cannot combine outline with visionMode or visionLevel.", { path: input.path });
6197
+ }
6198
+ if (input.visionLevel !== undefined && input.visionMode === undefined) {
6199
+ return buildToolErrorResult("view", "invalid-params-combo", "visionLevel requires visionMode.", { path: input.path });
6200
+ }
7251
6201
  const filePath = resolveToCwd(input.path, cwd);
7252
6202
  try {
7253
6203
  const output = await readSeekView(filePath, {
@@ -7256,6 +6206,8 @@ async function executeView(opts) {
7256
6206
  kind: input.kind,
7257
6207
  depth: depth.value,
7258
6208
  outline: input.outline,
6209
+ visionMode: input.visionMode,
6210
+ visionLevel: input.visionLevel,
7259
6211
  signal
7260
6212
  });
7261
6213
  const truncation = truncateHead5(output, {
@@ -7286,15 +6238,26 @@ function registerViewTool(pi) {
7286
6238
  description: VIEW_PROMPT_METADATA.description,
7287
6239
  promptSnippet: VIEW_PROMPT_METADATA.promptSnippet,
7288
6240
  promptGuidelines: VIEW_PROMPT_METADATA.promptGuidelines,
7289
- parameters: Type13.Object({
6241
+ parameters: Type12.Object({
7290
6242
  path: filePathParam(),
7291
- node: Type13.Optional(Type13.String({ description: "Node ID to use as the view root" })),
7292
- page: optionalIntOrString("One-based PDF page"),
7293
- kind: Type13.Optional(Type13.Union(NODE_KINDS.map((kind) => Type13.Literal(kind)), {
6243
+ node: Type12.Optional(Type12.String({ description: "Node ID to use as the view root" })),
6244
+ page: optionalIntOrString("One-based source page"),
6245
+ kind: Type12.Optional(Type12.Union(NODE_KINDS.map((kind) => Type12.Literal(kind)), {
7294
6246
  description: "Node kind filter"
7295
6247
  })),
7296
6248
  depth: optionalIntOrString("Maximum depth below selected roots"),
7297
- outline: Type13.Optional(Type13.Boolean({ description: "Return outline nodes only" }))
6249
+ visionMode: Type12.Optional(Type12.Union([
6250
+ Type12.Literal("all"),
6251
+ Type12.Literal("ocr"),
6252
+ Type12.Literal("caption"),
6253
+ Type12.Literal("objects")
6254
+ ], { description: "Analyze selected assets: all, ocr, caption, or objects" })),
6255
+ visionLevel: Type12.Optional(Type12.Union([
6256
+ Type12.Literal("low"),
6257
+ Type12.Literal("medium"),
6258
+ Type12.Literal("high")
6259
+ ], { description: "Vision inference level: low (default), medium, or high" })),
6260
+ outline: Type12.Optional(Type12.Boolean({ description: "Return outline nodes only" }))
7298
6261
  }),
7299
6262
  async execute(_toolCallId, params, signal, _onUpdate, ctx) {
7300
6263
  return executeView({ params, signal, cwd: ctx.cwd });
@@ -7304,7 +6267,7 @@ function registerViewTool(pi) {
7304
6267
  const cwd = context.cwd ?? process.cwd();
7305
6268
  const displayPath = typeof args?.path === "string" ? args.path : "?";
7306
6269
  const text = `${theme.fg("toolTitle", theme.bold("view"))} ${linkToolPath(theme.fg("accent", displayPath), displayPath, cwd)}`;
7307
- return new Text10(clampLineToWidth(text, context.width), 0, 0);
6270
+ return new Text9(clampLineToWidth(text, context.width), 0, 0);
7308
6271
  },
7309
6272
  renderResult(result, options, theme, ...rest) {
7310
6273
  const { isPartial, isError, expanded, width } = resolveRenderResultContext(options, rest);
@@ -7324,7 +6287,7 @@ function registerViewTool(pi) {
7324
6287
  if (expanded && textContent)
7325
6288
  text += `
7326
6289
  ${textContent}`;
7327
- return new Text10(clampLinesToWidth(text.split(`
6290
+ return new Text9(clampLinesToWidth(text.split(`
7328
6291
  `), width).join(`
7329
6292
  `), 0, 0);
7330
6293
  }
@@ -7356,10 +6319,9 @@ var READSEEK_TOOL_ENTRIES = [
7356
6319
  { builtIn: null, readSeekName: "readSeek_search" },
7357
6320
  { builtIn: null, readSeekName: "readSeek_refs" },
7358
6321
  { builtIn: null, readSeekName: "readSeek_rename" },
7359
- { builtIn: null, readSeekName: "readSeek_hover" },
7360
6322
  { builtIn: "write", readSeekName: "readSeek_write" },
7361
6323
  { builtIn: null, readSeekName: "readSeek_def" },
7362
- { builtIn: null, readSeekName: "readSeek_check" },
6324
+ { builtIn: null, readSeekName: "readSeek_digest" },
7363
6325
  { builtIn: null, readSeekName: "readSeek_view" }
7364
6326
  ];
7365
6327
  function formatSettingsWarning(warning) {
@@ -7370,7 +6332,7 @@ function editingPolicy(readName, editName, writeName) {
7370
6332
  "ReadSeek editing policy:",
7371
6333
  `- Read first with ${readName} when preparing to edit; ${editName} needs fresh LINE:HASH anchors.`,
7372
6334
  `- Use ${editName} for existing files, ${writeName} for whole-file creation or replacement, and readSeek_rename for symbol renames.`,
7373
- "- Run readSeek_check after source edits for a quick syntax check."
6335
+ "- Run readSeek_digest with select diagnostics after source edits for a quick syntax check."
7374
6336
  ].join(`
7375
6337
  `);
7376
6338
  }
@@ -7380,7 +6342,7 @@ function piReadSeekExtension(pi) {
7380
6342
  const hasFreshAnchors = (absolutePath) => sessionAnchors.hasFreshAnchors(absolutePath);
7381
6343
  const forgetAnchors = (absolutePath) => sessionAnchors.forget(absolutePath);
7382
6344
  const { settings } = resolveReadSeekJsonSettings();
7383
- const replacedBuiltIns = new Set(settings.replacedTools ?? []);
6345
+ const replacedBuiltIns = new Set(settings.overrideTools ?? []);
7384
6346
  const binaryAvailable = readSeekBinaryAvailability().available;
7385
6347
  const swap = (builtIn, readSeekName) => binaryAvailable && replacedBuiltIns.has(builtIn) ? builtIn : readSeekName;
7386
6348
  const readName = swap("read", "readSeek_read");
@@ -7399,9 +6361,8 @@ function piReadSeekExtension(pi) {
7399
6361
  registerSearchTool(pi, { onFileAnchored: markAnchored });
7400
6362
  registerRefsTool(pi, { onFileAnchored: markAnchored });
7401
6363
  registerRenameTool(pi, { onFileMutated: forgetAnchors });
7402
- registerHoverTool(pi);
7403
6364
  registerDefTool(pi, { onFileAnchored: markAnchored });
7404
- registerCheckTool(pi);
6365
+ registerDigestTool(pi, { onFileAnchored: markAnchored });
7405
6366
  registerViewTool(pi);
7406
6367
  registerWriteTool(pi, { onFileAnchored: markAnchored, name: writeName });
7407
6368
  pi.on("before_agent_start", (event) => {