@levnikolaevich/hex-line-mcp 1.5.0 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/server.mjs CHANGED
@@ -109,64 +109,6 @@ async function checkForUpdates(packageName, currentVersion) {
109
109
  // lib/read.mjs
110
110
  import { statSync as statSync4 } from "node:fs";
111
111
 
112
- // ../hex-common/src/text-protocol/hash.mjs
113
- var FNV_OFFSET = 2166136261;
114
- var FNV_PRIME = 16777619;
115
- var TAG_CHARS = "abcdefghijklmnopqrstuvwxyz234567";
116
- function fnv1a(str) {
117
- const normalized = str.replace(/\r$/, "").replace(/\s+/g, "");
118
- let h = FNV_OFFSET;
119
- for (let i = 0; i < normalized.length; i++) {
120
- let code = normalized.charCodeAt(i);
121
- if (code >= 55296 && code <= 56319 && i + 1 < normalized.length) {
122
- const lo = normalized.charCodeAt(i + 1);
123
- if (lo >= 56320 && lo <= 57343) {
124
- code = (code - 55296 << 10) + (lo - 56320) + 65536;
125
- i++;
126
- }
127
- }
128
- if (code < 128) {
129
- h = Math.imul(h ^ code, FNV_PRIME) >>> 0;
130
- } else if (code < 2048) {
131
- h = Math.imul(h ^ (192 | code >> 6), FNV_PRIME) >>> 0;
132
- h = Math.imul(h ^ (128 | code & 63), FNV_PRIME) >>> 0;
133
- } else if (code < 65536) {
134
- h = Math.imul(h ^ (224 | code >> 12), FNV_PRIME) >>> 0;
135
- h = Math.imul(h ^ (128 | code >> 6 & 63), FNV_PRIME) >>> 0;
136
- h = Math.imul(h ^ (128 | code & 63), FNV_PRIME) >>> 0;
137
- } else {
138
- h = Math.imul(h ^ (240 | code >> 18), FNV_PRIME) >>> 0;
139
- h = Math.imul(h ^ (128 | code >> 12 & 63), FNV_PRIME) >>> 0;
140
- h = Math.imul(h ^ (128 | code >> 6 & 63), FNV_PRIME) >>> 0;
141
- h = Math.imul(h ^ (128 | code & 63), FNV_PRIME) >>> 0;
142
- }
143
- }
144
- return h;
145
- }
146
- function lineTag(hash32) {
147
- return TAG_CHARS[hash32 & 31] + TAG_CHARS[hash32 >>> 8 & 31];
148
- }
149
- function rangeChecksum(lineHashes, startLine, endLine) {
150
- let acc = FNV_OFFSET;
151
- for (const h of lineHashes) {
152
- acc = Math.imul(acc ^ h & 255, FNV_PRIME) >>> 0;
153
- acc = Math.imul(acc ^ h >>> 8 & 255, FNV_PRIME) >>> 0;
154
- acc = Math.imul(acc ^ h >>> 16 & 255, FNV_PRIME) >>> 0;
155
- acc = Math.imul(acc ^ h >>> 24 & 255, FNV_PRIME) >>> 0;
156
- }
157
- return `${startLine}-${endLine}:${acc.toString(16).padStart(8, "0")}`;
158
- }
159
- function parseRef(ref) {
160
- const m = ref.trim().match(/^([a-z2-7]{2})\.(\d+)$/);
161
- if (!m) throw new Error(`Bad ref: "${ref}". Expected "ab.12" (tag.lineNum)`);
162
- return { tag: m[1], line: parseInt(m[2], 10) };
163
- }
164
- function parseChecksum(cs) {
165
- const m = cs.trim().match(/^(\d+)-(\d+):([0-9a-f]{8})$/);
166
- if (!m) throw new Error(`Bad checksum: "${cs}". Expected "1-50:f7e2a1b0"`);
167
- return { start: parseInt(m[1], 10), end: parseInt(m[2], 10), hex: m[3] };
168
- }
169
-
170
112
  // lib/security.mjs
171
113
  import { realpathSync, statSync as statSync2, existsSync, openSync, readSync, closeSync } from "node:fs";
172
114
  import { resolve, isAbsolute, dirname } from "node:path";
@@ -476,9 +418,69 @@ function findProjectRoot(filePath) {
476
418
  return null;
477
419
  }
478
420
 
479
- // lib/revisions.mjs
421
+ // lib/snapshot.mjs
480
422
  import { statSync as statSync3 } from "node:fs";
481
423
  import { diffLines } from "diff";
424
+
425
+ // ../hex-common/src/text-protocol/hash.mjs
426
+ var FNV_OFFSET = 2166136261;
427
+ var FNV_PRIME = 16777619;
428
+ var TAG_CHARS = "abcdefghijklmnopqrstuvwxyz234567";
429
+ function fnv1a(str) {
430
+ const normalized = str.replace(/\r$/, "").replace(/\s+/g, "");
431
+ let h = FNV_OFFSET;
432
+ for (let i = 0; i < normalized.length; i++) {
433
+ let code = normalized.charCodeAt(i);
434
+ if (code >= 55296 && code <= 56319 && i + 1 < normalized.length) {
435
+ const lo = normalized.charCodeAt(i + 1);
436
+ if (lo >= 56320 && lo <= 57343) {
437
+ code = (code - 55296 << 10) + (lo - 56320) + 65536;
438
+ i++;
439
+ }
440
+ }
441
+ if (code < 128) {
442
+ h = Math.imul(h ^ code, FNV_PRIME) >>> 0;
443
+ } else if (code < 2048) {
444
+ h = Math.imul(h ^ (192 | code >> 6), FNV_PRIME) >>> 0;
445
+ h = Math.imul(h ^ (128 | code & 63), FNV_PRIME) >>> 0;
446
+ } else if (code < 65536) {
447
+ h = Math.imul(h ^ (224 | code >> 12), FNV_PRIME) >>> 0;
448
+ h = Math.imul(h ^ (128 | code >> 6 & 63), FNV_PRIME) >>> 0;
449
+ h = Math.imul(h ^ (128 | code & 63), FNV_PRIME) >>> 0;
450
+ } else {
451
+ h = Math.imul(h ^ (240 | code >> 18), FNV_PRIME) >>> 0;
452
+ h = Math.imul(h ^ (128 | code >> 12 & 63), FNV_PRIME) >>> 0;
453
+ h = Math.imul(h ^ (128 | code >> 6 & 63), FNV_PRIME) >>> 0;
454
+ h = Math.imul(h ^ (128 | code & 63), FNV_PRIME) >>> 0;
455
+ }
456
+ }
457
+ return h;
458
+ }
459
+ function lineTag(hash32) {
460
+ return TAG_CHARS[hash32 & 31] + TAG_CHARS[hash32 >>> 8 & 31];
461
+ }
462
+ function rangeChecksum(lineHashes, startLine, endLine) {
463
+ let acc = FNV_OFFSET;
464
+ for (const h of lineHashes) {
465
+ acc = Math.imul(acc ^ h & 255, FNV_PRIME) >>> 0;
466
+ acc = Math.imul(acc ^ h >>> 8 & 255, FNV_PRIME) >>> 0;
467
+ acc = Math.imul(acc ^ h >>> 16 & 255, FNV_PRIME) >>> 0;
468
+ acc = Math.imul(acc ^ h >>> 24 & 255, FNV_PRIME) >>> 0;
469
+ }
470
+ return `${startLine}-${endLine}:${acc.toString(16).padStart(8, "0")}`;
471
+ }
472
+ function parseRef(ref) {
473
+ const m = ref.trim().match(/^([a-z2-7]{2})\.(\d+)$/);
474
+ if (!m) throw new Error(`Bad ref: "${ref}". Expected "ab.12" (tag.lineNum)`);
475
+ return { tag: m[1], line: parseInt(m[2], 10) };
476
+ }
477
+ function parseChecksum(cs) {
478
+ const m = cs.trim().match(/^(\d+)-(\d+):([0-9a-f]{8})$/);
479
+ if (!m) throw new Error(`Bad checksum: "${cs}". Expected "1-50:f7e2a1b0"`);
480
+ return { start: parseInt(m[1], 10), end: parseInt(m[2], 10), hex: m[3] };
481
+ }
482
+
483
+ // lib/snapshot.mjs
482
484
  var MAX_FILES = 200;
483
485
  var MAX_REVISIONS_PER_FILE = 5;
484
486
  var TTL_MS = 5 * 60 * 1e3;
@@ -658,13 +660,140 @@ function getSnapshotByRevision(revision) {
658
660
  return revisionsById.get(revision) || null;
659
661
  }
660
662
  function overlapsChangedRanges(ranges, startLine, endLine) {
661
- return (ranges || []).some((range) => range.start <= endLine && startLine <= range.end);
663
+ return (ranges || []).some((range) => range.start <= endLine && range.end >= startLine);
662
664
  }
663
665
  function buildRangeChecksum(snapshot, startLine, endLine) {
664
- const startIdx = startLine - 1;
665
- const endIdx = endLine - 1;
666
- if (startIdx < 0 || endIdx >= snapshot.lineHashes.length || startIdx > endIdx) return null;
667
- return rangeChecksum(snapshot.lineHashes.slice(startIdx, endIdx + 1), startLine, endLine);
666
+ if (startLine < 1 || endLine > snapshot.lineHashes.length || startLine > endLine) return null;
667
+ return rangeChecksum(snapshot.lineHashes.slice(startLine - 1, endLine), startLine, endLine);
668
+ }
669
+
670
+ // lib/block-protocol.mjs
671
+ function normalizeEntry(entry) {
672
+ const text = String(entry.text ?? "");
673
+ const hash32 = entry.hash32 ?? fnv1a(text);
674
+ return {
675
+ lineNumber: entry.lineNumber,
676
+ text,
677
+ hash32,
678
+ tag: entry.tag ?? lineTag(hash32),
679
+ role: entry.role ?? "content",
680
+ annotation: entry.annotation ?? ""
681
+ };
682
+ }
683
+ function renderRequestedSpan(block) {
684
+ if (block.requestedStartLine === null || block.requestedEndLine === null) return null;
685
+ if (block.requestedStartLine === block.startLine && block.requestedEndLine === block.endLine) return null;
686
+ return `requested_span: ${block.requestedStartLine}-${block.requestedEndLine}`;
687
+ }
688
+ function renderBaseEntry(entry, plain = false) {
689
+ return plain ? `${entry.lineNumber}|${entry.text}` : `${entry.tag}.${entry.lineNumber} ${entry.text}`;
690
+ }
691
+ function createSnapshotEntries(snapshot, startLine, endLine, extra = {}) {
692
+ const entries = [];
693
+ for (let lineNumber = startLine; lineNumber <= endLine; lineNumber++) {
694
+ const idx = lineNumber - 1;
695
+ entries.push(normalizeEntry({
696
+ lineNumber,
697
+ text: snapshot.lines[idx],
698
+ hash32: snapshot.lineHashes[idx],
699
+ ...extra
700
+ }));
701
+ }
702
+ return entries;
703
+ }
704
+ function buildEditReadyBlock({ path, kind, entries, requestedStartLine = null, requestedEndLine = null, meta = {} }) {
705
+ if (!Array.isArray(entries) || entries.length === 0) {
706
+ throw new Error("EditReadyBlock requires at least one entry");
707
+ }
708
+ const normalized = entries.map(normalizeEntry).sort((a, b) => a.lineNumber - b.lineNumber);
709
+ const startLine = normalized[0].lineNumber;
710
+ const endLine = normalized[normalized.length - 1].lineNumber;
711
+ return {
712
+ type: "edit_ready_block",
713
+ path,
714
+ kind,
715
+ startLine,
716
+ endLine,
717
+ requestedStartLine: requestedStartLine ?? startLine,
718
+ requestedEndLine: requestedEndLine ?? endLine,
719
+ entries: normalized,
720
+ checksum: rangeChecksum(normalized.map((entry) => entry.hash32), startLine, endLine),
721
+ meta
722
+ };
723
+ }
724
+ function buildDiagnosticBlock({
725
+ path = "",
726
+ kind,
727
+ message,
728
+ requestedStartLine = null,
729
+ requestedEndLine = null,
730
+ startLine = null,
731
+ endLine = null,
732
+ meta = {}
733
+ }) {
734
+ return {
735
+ type: "diagnostic_block",
736
+ path,
737
+ kind,
738
+ message,
739
+ requestedStartLine,
740
+ requestedEndLine,
741
+ startLine,
742
+ endLine,
743
+ meta
744
+ };
745
+ }
746
+ function serializeReadEntry(entry, opts = {}) {
747
+ return renderBaseEntry(entry, opts.plain);
748
+ }
749
+ function serializeSearchEntry(entry, opts = {}) {
750
+ const body = renderBaseEntry(entry, opts.plain);
751
+ if (opts.plain) return body;
752
+ const prefix = entry.role === "match" ? ">>" : " ";
753
+ const suffix = entry.annotation ? ` ${entry.annotation}` : "";
754
+ return `${prefix}${body}${suffix}`;
755
+ }
756
+ function serializeReadBlock(block, opts = {}) {
757
+ if (block.type !== "edit_ready_block") return serializeDiagnosticBlock(block);
758
+ const lines = [
759
+ `block: ${block.kind}`,
760
+ `span: ${block.startLine}-${block.endLine}`
761
+ ];
762
+ const requestedSpan = renderRequestedSpan(block);
763
+ if (requestedSpan) lines.push(requestedSpan);
764
+ lines.push(...block.entries.map((entry) => serializeReadEntry(entry, opts)));
765
+ lines.push(`checksum: ${block.checksum}`);
766
+ return lines.join("\n");
767
+ }
768
+ function serializeSearchBlock(block, opts = {}) {
769
+ if (block.type !== "edit_ready_block") return serializeDiagnosticBlock(block);
770
+ const lines = [
771
+ `block: ${block.kind}`,
772
+ `file: ${block.path}`,
773
+ `span: ${block.startLine}-${block.endLine}`
774
+ ];
775
+ const requestedSpan = renderRequestedSpan(block);
776
+ if (requestedSpan) lines.push(requestedSpan);
777
+ if (Array.isArray(block.meta.matchLines) && block.meta.matchLines.length > 0) {
778
+ lines.push(`match_lines: ${block.meta.matchLines.join(",")}`);
779
+ }
780
+ if (block.meta.summary) lines.push(`summary: ${block.meta.summary}`);
781
+ lines.push(...block.entries.map((entry) => serializeSearchEntry(entry, opts)));
782
+ lines.push(`checksum: ${block.checksum}`);
783
+ return lines.join("\n");
784
+ }
785
+ function serializeDiagnosticBlock(block) {
786
+ const lines = ["block: diagnostic"];
787
+ if (block.path) lines.push(`file: ${block.path}`);
788
+ lines.push(`kind: ${block.kind}`);
789
+ if (block.startLine !== null && block.endLine !== null) {
790
+ lines.push(`span: ${block.startLine}-${block.endLine}`);
791
+ }
792
+ if (block.requestedStartLine !== null && block.requestedEndLine !== null) {
793
+ lines.push(`requested_span: ${block.requestedStartLine}-${block.requestedEndLine}`);
794
+ }
795
+ lines.push(`message: ${block.message}`);
796
+ return lines.join("\n");
668
797
  }
669
798
 
670
799
  // lib/read.mjs
@@ -687,6 +816,67 @@ function parseRangeEntry(entry, total) {
687
816
  }
688
817
  return { start, end };
689
818
  }
819
+ function normalizeRange(entry, total) {
820
+ const parsed = parseRangeEntry(entry, total);
821
+ if (!Number.isInteger(parsed.start) || !Number.isInteger(parsed.end)) {
822
+ throw new Error("ranges entries must resolve to integer start/end values");
823
+ }
824
+ if (parsed.start > parsed.end) {
825
+ return buildDiagnosticBlock({
826
+ kind: "invalid_range",
827
+ message: `Requested range ${parsed.start}-${parsed.end} has start greater than end.`,
828
+ requestedStartLine: parsed.start,
829
+ requestedEndLine: parsed.end
830
+ });
831
+ }
832
+ if (parsed.end < 1 || parsed.start > total) {
833
+ return buildDiagnosticBlock({
834
+ kind: "invalid_range",
835
+ message: `Requested range ${parsed.start}-${parsed.end} is outside file length ${total}.`,
836
+ requestedStartLine: parsed.start,
837
+ requestedEndLine: parsed.end
838
+ });
839
+ }
840
+ return {
841
+ requestedStartLine: parsed.start,
842
+ requestedEndLine: parsed.end,
843
+ startLine: Math.max(1, parsed.start),
844
+ endLine: Math.min(total, parsed.end)
845
+ };
846
+ }
847
+ function buildReadBlock(snapshot, range, plain, remainingChars) {
848
+ const entries = [];
849
+ let nextBudget = remainingChars;
850
+ let cappedAtLine = 0;
851
+ for (let lineNumber = range.startLine; lineNumber <= range.endLine; lineNumber++) {
852
+ const entry = createSnapshotEntries(snapshot, lineNumber, lineNumber)[0];
853
+ const rendered = serializeReadEntry(entry, { plain });
854
+ const nextCost = rendered.length + 1;
855
+ if (entries.length > 0 && nextBudget - nextCost < 0) {
856
+ cappedAtLine = lineNumber;
857
+ break;
858
+ }
859
+ entries.push(entry);
860
+ nextBudget -= nextCost;
861
+ }
862
+ if (entries.length === 0) {
863
+ const entry = createSnapshotEntries(snapshot, range.startLine, range.startLine)[0];
864
+ entries.push(entry);
865
+ nextBudget -= serializeReadEntry(entry, { plain }).length + 1;
866
+ if (range.endLine > range.startLine) cappedAtLine = range.startLine + 1;
867
+ }
868
+ return {
869
+ block: buildEditReadyBlock({
870
+ path: snapshot.path,
871
+ kind: "read_range",
872
+ entries,
873
+ requestedStartLine: range.requestedStartLine,
874
+ requestedEndLine: range.requestedEndLine
875
+ }),
876
+ remainingChars: nextBudget,
877
+ cappedAtLine
878
+ };
879
+ }
690
880
  function readFile2(filePath, opts = {}) {
691
881
  filePath = normalizePath(filePath);
692
882
  const real = validatePath(filePath);
@@ -699,60 +889,52 @@ function readFile2(filePath, opts = {}) {
699
889
  ${text}
700
890
  \`\`\``;
701
891
  }
702
- const snapshot = rememberSnapshot(real, readText(real), { mtimeMs: stat.mtimeMs, size: stat.size });
703
- const lines = snapshot.lines;
704
- const total = lines.length;
705
- let ranges;
892
+ const snapshot = readSnapshot(real);
893
+ const total = snapshot.lines.length;
894
+ let requestedRanges;
706
895
  if (opts.ranges && opts.ranges.length > 0) {
707
- ranges = opts.ranges.map((entry) => {
708
- const parsed = parseRangeEntry(entry, total);
709
- return {
710
- start: Math.max(1, parsed.start),
711
- end: Math.min(total, parsed.end)
712
- };
713
- });
896
+ requestedRanges = opts.ranges;
714
897
  } else {
715
898
  const startLine = Math.max(1, opts.offset || 1);
716
899
  const maxLines = opts.limit && opts.limit > 0 ? opts.limit : DEFAULT_LIMIT;
717
- ranges = [{ start: startLine, end: Math.min(total, startLine - 1 + maxLines) }];
900
+ requestedRanges = [{ start: startLine, end: Math.min(total, startLine - 1 + maxLines) }];
718
901
  }
719
- const parts = [];
902
+ const blocks = [];
903
+ const diagnostics = [];
904
+ let remainingChars = MAX_OUTPUT_CHARS;
720
905
  let cappedAtLine = 0;
721
- for (const range of ranges) {
722
- const selected = lines.slice(range.start - 1, range.end);
723
- const lineHashes = [];
724
- const formatted = [];
725
- let charCount = 0;
726
- for (let i = 0; i < selected.length; i++) {
727
- const line = selected[i];
728
- const num = range.start + i;
729
- const hash32 = fnv1a(line);
730
- const entry = opts.plain ? `${num}|${line}` : `${lineTag(hash32)}.${num} ${line}`;
731
- if (charCount + entry.length > MAX_OUTPUT_CHARS && formatted.length > 0) {
732
- cappedAtLine = num;
733
- break;
734
- }
735
- lineHashes.push(hash32);
736
- formatted.push(entry);
737
- charCount += entry.length + 1;
738
- }
739
- const actualEnd = formatted.length > 0 ? range.start + formatted.length - 1 : range.start;
740
- range.end = actualEnd;
741
- parts.push(formatted.join("\n"));
742
- const cs = rangeChecksum(lineHashes, range.start, actualEnd);
743
- parts.push(`checksum: ${cs}`);
744
- if (cappedAtLine) break;
745
- }
746
- const sizeKB = (stat.size / 1024).toFixed(1);
906
+ for (const requested of requestedRanges) {
907
+ const normalized = normalizeRange(requested, total);
908
+ if (normalized.type === "diagnostic_block") {
909
+ diagnostics.push(normalized);
910
+ continue;
911
+ }
912
+ const built = buildReadBlock(snapshot, normalized, opts.plain, remainingChars);
913
+ blocks.push(built.block);
914
+ remainingChars = built.remainingChars;
915
+ if (built.cappedAtLine) {
916
+ cappedAtLine = built.cappedAtLine;
917
+ diagnostics.push(buildDiagnosticBlock({
918
+ kind: "output_capped",
919
+ message: `OUTPUT_CAPPED at line ${built.cappedAtLine} (${MAX_OUTPUT_CHARS} char limit). Use offset=${built.cappedAtLine} to continue reading.`,
920
+ requestedStartLine: built.cappedAtLine,
921
+ requestedEndLine: built.cappedAtLine,
922
+ startLine: built.block.startLine,
923
+ endLine: built.block.endLine
924
+ }));
925
+ break;
926
+ }
927
+ }
928
+ const sizeText = formatSize(stat.size);
747
929
  const ago = relativeTime(stat.mtime);
748
- let meta = `${total} lines, ${sizeKB}KB, ${ago}`;
749
- if (ranges.length === 1) {
750
- const r = ranges[0];
751
- if (r.start > 1 || r.end < total) {
752
- meta += `, showing ${r.start}-${r.end}`;
930
+ let meta = `${total} lines, ${sizeText}, ${ago}`;
931
+ if (requestedRanges.length === 1 && blocks.length === 1) {
932
+ const block = blocks[0];
933
+ if (block.startLine > 1 || block.endLine < total) {
934
+ meta += `, showing ${block.startLine}-${block.endLine}`;
753
935
  }
754
- if (r.end < total) {
755
- meta += `, ${total - r.end} more below`;
936
+ if (block.endLine < total) {
937
+ meta += `, ${total - block.endLine} more below`;
756
938
  }
757
939
  }
758
940
  const db = opts.includeGraph ? getGraphDB(real) : null;
@@ -769,23 +951,112 @@ ${text}
769
951
  Graph: ${items.join(" | ")}`;
770
952
  }
771
953
  }
772
- let result = `File: ${filePath}${graphLine}
954
+ const serializedBlocks = [
955
+ ...blocks.map((block) => serializeReadBlock(block, { plain: opts.plain })),
956
+ ...diagnostics.map((block) => serializeDiagnosticBlock(block))
957
+ ];
958
+ if (cappedAtLine && serializedBlocks.length === 0) {
959
+ serializedBlocks.push(serializeDiagnosticBlock(buildDiagnosticBlock({
960
+ kind: "output_capped",
961
+ message: `OUTPUT_CAPPED at line ${cappedAtLine} (${MAX_OUTPUT_CHARS} char limit). Use offset=${cappedAtLine} to continue reading.`,
962
+ requestedStartLine: cappedAtLine,
963
+ requestedEndLine: cappedAtLine
964
+ })));
965
+ }
966
+ return `File: ${filePath}${graphLine}
773
967
  meta: ${meta}
774
968
  revision: ${snapshot.revision}
775
969
  file: ${snapshot.fileChecksum}
776
970
 
777
- ${parts.join("\n\n")}`;
778
- if (cappedAtLine) {
779
- result += `
780
-
781
- OUTPUT_CAPPED at line ${cappedAtLine} (${MAX_OUTPUT_CHARS} char limit). Use offset=${cappedAtLine} to continue reading.`;
782
- }
783
- return result;
971
+ ${serializedBlocks.join("\n\n")}`.trim();
784
972
  }
785
973
 
786
974
  // lib/edit.mjs
787
975
  import { statSync as statSync5, writeFileSync } from "node:fs";
788
976
  import { diffLines as diffLines2 } from "diff";
977
+
978
+ // lib/edit-resolution.mjs
979
+ function normalizeAnchoredEdits(edits) {
980
+ const anchored = [];
981
+ for (const edit of edits) {
982
+ if (edit.set_line || edit.replace_lines || edit.insert_after || edit.replace_between) {
983
+ anchored.push(edit);
984
+ continue;
985
+ }
986
+ if (edit.replace) {
987
+ throw new Error("REPLACE_REMOVED: replace is no longer supported in edit_file. Use set_line/replace_lines for single edits, bulk_replace tool for rename/refactor.");
988
+ }
989
+ throw new Error(`BAD_INPUT: unknown edit type: ${JSON.stringify(edit)}`);
990
+ }
991
+ return anchored;
992
+ }
993
+ function getEditStartLine(edit) {
994
+ if (edit.set_line) return parseRef(edit.set_line.anchor).line;
995
+ if (edit.replace_lines) return parseRef(edit.replace_lines.start_anchor).line;
996
+ if (edit.insert_after) return parseRef(edit.insert_after.anchor).line;
997
+ if (edit.replace_between) return parseRef(edit.replace_between.start_anchor).line;
998
+ throw new Error(`BAD_INPUT: unsupported edit shape: ${JSON.stringify(edit)}`);
999
+ }
1000
+ function collectEditTargets(edits) {
1001
+ return edits.map((edit) => {
1002
+ if (edit.set_line) {
1003
+ const line = parseRef(edit.set_line.anchor).line;
1004
+ return { start: line, end: line, insert: false, kind: "set_line" };
1005
+ }
1006
+ if (edit.replace_lines) {
1007
+ const start = parseRef(edit.replace_lines.start_anchor).line;
1008
+ const end = parseRef(edit.replace_lines.end_anchor).line;
1009
+ return { start, end, insert: false, kind: "replace_lines" };
1010
+ }
1011
+ if (edit.insert_after) {
1012
+ const line = parseRef(edit.insert_after.anchor).line;
1013
+ return { start: line, end: line, insert: true, kind: "insert_after" };
1014
+ }
1015
+ if (edit.replace_between) {
1016
+ const start = parseRef(edit.replace_between.start_anchor).line;
1017
+ const end = parseRef(edit.replace_between.end_anchor).line;
1018
+ return { start, end, insert: false, kind: "replace_between" };
1019
+ }
1020
+ throw new Error(`BAD_INPUT: unsupported edit shape: ${JSON.stringify(edit)}`);
1021
+ });
1022
+ }
1023
+ function assertNonOverlappingTargets(targets) {
1024
+ for (let i = 0; i < targets.length; i++) {
1025
+ for (let j = i + 1; j < targets.length; j++) {
1026
+ const a = targets[i];
1027
+ const b = targets[j];
1028
+ if (a.insert || b.insert) continue;
1029
+ if (a.start <= b.end && b.start <= a.end) {
1030
+ throw new Error(
1031
+ `OVERLAPPING_EDITS: lines ${a.start}-${a.end} and ${b.start}-${b.end} overlap. Split into separate edit_file calls.`
1032
+ );
1033
+ }
1034
+ }
1035
+ }
1036
+ }
1037
+ function sortEditsForApply(edits) {
1038
+ return edits.map((edit) => ({ ...edit, _k: getEditStartLine(edit) })).sort((a, b) => b._k - a._k);
1039
+ }
1040
+ function targetRangeForReplaceBetween(startIdx, endIdx, boundaryMode) {
1041
+ if (boundaryMode === "exclusive") {
1042
+ return { start: startIdx + 2, end: Math.max(startIdx + 1, endIdx) };
1043
+ }
1044
+ return { start: startIdx + 1, end: endIdx + 1 };
1045
+ }
1046
+ function validateChecksumCoverage(rangeChecksum2, actualStart, actualEnd) {
1047
+ const { start, end } = parseChecksum(rangeChecksum2);
1048
+ if (start > actualStart || end < actualEnd) {
1049
+ return {
1050
+ ok: false,
1051
+ start,
1052
+ end,
1053
+ reason: `CHECKSUM_RANGE_GAP: checksum covers lines ${start}-${end} but edit spans ${actualStart}-${actualEnd} (inclusive). Checksum range must fully contain the anchor range.`
1054
+ };
1055
+ }
1056
+ return { ok: true, start, end, reason: null };
1057
+ }
1058
+
1059
+ // lib/edit.mjs
789
1060
  function restoreIndent(origLines, newLines) {
790
1061
  if (!origLines.length || !newLines.length) return newLines;
791
1062
  const origIndent = origLines[0].match(/^\s*/)[0];
@@ -971,11 +1242,137 @@ Tip: Retry from the fresh local snippet above.`;
971
1242
  path: ${filePath}`;
972
1243
  return msg;
973
1244
  }
974
- function targetRangeForReplaceBetween(startIdx, endIdx, boundaryMode) {
975
- if (boundaryMode === "exclusive") {
976
- return { start: startIdx + 2, end: Math.max(startIdx + 1, endIdx) };
1245
+ function applySetLineEdit(edit, ctx) {
1246
+ const { lines, opts, locateOrConflict, ensureRevisionContext } = ctx;
1247
+ const { tag, line } = parseRef(edit.set_line.anchor);
1248
+ const idx = locateOrConflict({ tag, line });
1249
+ if (typeof idx === "string") return idx;
1250
+ const conflict = ensureRevisionContext(idx + 1, idx + 1, idx);
1251
+ if (conflict) return conflict;
1252
+ const txt = edit.set_line.new_text;
1253
+ if (!txt && txt !== 0) {
1254
+ lines.splice(idx, 1);
1255
+ return null;
977
1256
  }
978
- return { start: startIdx + 1, end: endIdx + 1 };
1257
+ const origLine = [lines[idx]];
1258
+ const raw = sanitizeEditText(txt).split("\n");
1259
+ const newLines = opts.restoreIndent ? restoreIndent(origLine, raw) : raw;
1260
+ lines.splice(idx, 1, ...newLines);
1261
+ return null;
1262
+ }
1263
+ function applyInsertAfterEdit(edit, ctx) {
1264
+ const { lines, opts, locateOrConflict, ensureRevisionContext } = ctx;
1265
+ const { tag, line } = parseRef(edit.insert_after.anchor);
1266
+ const idx = locateOrConflict({ tag, line });
1267
+ if (typeof idx === "string") return idx;
1268
+ const conflict = ensureRevisionContext(idx + 1, idx + 1, idx);
1269
+ if (conflict) return conflict;
1270
+ let insertLines = sanitizeEditText(edit.insert_after.text).split("\n");
1271
+ if (opts.restoreIndent) insertLines = restoreIndent([lines[idx]], insertLines);
1272
+ lines.splice(idx + 1, 0, ...insertLines);
1273
+ return null;
1274
+ }
1275
+ function applyReplaceLinesEdit(edit, ctx) {
1276
+ const {
1277
+ baseSnapshot,
1278
+ buildStrictChecksumMismatchError,
1279
+ conflictIfNeeded,
1280
+ currentSnapshot,
1281
+ ensureRevisionContext,
1282
+ hasBaseSnapshot,
1283
+ lines,
1284
+ locateOrConflict,
1285
+ opts,
1286
+ origLines,
1287
+ staleRevision
1288
+ } = ctx;
1289
+ const startRef = parseRef(edit.replace_lines.start_anchor);
1290
+ const endRef = parseRef(edit.replace_lines.end_anchor);
1291
+ const startIdx = locateOrConflict(startRef);
1292
+ if (typeof startIdx === "string") return startIdx;
1293
+ const endIdx = locateOrConflict(endRef);
1294
+ if (typeof endIdx === "string") return endIdx;
1295
+ const actualStart = startIdx + 1;
1296
+ const actualEnd = endIdx + 1;
1297
+ const rangeChecksum2 = edit.replace_lines.range_checksum;
1298
+ if (!rangeChecksum2) {
1299
+ throw new Error("range_checksum required for replace_lines. Read the range first via read_file, then pass its checksum. The checksum range must cover start-to-end anchors (inclusive).");
1300
+ }
1301
+ if (staleRevision && opts.conflictPolicy === "conservative") {
1302
+ const conflict = ensureRevisionContext(actualStart, actualEnd, startIdx);
1303
+ if (conflict) return conflict;
1304
+ const baseCheck = hasBaseSnapshot ? verifyChecksumAgainstSnapshot(baseSnapshot, rangeChecksum2) : null;
1305
+ if (!baseCheck?.ok) {
1306
+ return conflictIfNeeded(
1307
+ "stale_checksum",
1308
+ startIdx,
1309
+ baseCheck?.actual || null,
1310
+ baseCheck?.actual ? `Provided checksum ${rangeChecksum2} does not match base revision ${opts.baseRevision}.` : `Checksum range from ${rangeChecksum2} is outside the available base revision.`
1311
+ );
1312
+ }
1313
+ } else {
1314
+ const coverage = validateChecksumCoverage(rangeChecksum2, actualStart, actualEnd);
1315
+ const { start: csStart, end: csEnd, hex: csHex } = parseChecksum(rangeChecksum2);
1316
+ if (!coverage.ok) {
1317
+ const snip = buildErrorSnippet(origLines, actualStart - 1);
1318
+ throw new Error(
1319
+ `${coverage.reason}
1320
+
1321
+ Current content (lines ${snip.start}-${snip.end}):
1322
+ ${snip.text}
1323
+
1324
+ Tip: Use updated hashes above for retry.`
1325
+ );
1326
+ }
1327
+ const actual = buildRangeChecksum(currentSnapshot, csStart, csEnd);
1328
+ const actualHex = actual?.split(":")[1];
1329
+ if (!actual || csHex !== actualHex) {
1330
+ const details = `CHECKSUM_MISMATCH: expected ${rangeChecksum2}, got ${actual}. Content at lines ${csStart}-${csEnd} differs from when you read it.
1331
+ Recovery: read_file path ranges=["${csStart}-${csEnd}"], then retry edit with fresh checksum.`;
1332
+ if (opts.conflictPolicy === "conservative") {
1333
+ return conflictIfNeeded("stale_checksum", csStart - 1, actual, details);
1334
+ }
1335
+ throw buildStrictChecksumMismatchError(details, csStart, actual);
1336
+ }
1337
+ }
1338
+ const txt = edit.replace_lines.new_text;
1339
+ if (!txt && txt !== 0) {
1340
+ lines.splice(startIdx, endIdx - startIdx + 1);
1341
+ return null;
1342
+ }
1343
+ const origRange = lines.slice(startIdx, endIdx + 1);
1344
+ let newLines = sanitizeEditText(txt).split("\n");
1345
+ if (opts.restoreIndent) newLines = restoreIndent(origRange, newLines);
1346
+ lines.splice(startIdx, endIdx - startIdx + 1, ...newLines);
1347
+ return null;
1348
+ }
1349
+ function applyReplaceBetweenEdit(edit, ctx) {
1350
+ const { lines, opts, locateOrConflict, ensureRevisionContext } = ctx;
1351
+ const boundaryMode = edit.replace_between.boundary_mode || "inclusive";
1352
+ if (boundaryMode !== "inclusive" && boundaryMode !== "exclusive") {
1353
+ throw new Error(`BAD_INPUT: replace_between boundary_mode must be inclusive or exclusive, got ${boundaryMode}`);
1354
+ }
1355
+ const startRef = parseRef(edit.replace_between.start_anchor);
1356
+ const endRef = parseRef(edit.replace_between.end_anchor);
1357
+ const startIdx = locateOrConflict(startRef);
1358
+ if (typeof startIdx === "string") return startIdx;
1359
+ const endIdx = locateOrConflict(endRef);
1360
+ if (typeof endIdx === "string") return endIdx;
1361
+ if (startIdx > endIdx) {
1362
+ throw new Error(`BAD_INPUT: replace_between start anchor resolves after end anchor (${startIdx + 1} > ${endIdx + 1})`);
1363
+ }
1364
+ const targetRange = targetRangeForReplaceBetween(startIdx, endIdx, boundaryMode);
1365
+ const conflict = ensureRevisionContext(targetRange.start, targetRange.end, startIdx);
1366
+ if (conflict) return conflict;
1367
+ const txt = edit.replace_between.new_text;
1368
+ let newLines = sanitizeEditText(txt ?? "").split("\n");
1369
+ const sliceStart = boundaryMode === "exclusive" ? startIdx + 1 : startIdx;
1370
+ const removeCount = boundaryMode === "exclusive" ? Math.max(0, endIdx - startIdx - 1) : endIdx - startIdx + 1;
1371
+ const origRange = lines.slice(sliceStart, sliceStart + removeCount);
1372
+ if (opts.restoreIndent && origRange.length > 0) newLines = restoreIndent(origRange, newLines);
1373
+ if (txt === "" || txt === null) newLines = [];
1374
+ lines.splice(sliceStart, removeCount, ...newLines);
1375
+ return null;
979
1376
  }
980
1377
  function editFile(filePath, edits, opts = {}) {
981
1378
  filePath = normalizePath(filePath);
@@ -994,49 +1391,9 @@ function editFile(filePath, edits, opts = {}) {
994
1391
  let autoRebased = false;
995
1392
  const remaps = [];
996
1393
  const remapKeys = /* @__PURE__ */ new Set();
997
- const anchored = [];
998
- for (const e of edits) {
999
- if (e.set_line || e.replace_lines || e.insert_after || e.replace_between) anchored.push(e);
1000
- else if (e.replace) throw new Error("REPLACE_REMOVED: replace is no longer supported in edit_file. Use set_line/replace_lines for single edits, bulk_replace tool for rename/refactor.");
1001
- else throw new Error(`BAD_INPUT: unknown edit type: ${JSON.stringify(e)}`);
1002
- }
1003
- const editTargets = [];
1004
- for (const e of anchored) {
1005
- if (e.set_line) {
1006
- const line = parseRef(e.set_line.anchor).line;
1007
- editTargets.push({ start: line, end: line });
1008
- } else if (e.replace_lines) {
1009
- const s = parseRef(e.replace_lines.start_anchor).line;
1010
- const en = parseRef(e.replace_lines.end_anchor).line;
1011
- editTargets.push({ start: s, end: en });
1012
- } else if (e.insert_after) {
1013
- const line = parseRef(e.insert_after.anchor).line;
1014
- editTargets.push({ start: line, end: line, insert: true });
1015
- } else if (e.replace_between) {
1016
- const s = parseRef(e.replace_between.start_anchor).line;
1017
- const en = parseRef(e.replace_between.end_anchor).line;
1018
- editTargets.push({ start: s, end: en });
1019
- }
1020
- }
1021
- for (let i = 0; i < editTargets.length; i++) {
1022
- for (let j = i + 1; j < editTargets.length; j++) {
1023
- const a = editTargets[i], b = editTargets[j];
1024
- if (a.insert || b.insert) continue;
1025
- if (a.start <= b.end && b.start <= a.end) {
1026
- throw new Error(
1027
- `OVERLAPPING_EDITS: lines ${a.start}-${a.end} and ${b.start}-${b.end} overlap. Split into separate edit_file calls.`
1028
- );
1029
- }
1030
- }
1031
- }
1032
- const sorted = anchored.map((e) => {
1033
- let sortKey;
1034
- if (e.set_line) sortKey = parseRef(e.set_line.anchor).line;
1035
- else if (e.replace_lines) sortKey = parseRef(e.replace_lines.start_anchor).line;
1036
- else if (e.insert_after) sortKey = parseRef(e.insert_after.anchor).line;
1037
- else if (e.replace_between) sortKey = parseRef(e.replace_between.start_anchor).line;
1038
- return { ...e, _k: sortKey };
1039
- }).sort((a, b) => b._k - a._k);
1394
+ const anchored = normalizeAnchoredEdits(edits);
1395
+ assertNonOverlappingTargets(collectEditTargets(anchored));
1396
+ const sorted = sortEditsForApply(anchored);
1040
1397
  const conflictIfNeeded = (reason, centerIdx, retryChecksum, details) => {
1041
1398
  if (conflictPolicy !== "conservative") {
1042
1399
  throw new Error(details);
@@ -1081,7 +1438,7 @@ function editFile(filePath, edits, opts = {}) {
1081
1438
  "base_revision_evicted",
1082
1439
  centerIdx,
1083
1440
  null,
1084
- `Base revision ${opts.baseRevision} is not available in the local revision cache.`
1441
+ `Base revision ${opts.baseRevision} is not available in the local revision cache. Recovery: re-read the file with read_file to get a fresh revision, then retry.`
1085
1442
  );
1086
1443
  }
1087
1444
  if (overlapsChangedRanges(changedRanges, actualStart, actualEnd)) {
@@ -1089,134 +1446,45 @@ function editFile(filePath, edits, opts = {}) {
1089
1446
  "overlap",
1090
1447
  centerIdx,
1091
1448
  null,
1092
- `Changes since ${opts.baseRevision} overlap edit range ${actualStart}-${actualEnd}.`
1449
+ `Changes since ${opts.baseRevision} overlap edit range ${actualStart}-${actualEnd}. Recovery: re-read lines ${actualStart}-${actualEnd} with read_file, then retry with fresh anchors and checksum.`
1093
1450
  );
1094
1451
  }
1095
1452
  autoRebased = true;
1096
1453
  return null;
1097
1454
  };
1098
- for (let _ei = 0; _ei < sorted.length; _ei++) {
1099
- const e = sorted[_ei];
1100
- try {
1101
- if (e.set_line) {
1102
- const { tag, line } = parseRef(e.set_line.anchor);
1103
- const idx = locateOrConflict({ tag, line });
1104
- if (typeof idx === "string") return idx;
1105
- const conflict = ensureRevisionContext(idx + 1, idx + 1, idx);
1106
- if (conflict) return conflict;
1107
- const txt = e.set_line.new_text;
1108
- if (!txt && txt !== 0) {
1109
- lines.splice(idx, 1);
1110
- } else {
1111
- const origLine = [lines[idx]];
1112
- const raw = sanitizeEditText(txt).split("\n");
1113
- const newLines = opts.restoreIndent ? restoreIndent(origLine, raw) : raw;
1114
- lines.splice(idx, 1, ...newLines);
1115
- }
1116
- continue;
1117
- }
1118
- if (e.insert_after) {
1119
- const { tag, line } = parseRef(e.insert_after.anchor);
1120
- const idx = locateOrConflict({ tag, line });
1121
- if (typeof idx === "string") return idx;
1122
- const conflict = ensureRevisionContext(idx + 1, idx + 1, idx);
1123
- if (conflict) return conflict;
1124
- let insertLines = sanitizeEditText(e.insert_after.text).split("\n");
1125
- if (opts.restoreIndent) insertLines = restoreIndent([lines[idx]], insertLines);
1126
- lines.splice(idx + 1, 0, ...insertLines);
1127
- continue;
1128
- }
1129
- if (e.replace_lines) {
1130
- const s = parseRef(e.replace_lines.start_anchor);
1131
- const en = parseRef(e.replace_lines.end_anchor);
1132
- const si = locateOrConflict(s);
1133
- if (typeof si === "string") return si;
1134
- const ei = locateOrConflict(en);
1135
- if (typeof ei === "string") return ei;
1136
- const actualStart = si + 1;
1137
- const actualEnd = ei + 1;
1138
- const rc = e.replace_lines.range_checksum;
1139
- if (!rc) throw new Error("range_checksum required for replace_lines. Read the range first via read_file, then pass its checksum. The checksum range must cover start-to-end anchors (inclusive).");
1140
- if (staleRevision && conflictPolicy === "conservative") {
1141
- const conflict = ensureRevisionContext(actualStart, actualEnd, si);
1142
- if (conflict) return conflict;
1143
- const baseCheck = hasBaseSnapshot ? verifyChecksumAgainstSnapshot(baseSnapshot, rc) : null;
1144
- if (!baseCheck?.ok) {
1145
- return conflictIfNeeded(
1146
- "stale_checksum",
1147
- si,
1148
- baseCheck?.actual || null,
1149
- baseCheck?.actual ? `Provided checksum ${rc} does not match base revision ${opts.baseRevision}.` : `Checksum range from ${rc} is outside the available base revision.`
1150
- );
1151
- }
1152
- } else {
1153
- const { start: csStart, end: csEnd, hex: csHex } = parseChecksum(rc);
1154
- if (csStart > actualStart || csEnd < actualEnd) {
1155
- const snip = buildErrorSnippet(origLines, actualStart - 1);
1156
- throw new Error(
1157
- `CHECKSUM_RANGE_GAP: checksum covers lines ${csStart}-${csEnd} but edit spans ${actualStart}-${actualEnd} (inclusive). Checksum range must fully contain the anchor range.
1158
-
1159
- Current content (lines ${snip.start}-${snip.end}):
1160
- ${snip.text}
1161
-
1162
- Tip: Use updated hashes above for retry.`
1163
- );
1164
- }
1165
- const actual = buildRangeChecksum(currentSnapshot, csStart, csEnd);
1166
- const actualHex = actual?.split(":")[1];
1167
- if (!actual || csHex !== actualHex) {
1168
- const details = `CHECKSUM_MISMATCH: expected ${rc}, got ${actual}. Content at lines ${csStart}-${csEnd} differs from when you read it \u2014 re-read before editing.`;
1169
- if (conflictPolicy === "conservative") {
1170
- return conflictIfNeeded("stale_checksum", csStart - 1, actual, details);
1171
- }
1172
- const snip = buildErrorSnippet(origLines, csStart - 1);
1173
- throw new Error(
1174
- `${details}
1455
+ const buildStrictChecksumMismatchError = (details, checksumStart, actual) => {
1456
+ const snip = buildErrorSnippet(origLines, checksumStart - 1);
1457
+ return new Error(
1458
+ `${details}
1175
1459
 
1176
1460
  Current content (lines ${snip.start}-${snip.end}):
1177
1461
  ${snip.text}
1178
1462
 
1179
1463
  Retry with fresh checksum ${actual}, or use set_line with hashes above.`
1180
- );
1181
- }
1182
- }
1183
- const txt = e.replace_lines.new_text;
1184
- if (!txt && txt !== 0) {
1185
- lines.splice(si, ei - si + 1);
1186
- } else {
1187
- const origRange = lines.slice(si, ei + 1);
1188
- let newLines = sanitizeEditText(txt).split("\n");
1189
- if (opts.restoreIndent) newLines = restoreIndent(origRange, newLines);
1190
- lines.splice(si, ei - si + 1, ...newLines);
1191
- }
1192
- continue;
1193
- }
1194
- if (e.replace_between) {
1195
- const boundaryMode = e.replace_between.boundary_mode || "inclusive";
1196
- if (boundaryMode !== "inclusive" && boundaryMode !== "exclusive") {
1197
- throw new Error(`BAD_INPUT: replace_between boundary_mode must be inclusive or exclusive, got ${boundaryMode}`);
1198
- }
1199
- const s = parseRef(e.replace_between.start_anchor);
1200
- const en = parseRef(e.replace_between.end_anchor);
1201
- const si = locateOrConflict(s);
1202
- if (typeof si === "string") return si;
1203
- const ei = locateOrConflict(en);
1204
- if (typeof ei === "string") return ei;
1205
- if (si > ei) {
1206
- throw new Error(`BAD_INPUT: replace_between start anchor resolves after end anchor (${si + 1} > ${ei + 1})`);
1207
- }
1208
- const targetRange = targetRangeForReplaceBetween(si, ei, boundaryMode);
1209
- const conflict = ensureRevisionContext(targetRange.start, targetRange.end, si);
1210
- if (conflict) return conflict;
1211
- const txt = e.replace_between.new_text;
1212
- let newLines = sanitizeEditText(txt ?? "").split("\n");
1213
- const sliceStart = boundaryMode === "exclusive" ? si + 1 : si;
1214
- const removeCount = boundaryMode === "exclusive" ? Math.max(0, ei - si - 1) : ei - si + 1;
1215
- const origRange = lines.slice(sliceStart, sliceStart + removeCount);
1216
- if (opts.restoreIndent && origRange.length > 0) newLines = restoreIndent(origRange, newLines);
1217
- if (txt === "" || txt === null) newLines = [];
1218
- lines.splice(sliceStart, removeCount, ...newLines);
1219
- }
1464
+ );
1465
+ };
1466
+ const editContext = {
1467
+ baseSnapshot,
1468
+ buildStrictChecksumMismatchError,
1469
+ conflictIfNeeded,
1470
+ currentSnapshot,
1471
+ ensureRevisionContext,
1472
+ hasBaseSnapshot,
1473
+ lines,
1474
+ locateOrConflict,
1475
+ opts,
1476
+ origLines,
1477
+ staleRevision
1478
+ };
1479
+ for (let _ei = 0; _ei < sorted.length; _ei++) {
1480
+ const e = sorted[_ei];
1481
+ try {
1482
+ let conflictResult = null;
1483
+ if (e.set_line) conflictResult = applySetLineEdit(e, editContext);
1484
+ else if (e.insert_after) conflictResult = applyInsertAfterEdit(e, editContext);
1485
+ else if (e.replace_lines) conflictResult = applyReplaceLinesEdit(e, editContext);
1486
+ else if (e.replace_between) conflictResult = applyReplaceBetweenEdit(e, editContext);
1487
+ if (typeof conflictResult === "string") return conflictResult;
1220
1488
  } catch (editErr) {
1221
1489
  if (sorted.length > 1) editErr.message = `Edit ${_ei + 1}/${sorted.length}: ${editErr.message}`;
1222
1490
  throw editErr;
@@ -1279,21 +1547,15 @@ ${remaps.map(({ from, to }) => `${from} -> ${to}`).join("\n")}`;
1279
1547
  msg += `
1280
1548
  Updated ${filePath} (${content.split("\n").length} lines)`;
1281
1549
  if (fullDiff && minLine <= maxLine) {
1282
- const ctxStart = Math.max(0, minLine - 6);
1550
+ const ctxStart = Math.max(0, minLine - 6) + 1;
1283
1551
  const ctxEnd = Math.min(newLinesAll.length, maxLine + 5);
1284
- const ctxLines = [];
1285
- const ctxHashes = [];
1286
- for (let i = ctxStart; i < ctxEnd; i++) {
1287
- const h = fnv1a(newLinesAll[i]);
1288
- ctxHashes.push(h);
1289
- ctxLines.push(`${lineTag(h)}.${i + 1} ${newLinesAll[i]}`);
1290
- }
1291
- const ctxCs = rangeChecksum(ctxHashes, ctxStart + 1, ctxEnd);
1292
- msg += `
1552
+ const entries = createSnapshotEntries(nextSnapshot, ctxStart, ctxEnd);
1553
+ if (entries.length > 0) {
1554
+ const block = buildEditReadyBlock({ path: real, kind: "post_edit", entries });
1555
+ msg += `
1293
1556
 
1294
- Post-edit (lines ${ctxStart + 1}-${ctxEnd}):
1295
- ${ctxLines.join("\n")}
1296
- checksum: ${ctxCs}`;
1557
+ ${serializeReadBlock(block)}`;
1558
+ }
1297
1559
  }
1298
1560
  try {
1299
1561
  const db = getGraphDB(real);
@@ -1428,21 +1690,33 @@ async function contentMode(pattern, target, opts, plain, totalLimit) {
1428
1690
  if (code === 1) return "No matches found.";
1429
1691
  if (code !== 0 && code !== null) throw new Error(`GREP_ERROR: rg exit ${code} \u2014 ${stderr.trim() || "unknown error"}`);
1430
1692
  const jsonLines = stdout.trimEnd().split("\n").filter(Boolean);
1431
- const formatted = [];
1693
+ const blocks = [];
1432
1694
  const db = getGraphDB(target);
1433
1695
  const relCache = /* @__PURE__ */ new Map();
1434
1696
  let groupFile = null;
1435
- let groupLines = [];
1697
+ let groupEntries = [];
1436
1698
  let matchCount = 0;
1437
1699
  function flushGroup() {
1438
- if (groupLines.length === 0) return;
1439
- const sorted = [...groupLines].sort((a, b) => a.lineNum - b.lineNum);
1440
- const start = sorted[0].lineNum;
1441
- const end = sorted[sorted.length - 1].lineNum;
1442
- const hashes = sorted.map((l) => l.hash32);
1443
- const cs = rangeChecksum(hashes, start, end);
1444
- formatted.push(`checksum: ${cs}`);
1445
- groupLines = [];
1700
+ if (groupEntries.length === 0) return;
1701
+ const matchLines = groupEntries.filter((entry) => entry.role === "match").map((entry) => entry.lineNumber);
1702
+ let graphScore = 0;
1703
+ let defs = 0;
1704
+ let usages = 0;
1705
+ for (const entry of groupEntries) {
1706
+ if (!entry.annotation) continue;
1707
+ const callerMatch = entry.annotation.match(/(\d+)\u2191/);
1708
+ if (callerMatch) graphScore += parseInt(callerMatch[1], 10);
1709
+ if (/\[fn|\[cls|\[mtd/.test(entry.annotation)) defs++;
1710
+ else usages++;
1711
+ }
1712
+ const summary = defs || usages ? `${defs} definition(s), ${usages} usage(s)` : null;
1713
+ blocks.push(buildEditReadyBlock({
1714
+ path: groupFile,
1715
+ kind: "search_hunk",
1716
+ entries: groupEntries,
1717
+ meta: { matchLines, graphScore, ...summary ? { summary } : {} }
1718
+ }));
1719
+ groupEntries = [];
1446
1720
  }
1447
1721
  for (const jl of jsonLines) {
1448
1722
  let msg;
@@ -1456,11 +1730,6 @@ async function contentMode(pattern, target, opts, plain, totalLimit) {
1456
1730
  flushGroup();
1457
1731
  groupFile = null;
1458
1732
  }
1459
- if (msg.type === "begin") {
1460
- if (formatted.length > 0 && formatted[formatted.length - 1] !== "") {
1461
- formatted.push("");
1462
- }
1463
- }
1464
1733
  continue;
1465
1734
  }
1466
1735
  if (msg.type !== "match" && msg.type !== "context") continue;
@@ -1482,44 +1751,46 @@ async function contentMode(pattern, target, opts, plain, totalLimit) {
1482
1751
  for (let i = 0; i < subLines.length; i++) {
1483
1752
  const ln = lineNum + i;
1484
1753
  const lineContent = subLines[i];
1485
- const hash32 = fnv1a(lineContent);
1486
- const tag = lineTag(hash32);
1487
- if (groupLines.length > 0) {
1488
- const lastLn = groupLines[groupLines.length - 1].lineNum;
1754
+ if (groupEntries.length > 0) {
1755
+ const lastLn = groupEntries[groupEntries.length - 1].lineNumber;
1489
1756
  if (ln > lastLn + 1) flushGroup();
1490
1757
  }
1491
- groupLines.push({ lineNum: ln, hash32 });
1492
1758
  const isMatch = msg.type === "match";
1493
- if (plain) {
1494
- formatted.push(`${filePath}:${ln}:${lineContent}`);
1495
- } else {
1496
- let anno = "";
1497
- if (db && isMatch) {
1498
- let rel = relCache.get(filePath);
1499
- if (rel === void 0) {
1500
- rel = getRelativePath(resolve2(filePath)) || "";
1501
- relCache.set(filePath, rel);
1502
- }
1503
- if (rel) {
1504
- const a = matchAnnotation(db, rel, ln);
1505
- if (a) anno = ` ${a}`;
1506
- }
1759
+ let annotation = "";
1760
+ if (db && isMatch) {
1761
+ let rel = relCache.get(filePath);
1762
+ if (rel === void 0) {
1763
+ rel = getRelativePath(resolve2(filePath)) || "";
1764
+ relCache.set(filePath, rel);
1765
+ }
1766
+ if (rel) {
1767
+ const a = matchAnnotation(db, rel, ln);
1768
+ if (a) annotation = a;
1507
1769
  }
1508
- const prefix = isMatch ? ">>" : " ";
1509
- formatted.push(`${filePath}:${prefix}${tag}.${ln} ${lineContent}${anno}`);
1510
1770
  }
1771
+ groupEntries.push({
1772
+ lineNumber: ln,
1773
+ text: lineContent,
1774
+ role: isMatch ? "match" : "context",
1775
+ annotation
1776
+ });
1511
1777
  }
1512
1778
  if (msg.type === "match") {
1513
1779
  matchCount++;
1514
1780
  if (totalLimit > 0 && matchCount >= totalLimit) {
1515
1781
  flushGroup();
1516
- formatted.push(`--- total_limit reached (${totalLimit}) ---`);
1517
- return formatted.join("\n");
1782
+ blocks.push(buildDiagnosticBlock({
1783
+ kind: "total_limit",
1784
+ message: `Search stopped after ${totalLimit} match event(s). Narrow the query to continue.`,
1785
+ path: String(target).replace(/\\/g, "/")
1786
+ }));
1787
+ return blocks.map((block) => block.type === "edit_ready_block" ? serializeSearchBlock(block, { plain }) : serializeDiagnosticBlock(block)).join("\n\n");
1518
1788
  }
1519
1789
  }
1520
1790
  }
1521
1791
  flushGroup();
1522
- return formatted.join("\n");
1792
+ if (db) blocks.sort((a, b) => (b.meta.graphScore || 0) - (a.meta.graphScore || 0));
1793
+ return blocks.map((block) => block.type === "edit_ready_block" ? serializeSearchBlock(block, { plain }) : serializeDiagnosticBlock(block)).join("\n\n");
1523
1794
  }
1524
1795
 
1525
1796
  // lib/outline.mjs
@@ -1583,15 +1854,6 @@ function supportedExtensions() {
1583
1854
  return Object.keys(EXTENSION_GRAMMARS);
1584
1855
  }
1585
1856
 
1586
- // ../hex-common/src/text/file-text.mjs
1587
- import { readFileSync as readFileSync2 } from "node:fs";
1588
- function normalizeSourceText(text) {
1589
- return text.replace(/\r\n/g, "\n");
1590
- }
1591
- function readUtf8Normalized(filePath) {
1592
- return normalizeSourceText(readFileSync2(filePath, "utf-8"));
1593
- }
1594
-
1595
1857
  // lib/outline.mjs
1596
1858
  var LANG_CONFIGS = {
1597
1859
  ".js": { outline: ["function_declaration", "class_declaration", "variable_declaration", "export_statement", "lexical_declaration"], skip: ["import_statement"], recurse: ["class_body"] },
@@ -1612,7 +1874,9 @@ var LANG_CONFIGS = {
1612
1874
  ".kt": { outline: ["function_declaration", "class_declaration", "object_declaration"], skip: ["import_header"], recurse: ["class_body"] },
1613
1875
  ".swift": { outline: ["function_declaration", "class_declaration", "struct_declaration", "protocol_declaration"], skip: ["import_declaration"], recurse: ["class_body"] },
1614
1876
  ".sh": { outline: ["function_definition"], skip: [], recurse: [] },
1615
- ".bash": { outline: ["function_definition"], skip: [], recurse: [] }
1877
+ ".bash": { outline: ["function_definition"], skip: [], recurse: [] },
1878
+ ".md": { outline: [], skip: [], recurse: [] },
1879
+ ".mdx": { outline: [], skip: [], recurse: [] }
1616
1880
  };
1617
1881
  function extractOutline(rootNode, config, sourceLines) {
1618
1882
  const entries = [];
@@ -1677,6 +1941,39 @@ function fallbackOutline(sourceLines) {
1677
1941
  }
1678
1942
  return entries;
1679
1943
  }
1944
+ function markdownOutline(sourceLines) {
1945
+ const entries = [];
1946
+ let activeFence = null;
1947
+ for (let index = 0; index < sourceLines.length; index++) {
1948
+ const line = sourceLines[index];
1949
+ const fenceMatch = line.match(/^\s{0,3}(```+|~~~+).*$/);
1950
+ if (fenceMatch) {
1951
+ const marker = fenceMatch[1][0];
1952
+ const length = fenceMatch[1].length;
1953
+ if (!activeFence) {
1954
+ activeFence = { marker, length };
1955
+ continue;
1956
+ }
1957
+ if (activeFence.marker === marker && length >= activeFence.length) {
1958
+ activeFence = null;
1959
+ continue;
1960
+ }
1961
+ }
1962
+ if (activeFence) continue;
1963
+ const match = line.match(/^\s{0,3}(#{1,6})\s+(.+?)\s*#*\s*$/);
1964
+ if (!match) continue;
1965
+ const level = match[1].length;
1966
+ const title = match[2].trim();
1967
+ entries.push({
1968
+ start: index + 1,
1969
+ end: index + 1,
1970
+ depth: level - 1,
1971
+ text: title.slice(0, 120),
1972
+ name: title.split(/\s+/)[0] || null
1973
+ });
1974
+ }
1975
+ return entries;
1976
+ }
1680
1977
  async function outlineFromContent(content, ext) {
1681
1978
  const config = LANG_CONFIGS[ext];
1682
1979
  const grammar = grammarForExtension(ext);
@@ -1693,7 +1990,7 @@ async function outlineFromContent(content, ext) {
1693
1990
  const tree = parser.parse(content);
1694
1991
  return extractOutline(tree.rootNode, config, sourceLines);
1695
1992
  }
1696
- function formatOutline(entries, skippedRanges, sourceLineCount, db, relFile, note = "") {
1993
+ function formatOutline(entries, skippedRanges, sourceLineCount, snapshot, db, relFile, note = "") {
1697
1994
  const lines = [];
1698
1995
  if (note) lines.push(note, "");
1699
1996
  if (skippedRanges.length > 0) {
@@ -1706,7 +2003,9 @@ function formatOutline(entries, skippedRanges, sourceLineCount, db, relFile, not
1706
2003
  const indent = " ".repeat(e.depth);
1707
2004
  const anno = db ? symbolAnnotation(db, relFile, e.name) : null;
1708
2005
  const suffix = anno ? ` ${anno}` : "";
1709
- lines.push(`${indent}${e.start}-${e.end}: ${e.text}${suffix}`);
2006
+ const tag = snapshot && snapshot.lineHashes[e.start - 1] ? lineTag(snapshot.lineHashes[e.start - 1]) : null;
2007
+ const prefix = tag ? `${tag}.` : "";
2008
+ lines.push(`${indent}${prefix}${e.start}-${e.end}: ${e.text}${suffix}`);
1710
2009
  }
1711
2010
  lines.push("");
1712
2011
  lines.push(`(${entries.length} symbols, ${sourceLineCount} source lines)`);
@@ -1719,63 +2018,136 @@ async function fileOutline(filePath) {
1719
2018
  if (!LANG_CONFIGS[ext]) {
1720
2019
  return `Outline unavailable for ${ext} files. Use read_file directly for non-code files (markdown, config, text). Supported code extensions: ${supportedExtensions().join(", ")}`;
1721
2020
  }
1722
- const content = readUtf8Normalized(real);
1723
- const result = await outlineFromContent(content, ext);
1724
- const entries = result.entries.length > 0 ? result.entries : fallbackOutline(content.split("\n"));
1725
- const note = result.entries.length > 0 || entries.length === 0 ? "" : "Fallback outline: heuristic symbols shown because parser returned no structural entries.";
2021
+ const snapshot = readSnapshot(real);
2022
+ const isMarkdown = ext === ".md" || ext === ".mdx";
2023
+ const result = isMarkdown ? null : await outlineFromContent(snapshot.content, ext);
2024
+ let entries;
2025
+ let skippedRanges = [];
2026
+ let note = "";
2027
+ if (result && result.entries.length > 0) {
2028
+ entries = result.entries;
2029
+ skippedRanges = result.skippedRanges;
2030
+ } else if (isMarkdown) {
2031
+ entries = markdownOutline(snapshot.lines);
2032
+ } else {
2033
+ entries = fallbackOutline(snapshot.lines);
2034
+ if (entries.length > 0) note = "Fallback outline: heuristic symbols shown because parser returned no structural entries.";
2035
+ }
1726
2036
  const db = getGraphDB(real);
1727
2037
  const relFile = db ? getRelativePath(real) : null;
1728
2038
  return `File: ${filePath}
1729
2039
 
1730
- ${formatOutline(entries, result.skippedRanges, content.split("\n").length, db, relFile, note)}`;
2040
+ ${formatOutline(entries, skippedRanges, snapshot.lines.length, snapshot, db, relFile, note)}`;
1731
2041
  }
1732
2042
 
1733
2043
  // lib/verify.mjs
2044
+ function parseChecksumEntry(raw) {
2045
+ try {
2046
+ return { ok: true, raw, parsed: parseChecksum(raw) };
2047
+ } catch (error) {
2048
+ return {
2049
+ ok: false,
2050
+ raw,
2051
+ error: error instanceof Error ? error.message : String(error)
2052
+ };
2053
+ }
2054
+ }
2055
+ function classifyChecksum(currentSnapshot, entry) {
2056
+ if (!entry.ok) {
2057
+ return {
2058
+ status: "INVALID",
2059
+ checksum: entry.raw,
2060
+ span: null,
2061
+ currentChecksum: null,
2062
+ reason: `invalid checksum format: ${entry.error}`
2063
+ };
2064
+ }
2065
+ const { start, end } = entry.parsed;
2066
+ if (start < 1 || end < start) {
2067
+ return {
2068
+ status: "INVALID",
2069
+ checksum: entry.raw,
2070
+ span: `${start}-${end}`,
2071
+ currentChecksum: null,
2072
+ reason: `invalid range ${start}-${end}`
2073
+ };
2074
+ }
2075
+ if (end > currentSnapshot.lines.length) {
2076
+ return {
2077
+ status: "INVALID",
2078
+ checksum: entry.raw,
2079
+ span: `${start}-${end}`,
2080
+ currentChecksum: null,
2081
+ reason: `range ${start}-${end} exceeds file length ${currentSnapshot.lines.length}`
2082
+ };
2083
+ }
2084
+ const currentChecksum = buildRangeChecksum(currentSnapshot, start, end);
2085
+ if (currentChecksum === entry.raw) {
2086
+ return {
2087
+ status: "VALID",
2088
+ checksum: entry.raw,
2089
+ span: `${start}-${end}`,
2090
+ currentChecksum,
2091
+ reason: null
2092
+ };
2093
+ }
2094
+ return {
2095
+ status: "STALE",
2096
+ checksum: entry.raw,
2097
+ span: `${start}-${end}`,
2098
+ currentChecksum,
2099
+ reason: `content changed since the checksum was captured. Recovery: read_file ranges=["${start}-${end}"] to get fresh content.`
2100
+ };
2101
+ }
2102
+ function summarizeStatuses(entries) {
2103
+ return entries.reduce((acc, entry) => {
2104
+ const key = entry.status.toLowerCase();
2105
+ acc[key] += 1;
2106
+ return acc;
2107
+ }, { valid: 0, stale: 0, invalid: 0 });
2108
+ }
2109
+ function renderEntry(entry) {
2110
+ const base = `checksum: ${entry.checksum}`;
2111
+ if (entry.status === "VALID") {
2112
+ return `${entry.status} ${entry.span} ${base}`;
2113
+ }
2114
+ if (entry.status === "STALE") {
2115
+ return `${entry.status} ${entry.span} ${base} current=${entry.currentChecksum} | re-read to refresh`;
2116
+ }
2117
+ return `${entry.status}${entry.span ? ` ${entry.span}` : ""} ${base} reason=${entry.reason}`;
2118
+ }
1734
2119
  function verifyChecksums(filePath, checksums, opts = {}) {
1735
2120
  filePath = normalizePath(filePath);
1736
2121
  const real = validatePath(filePath);
1737
- const current = readSnapshot(real);
2122
+ const currentSnapshot = readSnapshot(real);
1738
2123
  const baseSnapshot = opts.baseRevision ? getSnapshotByRevision(opts.baseRevision) : null;
1739
- const results = [];
1740
- let allValid = true;
1741
- for (const cs of checksums) {
1742
- const parsed = parseChecksum(cs);
1743
- if (parsed.start < 1 || parsed.end > current.lines.length) {
1744
- results.push(`${cs}: INVALID (range ${parsed.start}-${parsed.end} exceeds file length ${current.lines.length})`);
1745
- allValid = false;
1746
- continue;
1747
- }
1748
- const actual = buildRangeChecksum(current, parsed.start, parsed.end);
1749
- const currentHex = actual.split(":")[1];
1750
- if (currentHex === parsed.hex) {
1751
- results.push(`${cs}: valid`);
2124
+ const hasBaseSnapshot = !!(baseSnapshot && baseSnapshot.path === real);
2125
+ const parsedEntries = (checksums || []).map(parseChecksumEntry);
2126
+ const results = parsedEntries.map((entry) => classifyChecksum(currentSnapshot, entry));
2127
+ const summary = summarizeStatuses(results);
2128
+ const status = summary.invalid > 0 ? "INVALID" : summary.stale > 0 ? "STALE" : "OK";
2129
+ const lines = [
2130
+ `status: ${status}`,
2131
+ `revision: ${currentSnapshot.revision}`,
2132
+ `file: ${currentSnapshot.fileChecksum}`,
2133
+ `summary: valid=${summary.valid} stale=${summary.stale} invalid=${summary.invalid}`
2134
+ ];
2135
+ if (opts.baseRevision) {
2136
+ lines.push(`base_revision: ${opts.baseRevision}`);
2137
+ if (hasBaseSnapshot) {
2138
+ lines.push(`changed_ranges: ${describeChangedRanges(computeChangedRanges(baseSnapshot.lines, currentSnapshot.lines))}`);
1752
2139
  } else {
1753
- const staleBits = [`${cs}: STALE \u2192 current: ${actual}`];
1754
- if (baseSnapshot?.path === real) {
1755
- const changedRanges = computeChangedRanges(baseSnapshot.lines, current.lines);
1756
- staleBits.push(`revision: ${current.revision}`);
1757
- staleBits.push(`changed_ranges: ${describeChangedRanges(changedRanges)}`);
1758
- } else if (opts.baseRevision) {
1759
- staleBits.push(`revision: ${current.revision}`);
1760
- staleBits.push(`changed_ranges: unavailable (base revision evicted)`);
1761
- }
1762
- results.push(staleBits.join("\n"));
1763
- allValid = false;
2140
+ lines.push("base_revision_status: evicted");
1764
2141
  }
1765
2142
  }
1766
- if (allValid && checksums.length > 0) {
1767
- let msg = `All ${checksums.length} checksum(s) valid for ${filePath}`;
1768
- msg += `
1769
- revision: ${current.revision}`;
1770
- msg += `
1771
- file: ${current.fileChecksum}`;
1772
- return msg;
2143
+ if (results.length > 0) {
2144
+ lines.push("", ...results.map(renderEntry));
1773
2145
  }
1774
- return results.join("\n");
2146
+ return lines.join("\n");
1775
2147
  }
1776
2148
 
1777
2149
  // lib/tree.mjs
1778
- import { readdirSync as readdirSync2, readFileSync as readFileSync3, statSync as statSync6, existsSync as existsSync4 } from "node:fs";
2150
+ import { readdirSync as readdirSync2, readFileSync as readFileSync2, statSync as statSync6, existsSync as existsSync4 } from "node:fs";
1779
2151
  import { resolve as resolve4, basename, join as join4, relative as relative2 } from "node:path";
1780
2152
  import ignore from "ignore";
1781
2153
  var SKIP_DIRS = /* @__PURE__ */ new Set([
@@ -1796,7 +2168,7 @@ function loadGitignore(rootDir) {
1796
2168
  const gi = join4(rootDir, ".gitignore");
1797
2169
  if (!existsSync4(gi)) return null;
1798
2170
  try {
1799
- const content = readFileSync3(gi, "utf-8");
2171
+ const content = readFileSync2(gi, "utf-8");
1800
2172
  return ignore().add(content);
1801
2173
  } catch {
1802
2174
  return null;
@@ -2042,7 +2414,7 @@ function fileInfo(filePath) {
2042
2414
  }
2043
2415
 
2044
2416
  // lib/setup.mjs
2045
- import { readFileSync as readFileSync4, writeFileSync as writeFileSync2, existsSync as existsSync5, mkdirSync, copyFileSync } from "node:fs";
2417
+ import { readFileSync as readFileSync3, writeFileSync as writeFileSync2, existsSync as existsSync5, mkdirSync, copyFileSync } from "node:fs";
2046
2418
  import { resolve as resolve6, dirname as dirname3, join as join5 } from "node:path";
2047
2419
  import { fileURLToPath } from "node:url";
2048
2420
  import { homedir } from "node:os";
@@ -2070,7 +2442,7 @@ var CLAUDE_HOOKS = {
2070
2442
  };
2071
2443
  function readJson(filePath) {
2072
2444
  if (!existsSync5(filePath)) return null;
2073
- return JSON.parse(readFileSync4(filePath, "utf-8"));
2445
+ return JSON.parse(readFileSync3(filePath, "utf-8"));
2074
2446
  }
2075
2447
  function writeJson(filePath, data) {
2076
2448
  mkdirSync(dirname3(filePath), { recursive: true });
@@ -2149,7 +2521,7 @@ function installOutputStyle() {
2149
2521
  const source = resolve6(dirname3(fileURLToPath(import.meta.url)), "..", "output-style.md");
2150
2522
  const target = resolve6(homedir(), ".claude", "output-styles", "hex-line.md");
2151
2523
  mkdirSync(dirname3(target), { recursive: true });
2152
- writeFileSync2(target, readFileSync4(source, "utf-8"), "utf-8");
2524
+ writeFileSync2(target, readFileSync3(source, "utf-8"), "utf-8");
2153
2525
  const userSettings = resolve6(homedir(), ".claude/settings.json");
2154
2526
  const config = readJson(userSettings) || {};
2155
2527
  const prev = config.outputStyle;
@@ -2465,7 +2837,7 @@ OUTPUT_CAPPED: Output exceeded ${MAX_BULK_OUTPUT_CHARS} chars.`;
2465
2837
  }
2466
2838
 
2467
2839
  // server.mjs
2468
- var version = true ? "1.5.0" : (await null).createRequire(import.meta.url)("./package.json").version;
2840
+ var version = true ? "1.7.0" : (await null).createRequire(import.meta.url)("./package.json").version;
2469
2841
  var { server, StdioServerTransport } = await createServerRuntime({
2470
2842
  name: "hex-line-mcp",
2471
2843
  version
@@ -2488,27 +2860,9 @@ function parseReadRanges(rawRanges) {
2488
2860
  }
2489
2861
  return parsed;
2490
2862
  }
2491
- function coerceEdit(e) {
2492
- if (!e || typeof e !== "object" || Array.isArray(e)) return e;
2493
- if (e.set_line || e.replace_lines || e.insert_after || e.replace_between || e.replace) return e;
2494
- if (e.anchor && !e.start_anchor && !e.end_anchor && !e.boundary_mode && !e.range_checksum) {
2495
- const raw = e.new_text ?? e.updated_lines ?? e.content ?? e.line;
2496
- if (raw !== void 0) {
2497
- const text = Array.isArray(raw) ? raw.join("\n") : raw;
2498
- return { set_line: { anchor: e.anchor, new_text: text } };
2499
- }
2500
- }
2501
- if (e.start_anchor && e.end_anchor && e.boundary_mode && e.new_text !== void 0) {
2502
- return { replace_between: { start_anchor: e.start_anchor, end_anchor: e.end_anchor, new_text: e.new_text, boundary_mode: e.boundary_mode } };
2503
- }
2504
- if (e.start_anchor && e.end_anchor && e.new_text !== void 0) {
2505
- return { replace_lines: { start_anchor: e.start_anchor, end_anchor: e.end_anchor, new_text: e.new_text, ...e.range_checksum ? { range_checksum: e.range_checksum } : {} } };
2506
- }
2507
- return e;
2508
- }
2509
2863
  server.registerTool("read_file", {
2510
2864
  title: "Read File",
2511
- description: "Read file lines with hashes, checksums, and revision metadata.",
2865
+ description: "Read file with hash-annotated lines, checksums, and revision metadata. Default: edit-ready output. Use plain:true for non-edit workflows.",
2512
2866
  inputSchema: z2.object({
2513
2867
  path: z2.string().optional().describe("File or directory path"),
2514
2868
  paths: z2.array(z2.string()).optional().describe("Array of file paths to read (batch mode)"),
@@ -2548,7 +2902,7 @@ server.registerTool("edit_file", {
2548
2902
  inputSchema: z2.object({
2549
2903
  path: z2.string().describe("File to edit"),
2550
2904
  edits: z2.union([z2.string(), z2.array(z2.any())]).describe(
2551
- 'JSON array. Types: set_line, replace_lines, insert_after, replace_between.\n[{"set_line":{"anchor":"ab.12","new_text":"x"}}]\n[{"replace_lines":{"start_anchor":"ab.10","end_anchor":"cd.15","new_text":"x","range_checksum":"10-15:a1b2"}}]\n[{"replace_between":{"start_anchor":"ab.10","end_anchor":"cd.40","new_text":"x","boundary_mode":"inclusive"}}]\n[{"insert_after":{"anchor":"ab.20","text":"x"}}]'
2905
+ 'JSON array of canonical edits.\n[{"set_line":{"anchor":"ab.12","new_text":"x"}}]\n[{"replace_lines":{"start_anchor":"ab.10","end_anchor":"cd.15","new_text":"x","range_checksum":"10-15:a1b2"}}]\n[{"insert_after":{"anchor":"ab.20","text":"x"}}]\n[{"replace_between":{"start_anchor":"ab.10","end_anchor":"cd.40","new_text":"x","boundary_mode":"inclusive"}}]'
2552
2906
  ),
2553
2907
  dry_run: flexBool().describe("Preview changes without writing"),
2554
2908
  restore_indent: flexBool().describe("Auto-fix indentation to match anchor (default: false)"),
@@ -2566,11 +2920,10 @@ server.registerTool("edit_file", {
2566
2920
  throw new Error('edits: invalid JSON. Expected: [{"set_line":{"anchor":"xx.N","new_text":"..."}}]');
2567
2921
  }
2568
2922
  if (!Array.isArray(parsed) || !parsed.length) throw new Error("Edits: non-empty JSON array required");
2569
- const normalized = parsed.map(coerceEdit);
2570
2923
  return {
2571
2924
  content: [{
2572
2925
  type: "text",
2573
- text: editFile(p, normalized, {
2926
+ text: editFile(p, parsed, {
2574
2927
  dryRun: dry_run,
2575
2928
  restoreIndent: restore_indent,
2576
2929
  baseRevision: base_revision,
@@ -2603,7 +2956,7 @@ server.registerTool("write_file", {
2603
2956
  });
2604
2957
  server.registerTool("grep_search", {
2605
2958
  title: "Search Files",
2606
- description: "Search file contents with ripgrep and return edit-ready matches.",
2959
+ description: "Search file contents with ripgrep. Default: edit-ready blocks with hashes and checksums. Use output:'files' or 'count' for non-edit workflows. With graph DB: results ranked by importance.",
2607
2960
  inputSchema: z2.object({
2608
2961
  pattern: z2.string().describe("Search pattern (regex by default, literal if literal:true)"),
2609
2962
  path: z2.string().optional().describe("Search dir/file (default: cwd)"),
@@ -2664,7 +3017,7 @@ server.registerTool("grep_search", {
2664
3017
  });
2665
3018
  server.registerTool("outline", {
2666
3019
  title: "File Outline",
2667
- description: "AST-based structural outline: functions, classes, interfaces with line ranges. Use before reading large code files. Not for .md/.json/.yaml.",
3020
+ description: "AST-based structural outline with hash anchors for direct edit_file usage. Supports code files (JS/TS/Python/Go/Rust/Java/C/C++/C#/Ruby/PHP/Kotlin/Swift/Bash) and markdown headings (.md/.mdx, fence-aware).",
2668
3021
  inputSchema: z2.object({
2669
3022
  path: z2.string().describe("Source file path")
2670
3023
  }),