@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/README.md +38 -30
- package/dist/hook.mjs +45 -13
- package/dist/server.mjs +769 -416
- package/output-style.md +2 -0
- package/package.json +4 -4
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/
|
|
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 &&
|
|
663
|
+
return (ranges || []).some((range) => range.start <= endLine && range.end >= startLine);
|
|
662
664
|
}
|
|
663
665
|
function buildRangeChecksum(snapshot, startLine, endLine) {
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
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 =
|
|
703
|
-
const
|
|
704
|
-
|
|
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
|
-
|
|
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
|
-
|
|
900
|
+
requestedRanges = [{ start: startLine, end: Math.min(total, startLine - 1 + maxLines) }];
|
|
718
901
|
}
|
|
719
|
-
const
|
|
902
|
+
const blocks = [];
|
|
903
|
+
const diagnostics = [];
|
|
904
|
+
let remainingChars = MAX_OUTPUT_CHARS;
|
|
720
905
|
let cappedAtLine = 0;
|
|
721
|
-
for (const
|
|
722
|
-
const
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
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, ${
|
|
749
|
-
if (
|
|
750
|
-
const
|
|
751
|
-
if (
|
|
752
|
-
meta += `, showing ${
|
|
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 (
|
|
755
|
-
meta += `, ${total -
|
|
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
|
-
|
|
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
|
-
${
|
|
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
|
|
975
|
-
|
|
976
|
-
|
|
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
|
-
|
|
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
|
-
|
|
999
|
-
|
|
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
|
-
|
|
1099
|
-
const
|
|
1100
|
-
|
|
1101
|
-
|
|
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
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
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
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
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
|
-
|
|
1295
|
-
|
|
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
|
|
1693
|
+
const blocks = [];
|
|
1432
1694
|
const db = getGraphDB(target);
|
|
1433
1695
|
const relCache = /* @__PURE__ */ new Map();
|
|
1434
1696
|
let groupFile = null;
|
|
1435
|
-
let
|
|
1697
|
+
let groupEntries = [];
|
|
1436
1698
|
let matchCount = 0;
|
|
1437
1699
|
function flushGroup() {
|
|
1438
|
-
if (
|
|
1439
|
-
const
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
const
|
|
1444
|
-
|
|
1445
|
-
|
|
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
|
-
|
|
1486
|
-
|
|
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
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
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
|
-
|
|
1517
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1723
|
-
const
|
|
1724
|
-
const
|
|
1725
|
-
|
|
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,
|
|
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
|
|
2122
|
+
const currentSnapshot = readSnapshot(real);
|
|
1738
2123
|
const baseSnapshot = opts.baseRevision ? getSnapshotByRevision(opts.baseRevision) : null;
|
|
1739
|
-
const
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
}
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
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
|
-
|
|
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 (
|
|
1767
|
-
|
|
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
|
|
2146
|
+
return lines.join("\n");
|
|
1775
2147
|
}
|
|
1776
2148
|
|
|
1777
2149
|
// lib/tree.mjs
|
|
1778
|
-
import { readdirSync as readdirSync2, readFileSync as
|
|
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 =
|
|
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
|
|
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(
|
|
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,
|
|
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.
|
|
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
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
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
|
}),
|