pi-readseek 0.8.24 → 0.9.1

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