@levnikolaevich/hex-line-mcp 1.4.0 → 1.6.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 +51 -37
- package/dist/hook.mjs +63 -22
- package/dist/server.mjs +895 -442
- package/output-style.md +23 -21
- 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,17 +660,223 @@ 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
|
|
671
800
|
var DEFAULT_LIMIT = 2e3;
|
|
801
|
+
function parseRangeEntry(entry, total) {
|
|
802
|
+
if (typeof entry === "string") {
|
|
803
|
+
const match = entry.trim().match(/^(\d+)(?:-(\d*)?)?$/);
|
|
804
|
+
if (!match) throw new Error(`Invalid range "${entry}". Use "10", "10-25", or "10-"`);
|
|
805
|
+
const start2 = Number(match[1]);
|
|
806
|
+
const end2 = match[2] === void 0 || match[2] === "" ? total : Number(match[2]);
|
|
807
|
+
return { start: start2, end: end2 };
|
|
808
|
+
}
|
|
809
|
+
if (!entry || typeof entry !== "object") {
|
|
810
|
+
throw new Error("ranges entries must be strings or {start,end} objects");
|
|
811
|
+
}
|
|
812
|
+
const start = Number(entry.start ?? 1);
|
|
813
|
+
const end = entry.end === void 0 || entry.end === null ? total : Number(entry.end);
|
|
814
|
+
if (!Number.isFinite(start) || !Number.isFinite(end)) {
|
|
815
|
+
throw new Error("ranges entries must contain numeric start/end values");
|
|
816
|
+
}
|
|
817
|
+
return { start, end };
|
|
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
|
+
}
|
|
672
880
|
function readFile2(filePath, opts = {}) {
|
|
673
881
|
filePath = normalizePath(filePath);
|
|
674
882
|
const real = validatePath(filePath);
|
|
@@ -681,62 +889,55 @@ function readFile2(filePath, opts = {}) {
|
|
|
681
889
|
${text}
|
|
682
890
|
\`\`\``;
|
|
683
891
|
}
|
|
684
|
-
const snapshot =
|
|
685
|
-
const
|
|
686
|
-
|
|
687
|
-
let ranges;
|
|
892
|
+
const snapshot = readSnapshot(real);
|
|
893
|
+
const total = snapshot.lines.length;
|
|
894
|
+
let requestedRanges;
|
|
688
895
|
if (opts.ranges && opts.ranges.length > 0) {
|
|
689
|
-
|
|
690
|
-
start: Math.max(1, r.start || 1),
|
|
691
|
-
end: Math.min(total, r.end || total)
|
|
692
|
-
}));
|
|
896
|
+
requestedRanges = opts.ranges;
|
|
693
897
|
} else {
|
|
694
898
|
const startLine = Math.max(1, opts.offset || 1);
|
|
695
899
|
const maxLines = opts.limit && opts.limit > 0 ? opts.limit : DEFAULT_LIMIT;
|
|
696
|
-
|
|
900
|
+
requestedRanges = [{ start: startLine, end: Math.min(total, startLine - 1 + maxLines) }];
|
|
697
901
|
}
|
|
698
|
-
const
|
|
902
|
+
const blocks = [];
|
|
903
|
+
const diagnostics = [];
|
|
904
|
+
let remainingChars = MAX_OUTPUT_CHARS;
|
|
699
905
|
let cappedAtLine = 0;
|
|
700
|
-
for (const
|
|
701
|
-
const
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
for (let i = 0; i < selected.length; i++) {
|
|
706
|
-
const line = selected[i];
|
|
707
|
-
const num = range.start + i;
|
|
708
|
-
const hash32 = fnv1a(line);
|
|
709
|
-
const entry = opts.plain ? `${num}|${line}` : `${lineTag(hash32)}.${num} ${line}`;
|
|
710
|
-
if (charCount + entry.length > MAX_OUTPUT_CHARS && formatted.length > 0) {
|
|
711
|
-
cappedAtLine = num;
|
|
712
|
-
break;
|
|
713
|
-
}
|
|
714
|
-
lineHashes.push(hash32);
|
|
715
|
-
formatted.push(entry);
|
|
716
|
-
charCount += entry.length + 1;
|
|
717
|
-
}
|
|
718
|
-
const actualEnd = formatted.length > 0 ? range.start + formatted.length - 1 : range.start;
|
|
719
|
-
range.end = actualEnd;
|
|
720
|
-
parts.push(formatted.join("\n"));
|
|
721
|
-
const cs = rangeChecksum(lineHashes, range.start, actualEnd);
|
|
722
|
-
parts.push(`
|
|
723
|
-
checksum: ${cs}`);
|
|
724
|
-
if (cappedAtLine) break;
|
|
725
|
-
}
|
|
726
|
-
const sizeKB = (stat.size / 1024).toFixed(1);
|
|
727
|
-
const mtime = stat.mtime;
|
|
728
|
-
const ago = relativeTime(mtime);
|
|
729
|
-
let header = `File: ${filePath} (${total} lines, ${sizeKB}KB, ${ago})`;
|
|
730
|
-
if (ranges.length === 1) {
|
|
731
|
-
const r = ranges[0];
|
|
732
|
-
if (r.start > 1 || r.end < total) {
|
|
733
|
-
header += ` [showing ${r.start}-${r.end}]`;
|
|
906
|
+
for (const requested of requestedRanges) {
|
|
907
|
+
const normalized = normalizeRange(requested, total);
|
|
908
|
+
if (normalized.type === "diagnostic_block") {
|
|
909
|
+
diagnostics.push(normalized);
|
|
910
|
+
continue;
|
|
734
911
|
}
|
|
735
|
-
|
|
736
|
-
|
|
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;
|
|
737
926
|
}
|
|
738
927
|
}
|
|
739
|
-
const
|
|
928
|
+
const sizeText = formatSize(stat.size);
|
|
929
|
+
const ago = relativeTime(stat.mtime);
|
|
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}`;
|
|
935
|
+
}
|
|
936
|
+
if (block.endLine < total) {
|
|
937
|
+
meta += `, ${total - block.endLine} more below`;
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
const db = opts.includeGraph ? getGraphDB(real) : null;
|
|
740
941
|
const relFile = db ? getRelativePath(real) : null;
|
|
741
942
|
let graphLine = "";
|
|
742
943
|
if (db && relFile) {
|
|
@@ -750,29 +951,112 @@ checksum: ${cs}`);
|
|
|
750
951
|
Graph: ${items.join(" | ")}`;
|
|
751
952
|
}
|
|
752
953
|
}
|
|
753
|
-
|
|
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}
|
|
967
|
+
meta: ${meta}
|
|
754
968
|
revision: ${snapshot.revision}
|
|
755
969
|
file: ${snapshot.fileChecksum}
|
|
756
970
|
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
\`\`\``;
|
|
760
|
-
if (total > 200 && (!opts.offset || opts.offset <= 1) && !cappedAtLine) {
|
|
761
|
-
result += `
|
|
971
|
+
${serializedBlocks.join("\n\n")}`.trim();
|
|
972
|
+
}
|
|
762
973
|
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
result += `
|
|
974
|
+
// lib/edit.mjs
|
|
975
|
+
import { statSync as statSync5, writeFileSync } from "node:fs";
|
|
976
|
+
import { diffLines as diffLines2 } from "diff";
|
|
767
977
|
|
|
768
|
-
|
|
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
|
+
}
|
|
769
1035
|
}
|
|
770
|
-
|
|
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 };
|
|
771
1057
|
}
|
|
772
1058
|
|
|
773
1059
|
// lib/edit.mjs
|
|
774
|
-
import { statSync as statSync5, writeFileSync } from "node:fs";
|
|
775
|
-
import { diffLines as diffLines2 } from "diff";
|
|
776
1060
|
function restoreIndent(origLines, newLines) {
|
|
777
1061
|
if (!origLines.length || !newLines.length) return newLines;
|
|
778
1062
|
const origIndent = origLines[0].match(/^\s*/)[0];
|
|
@@ -794,6 +1078,26 @@ function buildErrorSnippet(lines, centerIdx, radius = 5) {
|
|
|
794
1078
|
}).join("\n");
|
|
795
1079
|
return { start: start + 1, end, text };
|
|
796
1080
|
}
|
|
1081
|
+
function stripAnchorOrDiffPrefix(line) {
|
|
1082
|
+
let next = line;
|
|
1083
|
+
next = next.replace(/^\s*(?:>>| )?[a-z2-7]{2}\.\d+\t/, "");
|
|
1084
|
+
next = next.replace(/^.+:(?:>>| )[a-z2-7]{2}\.\d+\t/, "");
|
|
1085
|
+
next = next.replace(/^[ +-]\d+\|\s?/, "");
|
|
1086
|
+
return next;
|
|
1087
|
+
}
|
|
1088
|
+
function sanitizeEditText(text) {
|
|
1089
|
+
const original = String(text ?? "");
|
|
1090
|
+
const hadTrailingNewline = original.endsWith("\n");
|
|
1091
|
+
let lines = original.split("\n");
|
|
1092
|
+
const nonEmpty = lines.filter((line) => line.length > 0);
|
|
1093
|
+
if (nonEmpty.length > 0 && nonEmpty.every((line) => /^\+(?!\+)/.test(line))) {
|
|
1094
|
+
lines = lines.map((line) => line.startsWith("+") && !line.startsWith("++") ? line.slice(1) : line);
|
|
1095
|
+
}
|
|
1096
|
+
lines = lines.map(stripAnchorOrDiffPrefix);
|
|
1097
|
+
let cleaned = lines.join("\n");
|
|
1098
|
+
if (hadTrailingNewline && !cleaned.endsWith("\n")) cleaned += "\n";
|
|
1099
|
+
return cleaned;
|
|
1100
|
+
}
|
|
797
1101
|
function findLine(lines, lineNum, expectedTag, hashIndex) {
|
|
798
1102
|
const idx = lineNum - 1;
|
|
799
1103
|
if (idx < 0 || idx >= lines.length) {
|
|
@@ -909,6 +1213,7 @@ function buildConflictMessage({
|
|
|
909
1213
|
centerIdx,
|
|
910
1214
|
changedRanges,
|
|
911
1215
|
retryChecksum,
|
|
1216
|
+
remaps,
|
|
912
1217
|
details
|
|
913
1218
|
}) {
|
|
914
1219
|
const safeCenter = Math.max(0, Math.min(lines.length - 1, centerIdx));
|
|
@@ -921,6 +1226,9 @@ file: ${fileChecksum}`;
|
|
|
921
1226
|
changed_ranges: ${describeChangedRanges(changedRanges)}`;
|
|
922
1227
|
if (retryChecksum) msg += `
|
|
923
1228
|
retry_checksum: ${retryChecksum}`;
|
|
1229
|
+
if (remaps?.length) msg += `
|
|
1230
|
+
remapped_refs:
|
|
1231
|
+
${remaps.map(({ from, to }) => `${from} -> ${to}`).join("\n")}`;
|
|
924
1232
|
msg += `
|
|
925
1233
|
|
|
926
1234
|
${details}
|
|
@@ -934,11 +1242,137 @@ Tip: Retry from the fresh local snippet above.`;
|
|
|
934
1242
|
path: ${filePath}`;
|
|
935
1243
|
return msg;
|
|
936
1244
|
}
|
|
937
|
-
function
|
|
938
|
-
|
|
939
|
-
|
|
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;
|
|
940
1256
|
}
|
|
941
|
-
|
|
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;
|
|
942
1376
|
}
|
|
943
1377
|
function editFile(filePath, edits, opts = {}) {
|
|
944
1378
|
filePath = normalizePath(filePath);
|
|
@@ -955,49 +1389,11 @@ function editFile(filePath, edits, opts = {}) {
|
|
|
955
1389
|
const hadTrailingNewline = original.endsWith("\n");
|
|
956
1390
|
const hashIndex = currentSnapshot.uniqueTagIndex;
|
|
957
1391
|
let autoRebased = false;
|
|
958
|
-
const
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
}
|
|
964
|
-
const editTargets = [];
|
|
965
|
-
for (const e of anchored) {
|
|
966
|
-
if (e.set_line) {
|
|
967
|
-
const line = parseRef(e.set_line.anchor).line;
|
|
968
|
-
editTargets.push({ start: line, end: line });
|
|
969
|
-
} else if (e.replace_lines) {
|
|
970
|
-
const s = parseRef(e.replace_lines.start_anchor).line;
|
|
971
|
-
const en = parseRef(e.replace_lines.end_anchor).line;
|
|
972
|
-
editTargets.push({ start: s, end: en });
|
|
973
|
-
} else if (e.insert_after) {
|
|
974
|
-
const line = parseRef(e.insert_after.anchor).line;
|
|
975
|
-
editTargets.push({ start: line, end: line, insert: true });
|
|
976
|
-
} else if (e.replace_between) {
|
|
977
|
-
const s = parseRef(e.replace_between.start_anchor).line;
|
|
978
|
-
const en = parseRef(e.replace_between.end_anchor).line;
|
|
979
|
-
editTargets.push({ start: s, end: en });
|
|
980
|
-
}
|
|
981
|
-
}
|
|
982
|
-
for (let i = 0; i < editTargets.length; i++) {
|
|
983
|
-
for (let j = i + 1; j < editTargets.length; j++) {
|
|
984
|
-
const a = editTargets[i], b = editTargets[j];
|
|
985
|
-
if (a.insert || b.insert) continue;
|
|
986
|
-
if (a.start <= b.end && b.start <= a.end) {
|
|
987
|
-
throw new Error(
|
|
988
|
-
`OVERLAPPING_EDITS: lines ${a.start}-${a.end} and ${b.start}-${b.end} overlap. Split into separate edit_file calls.`
|
|
989
|
-
);
|
|
990
|
-
}
|
|
991
|
-
}
|
|
992
|
-
}
|
|
993
|
-
const sorted = anchored.map((e) => {
|
|
994
|
-
let sortKey;
|
|
995
|
-
if (e.set_line) sortKey = parseRef(e.set_line.anchor).line;
|
|
996
|
-
else if (e.replace_lines) sortKey = parseRef(e.replace_lines.start_anchor).line;
|
|
997
|
-
else if (e.insert_after) sortKey = parseRef(e.insert_after.anchor).line;
|
|
998
|
-
else if (e.replace_between) sortKey = parseRef(e.replace_between.start_anchor).line;
|
|
999
|
-
return { ...e, _k: sortKey };
|
|
1000
|
-
}).sort((a, b) => b._k - a._k);
|
|
1392
|
+
const remaps = [];
|
|
1393
|
+
const remapKeys = /* @__PURE__ */ new Set();
|
|
1394
|
+
const anchored = normalizeAnchoredEdits(edits);
|
|
1395
|
+
assertNonOverlappingTargets(collectEditTargets(anchored));
|
|
1396
|
+
const sorted = sortEditsForApply(anchored);
|
|
1001
1397
|
const conflictIfNeeded = (reason, centerIdx, retryChecksum, details) => {
|
|
1002
1398
|
if (conflictPolicy !== "conservative") {
|
|
1003
1399
|
throw new Error(details);
|
|
@@ -1011,12 +1407,24 @@ function editFile(filePath, edits, opts = {}) {
|
|
|
1011
1407
|
centerIdx,
|
|
1012
1408
|
changedRanges: staleRevision && hasBaseSnapshot ? changedRanges : null,
|
|
1013
1409
|
retryChecksum,
|
|
1410
|
+
remaps,
|
|
1014
1411
|
details
|
|
1015
1412
|
});
|
|
1016
1413
|
};
|
|
1414
|
+
const trackRemap = (ref, idx) => {
|
|
1415
|
+
const actualRef = `${lineTag(fnv1a(lines[idx]))}.${idx + 1}`;
|
|
1416
|
+
const expectedRef = `${ref.tag}.${ref.line}`;
|
|
1417
|
+
if (actualRef === expectedRef) return;
|
|
1418
|
+
const key = `${expectedRef}->${actualRef}`;
|
|
1419
|
+
if (remapKeys.has(key)) return;
|
|
1420
|
+
remapKeys.add(key);
|
|
1421
|
+
remaps.push({ from: expectedRef, to: actualRef });
|
|
1422
|
+
};
|
|
1017
1423
|
const locateOrConflict = (ref, reason = "stale_anchor") => {
|
|
1018
1424
|
try {
|
|
1019
|
-
|
|
1425
|
+
const idx = findLine(lines, ref.line, ref.tag, hashIndex);
|
|
1426
|
+
trackRemap(ref, idx);
|
|
1427
|
+
return idx;
|
|
1020
1428
|
} catch (e) {
|
|
1021
1429
|
if (conflictPolicy !== "conservative" || !staleRevision) throw e;
|
|
1022
1430
|
const centerIdx = Math.max(0, Math.min(lines.length - 1, ref.line - 1));
|
|
@@ -1030,7 +1438,7 @@ function editFile(filePath, edits, opts = {}) {
|
|
|
1030
1438
|
"base_revision_evicted",
|
|
1031
1439
|
centerIdx,
|
|
1032
1440
|
null,
|
|
1033
|
-
`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.`
|
|
1034
1442
|
);
|
|
1035
1443
|
}
|
|
1036
1444
|
if (overlapsChangedRanges(changedRanges, actualStart, actualEnd)) {
|
|
@@ -1038,134 +1446,45 @@ function editFile(filePath, edits, opts = {}) {
|
|
|
1038
1446
|
"overlap",
|
|
1039
1447
|
centerIdx,
|
|
1040
1448
|
null,
|
|
1041
|
-
`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.`
|
|
1042
1450
|
);
|
|
1043
1451
|
}
|
|
1044
1452
|
autoRebased = true;
|
|
1045
1453
|
return null;
|
|
1046
1454
|
};
|
|
1047
|
-
|
|
1048
|
-
const
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
const { tag, line } = parseRef(e.set_line.anchor);
|
|
1052
|
-
const idx = locateOrConflict({ tag, line });
|
|
1053
|
-
if (typeof idx === "string") return idx;
|
|
1054
|
-
const conflict = ensureRevisionContext(idx + 1, idx + 1, idx);
|
|
1055
|
-
if (conflict) return conflict;
|
|
1056
|
-
const txt = e.set_line.new_text;
|
|
1057
|
-
if (!txt && txt !== 0) {
|
|
1058
|
-
lines.splice(idx, 1);
|
|
1059
|
-
} else {
|
|
1060
|
-
const origLine = [lines[idx]];
|
|
1061
|
-
const raw = String(txt).split("\n");
|
|
1062
|
-
const newLines = opts.restoreIndent ? restoreIndent(origLine, raw) : raw;
|
|
1063
|
-
lines.splice(idx, 1, ...newLines);
|
|
1064
|
-
}
|
|
1065
|
-
continue;
|
|
1066
|
-
}
|
|
1067
|
-
if (e.insert_after) {
|
|
1068
|
-
const { tag, line } = parseRef(e.insert_after.anchor);
|
|
1069
|
-
const idx = locateOrConflict({ tag, line });
|
|
1070
|
-
if (typeof idx === "string") return idx;
|
|
1071
|
-
const conflict = ensureRevisionContext(idx + 1, idx + 1, idx);
|
|
1072
|
-
if (conflict) return conflict;
|
|
1073
|
-
let insertLines = e.insert_after.text.split("\n");
|
|
1074
|
-
if (opts.restoreIndent) insertLines = restoreIndent([lines[idx]], insertLines);
|
|
1075
|
-
lines.splice(idx + 1, 0, ...insertLines);
|
|
1076
|
-
continue;
|
|
1077
|
-
}
|
|
1078
|
-
if (e.replace_lines) {
|
|
1079
|
-
const s = parseRef(e.replace_lines.start_anchor);
|
|
1080
|
-
const en = parseRef(e.replace_lines.end_anchor);
|
|
1081
|
-
const si = locateOrConflict(s);
|
|
1082
|
-
if (typeof si === "string") return si;
|
|
1083
|
-
const ei = locateOrConflict(en);
|
|
1084
|
-
if (typeof ei === "string") return ei;
|
|
1085
|
-
const actualStart = si + 1;
|
|
1086
|
-
const actualEnd = ei + 1;
|
|
1087
|
-
const rc = e.replace_lines.range_checksum;
|
|
1088
|
-
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).");
|
|
1089
|
-
if (staleRevision && conflictPolicy === "conservative") {
|
|
1090
|
-
const conflict = ensureRevisionContext(actualStart, actualEnd, si);
|
|
1091
|
-
if (conflict) return conflict;
|
|
1092
|
-
const baseCheck = hasBaseSnapshot ? verifyChecksumAgainstSnapshot(baseSnapshot, rc) : null;
|
|
1093
|
-
if (!baseCheck?.ok) {
|
|
1094
|
-
return conflictIfNeeded(
|
|
1095
|
-
"stale_checksum",
|
|
1096
|
-
si,
|
|
1097
|
-
baseCheck?.actual || null,
|
|
1098
|
-
baseCheck?.actual ? `Provided checksum ${rc} does not match base revision ${opts.baseRevision}.` : `Checksum range from ${rc} is outside the available base revision.`
|
|
1099
|
-
);
|
|
1100
|
-
}
|
|
1101
|
-
} else {
|
|
1102
|
-
const { start: csStart, end: csEnd, hex: csHex } = parseChecksum(rc);
|
|
1103
|
-
if (csStart > actualStart || csEnd < actualEnd) {
|
|
1104
|
-
const snip = buildErrorSnippet(origLines, actualStart - 1);
|
|
1105
|
-
throw new Error(
|
|
1106
|
-
`CHECKSUM_RANGE_GAP: checksum covers lines ${csStart}-${csEnd} but edit spans ${actualStart}-${actualEnd} (inclusive). Checksum range must fully contain the anchor range.
|
|
1107
|
-
|
|
1108
|
-
Current content (lines ${snip.start}-${snip.end}):
|
|
1109
|
-
${snip.text}
|
|
1110
|
-
|
|
1111
|
-
Tip: Use updated hashes above for retry.`
|
|
1112
|
-
);
|
|
1113
|
-
}
|
|
1114
|
-
const actual = buildRangeChecksum(currentSnapshot, csStart, csEnd);
|
|
1115
|
-
const actualHex = actual?.split(":")[1];
|
|
1116
|
-
if (!actual || csHex !== actualHex) {
|
|
1117
|
-
const details = `CHECKSUM_MISMATCH: expected ${rc}, got ${actual}. Content at lines ${csStart}-${csEnd} differs from when you read it \u2014 re-read before editing.`;
|
|
1118
|
-
if (conflictPolicy === "conservative") {
|
|
1119
|
-
return conflictIfNeeded("stale_checksum", csStart - 1, actual, details);
|
|
1120
|
-
}
|
|
1121
|
-
const snip = buildErrorSnippet(origLines, csStart - 1);
|
|
1122
|
-
throw new Error(
|
|
1123
|
-
`${details}
|
|
1455
|
+
const buildStrictChecksumMismatchError = (details, checksumStart, actual) => {
|
|
1456
|
+
const snip = buildErrorSnippet(origLines, checksumStart - 1);
|
|
1457
|
+
return new Error(
|
|
1458
|
+
`${details}
|
|
1124
1459
|
|
|
1125
1460
|
Current content (lines ${snip.start}-${snip.end}):
|
|
1126
1461
|
${snip.text}
|
|
1127
1462
|
|
|
1128
1463
|
Retry with fresh checksum ${actual}, or use set_line with hashes above.`
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
if (typeof ei === "string") return ei;
|
|
1154
|
-
if (si > ei) {
|
|
1155
|
-
throw new Error(`BAD_INPUT: replace_between start anchor resolves after end anchor (${si + 1} > ${ei + 1})`);
|
|
1156
|
-
}
|
|
1157
|
-
const targetRange = targetRangeForReplaceBetween(si, ei, boundaryMode);
|
|
1158
|
-
const conflict = ensureRevisionContext(targetRange.start, targetRange.end, si);
|
|
1159
|
-
if (conflict) return conflict;
|
|
1160
|
-
const txt = e.replace_between.new_text;
|
|
1161
|
-
let newLines = String(txt ?? "").split("\n");
|
|
1162
|
-
const sliceStart = boundaryMode === "exclusive" ? si + 1 : si;
|
|
1163
|
-
const removeCount = boundaryMode === "exclusive" ? Math.max(0, ei - si - 1) : ei - si + 1;
|
|
1164
|
-
const origRange = lines.slice(sliceStart, sliceStart + removeCount);
|
|
1165
|
-
if (opts.restoreIndent && origRange.length > 0) newLines = restoreIndent(origRange, newLines);
|
|
1166
|
-
if (txt === "" || txt === null) newLines = [];
|
|
1167
|
-
lines.splice(sliceStart, removeCount, ...newLines);
|
|
1168
|
-
}
|
|
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;
|
|
1169
1488
|
} catch (editErr) {
|
|
1170
1489
|
if (sorted.length > 1) editErr.message = `Edit ${_ei + 1}/${sorted.length}: ${editErr.message}`;
|
|
1171
1490
|
throw editErr;
|
|
@@ -1219,25 +1538,24 @@ file: ${nextSnapshot.fileChecksum}`;
|
|
|
1219
1538
|
if (autoRebased && staleRevision && hasBaseSnapshot) {
|
|
1220
1539
|
msg += `
|
|
1221
1540
|
changed_ranges: ${describeChangedRanges(changedRanges)}`;
|
|
1541
|
+
}
|
|
1542
|
+
if (remaps.length > 0) {
|
|
1543
|
+
msg += `
|
|
1544
|
+
remapped_refs:
|
|
1545
|
+
${remaps.map(({ from, to }) => `${from} -> ${to}`).join("\n")}`;
|
|
1222
1546
|
}
|
|
1223
1547
|
msg += `
|
|
1224
1548
|
Updated ${filePath} (${content.split("\n").length} lines)`;
|
|
1225
1549
|
if (fullDiff && minLine <= maxLine) {
|
|
1226
|
-
const ctxStart = Math.max(0, minLine - 6);
|
|
1550
|
+
const ctxStart = Math.max(0, minLine - 6) + 1;
|
|
1227
1551
|
const ctxEnd = Math.min(newLinesAll.length, maxLine + 5);
|
|
1228
|
-
const
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
ctxHashes.push(h);
|
|
1233
|
-
ctxLines.push(`${lineTag(h)}.${i + 1} ${newLinesAll[i]}`);
|
|
1234
|
-
}
|
|
1235
|
-
const ctxCs = rangeChecksum(ctxHashes, ctxStart + 1, ctxEnd);
|
|
1236
|
-
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 += `
|
|
1237
1556
|
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
checksum: ${ctxCs}`;
|
|
1557
|
+
${serializeReadBlock(block)}`;
|
|
1558
|
+
}
|
|
1241
1559
|
}
|
|
1242
1560
|
try {
|
|
1243
1561
|
const db = getGraphDB(real);
|
|
@@ -1334,9 +1652,7 @@ async function filesMode(pattern, target, opts) {
|
|
|
1334
1652
|
if (code !== 0 && code !== null) throw new Error(`GREP_ERROR: rg exit ${code} \u2014 ${stderr.trim() || "unknown error"}`);
|
|
1335
1653
|
const lines = stdout.trimEnd().split("\n").filter(Boolean);
|
|
1336
1654
|
const normalized = lines.map((l) => l.replace(/\\/g, "/"));
|
|
1337
|
-
return
|
|
1338
|
-
${normalized.join("\n")}
|
|
1339
|
-
\`\`\``;
|
|
1655
|
+
return normalized.join("\n");
|
|
1340
1656
|
}
|
|
1341
1657
|
async function countMode(pattern, target, opts) {
|
|
1342
1658
|
const realArgs = ["-c"];
|
|
@@ -1353,9 +1669,7 @@ async function countMode(pattern, target, opts) {
|
|
|
1353
1669
|
if (code !== 0 && code !== null) throw new Error(`GREP_ERROR: rg exit ${code} \u2014 ${stderr.trim() || "unknown error"}`);
|
|
1354
1670
|
const lines = stdout.trimEnd().split("\n").filter(Boolean);
|
|
1355
1671
|
const normalized = lines.map((l) => l.replace(/\\/g, "/"));
|
|
1356
|
-
return
|
|
1357
|
-
${normalized.join("\n")}
|
|
1358
|
-
\`\`\``;
|
|
1672
|
+
return normalized.join("\n");
|
|
1359
1673
|
}
|
|
1360
1674
|
async function contentMode(pattern, target, opts, plain, totalLimit) {
|
|
1361
1675
|
const realArgs = ["--json"];
|
|
@@ -1376,21 +1690,33 @@ async function contentMode(pattern, target, opts, plain, totalLimit) {
|
|
|
1376
1690
|
if (code === 1) return "No matches found.";
|
|
1377
1691
|
if (code !== 0 && code !== null) throw new Error(`GREP_ERROR: rg exit ${code} \u2014 ${stderr.trim() || "unknown error"}`);
|
|
1378
1692
|
const jsonLines = stdout.trimEnd().split("\n").filter(Boolean);
|
|
1379
|
-
const
|
|
1693
|
+
const blocks = [];
|
|
1380
1694
|
const db = getGraphDB(target);
|
|
1381
1695
|
const relCache = /* @__PURE__ */ new Map();
|
|
1382
1696
|
let groupFile = null;
|
|
1383
|
-
let
|
|
1697
|
+
let groupEntries = [];
|
|
1384
1698
|
let matchCount = 0;
|
|
1385
1699
|
function flushGroup() {
|
|
1386
|
-
if (
|
|
1387
|
-
const
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
const
|
|
1392
|
-
|
|
1393
|
-
|
|
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 = [];
|
|
1394
1720
|
}
|
|
1395
1721
|
for (const jl of jsonLines) {
|
|
1396
1722
|
let msg;
|
|
@@ -1404,11 +1730,6 @@ async function contentMode(pattern, target, opts, plain, totalLimit) {
|
|
|
1404
1730
|
flushGroup();
|
|
1405
1731
|
groupFile = null;
|
|
1406
1732
|
}
|
|
1407
|
-
if (msg.type === "begin") {
|
|
1408
|
-
if (formatted.length > 0 && formatted[formatted.length - 1] !== "") {
|
|
1409
|
-
formatted.push("");
|
|
1410
|
-
}
|
|
1411
|
-
}
|
|
1412
1733
|
continue;
|
|
1413
1734
|
}
|
|
1414
1735
|
if (msg.type !== "match" && msg.type !== "context") continue;
|
|
@@ -1430,48 +1751,46 @@ async function contentMode(pattern, target, opts, plain, totalLimit) {
|
|
|
1430
1751
|
for (let i = 0; i < subLines.length; i++) {
|
|
1431
1752
|
const ln = lineNum + i;
|
|
1432
1753
|
const lineContent = subLines[i];
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
if (groupLines.length > 0) {
|
|
1436
|
-
const lastLn = groupLines[groupLines.length - 1].lineNum;
|
|
1754
|
+
if (groupEntries.length > 0) {
|
|
1755
|
+
const lastLn = groupEntries[groupEntries.length - 1].lineNumber;
|
|
1437
1756
|
if (ln > lastLn + 1) flushGroup();
|
|
1438
1757
|
}
|
|
1439
|
-
groupLines.push({ lineNum: ln, hash32 });
|
|
1440
1758
|
const isMatch = msg.type === "match";
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
if (rel) {
|
|
1452
|
-
const a = matchAnnotation(db, rel, ln);
|
|
1453
|
-
if (a) anno = ` ${a}`;
|
|
1454
|
-
}
|
|
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;
|
|
1455
1769
|
}
|
|
1456
|
-
const prefix = isMatch ? ">>" : " ";
|
|
1457
|
-
formatted.push(`${filePath}:${prefix}${tag}.${ln} ${lineContent}${anno}`);
|
|
1458
1770
|
}
|
|
1771
|
+
groupEntries.push({
|
|
1772
|
+
lineNumber: ln,
|
|
1773
|
+
text: lineContent,
|
|
1774
|
+
role: isMatch ? "match" : "context",
|
|
1775
|
+
annotation
|
|
1776
|
+
});
|
|
1459
1777
|
}
|
|
1460
1778
|
if (msg.type === "match") {
|
|
1461
1779
|
matchCount++;
|
|
1462
1780
|
if (totalLimit > 0 && matchCount >= totalLimit) {
|
|
1463
1781
|
flushGroup();
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
${
|
|
1467
|
-
|
|
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");
|
|
1468
1788
|
}
|
|
1469
1789
|
}
|
|
1470
1790
|
}
|
|
1471
1791
|
flushGroup();
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
\`\`\``;
|
|
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");
|
|
1475
1794
|
}
|
|
1476
1795
|
|
|
1477
1796
|
// lib/outline.mjs
|
|
@@ -1535,15 +1854,6 @@ function supportedExtensions() {
|
|
|
1535
1854
|
return Object.keys(EXTENSION_GRAMMARS);
|
|
1536
1855
|
}
|
|
1537
1856
|
|
|
1538
|
-
// ../hex-common/src/text/file-text.mjs
|
|
1539
|
-
import { readFileSync as readFileSync2 } from "node:fs";
|
|
1540
|
-
function normalizeSourceText(text) {
|
|
1541
|
-
return text.replace(/\r\n/g, "\n");
|
|
1542
|
-
}
|
|
1543
|
-
function readUtf8Normalized(filePath) {
|
|
1544
|
-
return normalizeSourceText(readFileSync2(filePath, "utf-8"));
|
|
1545
|
-
}
|
|
1546
|
-
|
|
1547
1857
|
// lib/outline.mjs
|
|
1548
1858
|
var LANG_CONFIGS = {
|
|
1549
1859
|
".js": { outline: ["function_declaration", "class_declaration", "variable_declaration", "export_statement", "lexical_declaration"], skip: ["import_statement"], recurse: ["class_body"] },
|
|
@@ -1564,7 +1874,9 @@ var LANG_CONFIGS = {
|
|
|
1564
1874
|
".kt": { outline: ["function_declaration", "class_declaration", "object_declaration"], skip: ["import_header"], recurse: ["class_body"] },
|
|
1565
1875
|
".swift": { outline: ["function_declaration", "class_declaration", "struct_declaration", "protocol_declaration"], skip: ["import_declaration"], recurse: ["class_body"] },
|
|
1566
1876
|
".sh": { outline: ["function_definition"], skip: [], recurse: [] },
|
|
1567
|
-
".bash": { outline: ["function_definition"], skip: [], recurse: [] }
|
|
1877
|
+
".bash": { outline: ["function_definition"], skip: [], recurse: [] },
|
|
1878
|
+
".md": { outline: [], skip: [], recurse: [] },
|
|
1879
|
+
".mdx": { outline: [], skip: [], recurse: [] }
|
|
1568
1880
|
};
|
|
1569
1881
|
function extractOutline(rootNode, config, sourceLines) {
|
|
1570
1882
|
const entries = [];
|
|
@@ -1609,6 +1921,59 @@ function extractOutline(rootNode, config, sourceLines) {
|
|
|
1609
1921
|
walk(rootNode, 0);
|
|
1610
1922
|
return { entries, skippedRanges };
|
|
1611
1923
|
}
|
|
1924
|
+
function fallbackOutline(sourceLines) {
|
|
1925
|
+
const entries = [];
|
|
1926
|
+
for (let index = 0; index < sourceLines.length; index++) {
|
|
1927
|
+
const line = sourceLines[index];
|
|
1928
|
+
const trimmed = line.trim();
|
|
1929
|
+
if (!trimmed) continue;
|
|
1930
|
+
const match = trimmed.match(
|
|
1931
|
+
/^(?:export\s+)?(?:async\s+)?function\s+[\w$]+|^(?:export\s+)?(?:const|let|var)\s+[\w$]+\s*=|^(?:export\s+)?class\s+[\w$]+|^(?:export\s+)?interface\s+[\w$]+|^(?:export\s+)?type\s+[\w$]+\s*=|^(?:export\s+)?enum\s+[\w$]+|^(?:export\s+default\s+)?[\w$]+\s*=>/
|
|
1932
|
+
);
|
|
1933
|
+
if (!match) continue;
|
|
1934
|
+
entries.push({
|
|
1935
|
+
start: index + 1,
|
|
1936
|
+
end: index + 1,
|
|
1937
|
+
depth: 0,
|
|
1938
|
+
text: trimmed.slice(0, 120),
|
|
1939
|
+
name: trimmed.match(/([\w$]+)/)?.[1] || null
|
|
1940
|
+
});
|
|
1941
|
+
}
|
|
1942
|
+
return entries;
|
|
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
|
+
}
|
|
1612
1977
|
async function outlineFromContent(content, ext) {
|
|
1613
1978
|
const config = LANG_CONFIGS[ext];
|
|
1614
1979
|
const grammar = grammarForExtension(ext);
|
|
@@ -1625,8 +1990,9 @@ async function outlineFromContent(content, ext) {
|
|
|
1625
1990
|
const tree = parser.parse(content);
|
|
1626
1991
|
return extractOutline(tree.rootNode, config, sourceLines);
|
|
1627
1992
|
}
|
|
1628
|
-
function formatOutline(entries, skippedRanges, sourceLineCount, db, relFile) {
|
|
1993
|
+
function formatOutline(entries, skippedRanges, sourceLineCount, snapshot, db, relFile, note = "") {
|
|
1629
1994
|
const lines = [];
|
|
1995
|
+
if (note) lines.push(note, "");
|
|
1630
1996
|
if (skippedRanges.length > 0) {
|
|
1631
1997
|
const first = skippedRanges[0].start;
|
|
1632
1998
|
const last = skippedRanges[skippedRanges.length - 1].end;
|
|
@@ -1637,7 +2003,9 @@ function formatOutline(entries, skippedRanges, sourceLineCount, db, relFile) {
|
|
|
1637
2003
|
const indent = " ".repeat(e.depth);
|
|
1638
2004
|
const anno = db ? symbolAnnotation(db, relFile, e.name) : null;
|
|
1639
2005
|
const suffix = anno ? ` ${anno}` : "";
|
|
1640
|
-
|
|
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}`);
|
|
1641
2009
|
}
|
|
1642
2010
|
lines.push("");
|
|
1643
2011
|
lines.push(`(${entries.length} symbols, ${sourceLineCount} source lines)`);
|
|
@@ -1650,61 +2018,136 @@ async function fileOutline(filePath) {
|
|
|
1650
2018
|
if (!LANG_CONFIGS[ext]) {
|
|
1651
2019
|
return `Outline unavailable for ${ext} files. Use read_file directly for non-code files (markdown, config, text). Supported code extensions: ${supportedExtensions().join(", ")}`;
|
|
1652
2020
|
}
|
|
1653
|
-
const
|
|
1654
|
-
const
|
|
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
|
+
}
|
|
1655
2036
|
const db = getGraphDB(real);
|
|
1656
2037
|
const relFile = db ? getRelativePath(real) : null;
|
|
1657
2038
|
return `File: ${filePath}
|
|
1658
2039
|
|
|
1659
|
-
${formatOutline(
|
|
2040
|
+
${formatOutline(entries, skippedRanges, snapshot.lines.length, snapshot, db, relFile, note)}`;
|
|
1660
2041
|
}
|
|
1661
2042
|
|
|
1662
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
|
+
}
|
|
1663
2119
|
function verifyChecksums(filePath, checksums, opts = {}) {
|
|
1664
2120
|
filePath = normalizePath(filePath);
|
|
1665
2121
|
const real = validatePath(filePath);
|
|
1666
|
-
const
|
|
2122
|
+
const currentSnapshot = readSnapshot(real);
|
|
1667
2123
|
const baseSnapshot = opts.baseRevision ? getSnapshotByRevision(opts.baseRevision) : null;
|
|
1668
|
-
const
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
}
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
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))}`);
|
|
1681
2139
|
} else {
|
|
1682
|
-
|
|
1683
|
-
if (baseSnapshot?.path === real) {
|
|
1684
|
-
const changedRanges = computeChangedRanges(baseSnapshot.lines, current.lines);
|
|
1685
|
-
staleBits.push(`revision: ${current.revision}`);
|
|
1686
|
-
staleBits.push(`changed_ranges: ${describeChangedRanges(changedRanges)}`);
|
|
1687
|
-
} else if (opts.baseRevision) {
|
|
1688
|
-
staleBits.push(`revision: ${current.revision}`);
|
|
1689
|
-
staleBits.push(`changed_ranges: unavailable (base revision evicted)`);
|
|
1690
|
-
}
|
|
1691
|
-
results.push(staleBits.join("\n"));
|
|
1692
|
-
allValid = false;
|
|
2140
|
+
lines.push("base_revision_status: evicted");
|
|
1693
2141
|
}
|
|
1694
2142
|
}
|
|
1695
|
-
if (
|
|
1696
|
-
|
|
1697
|
-
msg += `
|
|
1698
|
-
revision: ${current.revision}`;
|
|
1699
|
-
msg += `
|
|
1700
|
-
file: ${current.fileChecksum}`;
|
|
1701
|
-
return msg;
|
|
2143
|
+
if (results.length > 0) {
|
|
2144
|
+
lines.push("", ...results.map(renderEntry));
|
|
1702
2145
|
}
|
|
1703
|
-
return
|
|
2146
|
+
return lines.join("\n");
|
|
1704
2147
|
}
|
|
1705
2148
|
|
|
1706
2149
|
// lib/tree.mjs
|
|
1707
|
-
import { readdirSync as readdirSync2, readFileSync as
|
|
2150
|
+
import { readdirSync as readdirSync2, readFileSync as readFileSync2, statSync as statSync6, existsSync as existsSync4 } from "node:fs";
|
|
1708
2151
|
import { resolve as resolve4, basename, join as join4, relative as relative2 } from "node:path";
|
|
1709
2152
|
import ignore from "ignore";
|
|
1710
2153
|
var SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
@@ -1725,7 +2168,7 @@ function loadGitignore(rootDir) {
|
|
|
1725
2168
|
const gi = join4(rootDir, ".gitignore");
|
|
1726
2169
|
if (!existsSync4(gi)) return null;
|
|
1727
2170
|
try {
|
|
1728
|
-
const content =
|
|
2171
|
+
const content = readFileSync2(gi, "utf-8");
|
|
1729
2172
|
return ignore().add(content);
|
|
1730
2173
|
} catch {
|
|
1731
2174
|
return null;
|
|
@@ -1971,7 +2414,7 @@ function fileInfo(filePath) {
|
|
|
1971
2414
|
}
|
|
1972
2415
|
|
|
1973
2416
|
// lib/setup.mjs
|
|
1974
|
-
import { readFileSync as
|
|
2417
|
+
import { readFileSync as readFileSync3, writeFileSync as writeFileSync2, existsSync as existsSync5, mkdirSync, copyFileSync } from "node:fs";
|
|
1975
2418
|
import { resolve as resolve6, dirname as dirname3, join as join5 } from "node:path";
|
|
1976
2419
|
import { fileURLToPath } from "node:url";
|
|
1977
2420
|
import { homedir } from "node:os";
|
|
@@ -1999,7 +2442,7 @@ var CLAUDE_HOOKS = {
|
|
|
1999
2442
|
};
|
|
2000
2443
|
function readJson(filePath) {
|
|
2001
2444
|
if (!existsSync5(filePath)) return null;
|
|
2002
|
-
return JSON.parse(
|
|
2445
|
+
return JSON.parse(readFileSync3(filePath, "utf-8"));
|
|
2003
2446
|
}
|
|
2004
2447
|
function writeJson(filePath, data) {
|
|
2005
2448
|
mkdirSync(dirname3(filePath), { recursive: true });
|
|
@@ -2078,7 +2521,7 @@ function installOutputStyle() {
|
|
|
2078
2521
|
const source = resolve6(dirname3(fileURLToPath(import.meta.url)), "..", "output-style.md");
|
|
2079
2522
|
const target = resolve6(homedir(), ".claude", "output-styles", "hex-line.md");
|
|
2080
2523
|
mkdirSync(dirname3(target), { recursive: true });
|
|
2081
|
-
writeFileSync2(target,
|
|
2524
|
+
writeFileSync2(target, readFileSync3(source, "utf-8"), "utf-8");
|
|
2082
2525
|
const userSettings = resolve6(homedir(), ".claude/settings.json");
|
|
2083
2526
|
const config = readJson(userSettings) || {};
|
|
2084
2527
|
const prev = config.outputStyle;
|
|
@@ -2275,7 +2718,7 @@ Summary: ${summary}`);
|
|
|
2275
2718
|
}
|
|
2276
2719
|
|
|
2277
2720
|
// lib/bulk-replace.mjs
|
|
2278
|
-
import { writeFileSync as writeFileSync3, readdirSync as readdirSync3 } from "node:fs";
|
|
2721
|
+
import { writeFileSync as writeFileSync3, readdirSync as readdirSync3, renameSync, unlinkSync } from "node:fs";
|
|
2279
2722
|
import { resolve as resolve7, relative as relative3, join as join6 } from "node:path";
|
|
2280
2723
|
var ignoreMod;
|
|
2281
2724
|
try {
|
|
@@ -2348,7 +2791,17 @@ function bulkReplace(rootDir, globPattern, replacements, opts = {}) {
|
|
|
2348
2791
|
continue;
|
|
2349
2792
|
}
|
|
2350
2793
|
if (!dryRun) {
|
|
2351
|
-
|
|
2794
|
+
const tempPath = `${file}.hexline-tmp-${process.pid}`;
|
|
2795
|
+
try {
|
|
2796
|
+
writeFileSync3(tempPath, content, "utf-8");
|
|
2797
|
+
renameSync(tempPath, file);
|
|
2798
|
+
} catch (error) {
|
|
2799
|
+
try {
|
|
2800
|
+
unlinkSync(tempPath);
|
|
2801
|
+
} catch {
|
|
2802
|
+
}
|
|
2803
|
+
throw error;
|
|
2804
|
+
}
|
|
2352
2805
|
}
|
|
2353
2806
|
const relPath = relative3(abs, file).replace(/\\/g, "/");
|
|
2354
2807
|
totalReplacements += replacementCount;
|
|
@@ -2384,7 +2837,7 @@ OUTPUT_CAPPED: Output exceeded ${MAX_BULK_OUTPUT_CHARS} chars.`;
|
|
|
2384
2837
|
}
|
|
2385
2838
|
|
|
2386
2839
|
// server.mjs
|
|
2387
|
-
var version = true ? "1.
|
|
2840
|
+
var version = true ? "1.6.0" : (await null).createRequire(import.meta.url)("./package.json").version;
|
|
2388
2841
|
var { server, StdioServerTransport } = await createServerRuntime({
|
|
2389
2842
|
name: "hex-line-mcp",
|
|
2390
2843
|
version
|
|
@@ -2392,43 +2845,43 @@ var { server, StdioServerTransport } = await createServerRuntime({
|
|
|
2392
2845
|
var replacementPairsSchema = z2.array(
|
|
2393
2846
|
z2.object({ old: z2.string().min(1), new: z2.string() })
|
|
2394
2847
|
).min(1);
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
if (
|
|
2406
|
-
|
|
2407
|
-
}
|
|
2408
|
-
if (e.start_anchor && e.end_anchor && e.new_text !== void 0) {
|
|
2409
|
-
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 } : {} } };
|
|
2848
|
+
var readRangeSchema = z2.union([
|
|
2849
|
+
z2.string(),
|
|
2850
|
+
z2.object({
|
|
2851
|
+
start: flexNum().optional(),
|
|
2852
|
+
end: flexNum().optional()
|
|
2853
|
+
})
|
|
2854
|
+
]);
|
|
2855
|
+
function parseReadRanges(rawRanges) {
|
|
2856
|
+
if (!rawRanges) return void 0;
|
|
2857
|
+
const parsed = Array.isArray(rawRanges) ? rawRanges : JSON.parse(rawRanges);
|
|
2858
|
+
if (!Array.isArray(parsed) || parsed.length === 0) {
|
|
2859
|
+
throw new Error("ranges must be a non-empty array");
|
|
2410
2860
|
}
|
|
2411
|
-
return
|
|
2861
|
+
return parsed;
|
|
2412
2862
|
}
|
|
2413
2863
|
server.registerTool("read_file", {
|
|
2414
2864
|
title: "Read File",
|
|
2415
|
-
description: "Read
|
|
2865
|
+
description: "Read file with hash-annotated lines, checksums, and revision metadata. Default: edit-ready output. Use plain:true for non-edit workflows.",
|
|
2416
2866
|
inputSchema: z2.object({
|
|
2417
2867
|
path: z2.string().optional().describe("File or directory path"),
|
|
2418
2868
|
paths: z2.array(z2.string()).optional().describe("Array of file paths to read (batch mode)"),
|
|
2419
2869
|
offset: flexNum().describe("Start line (1-indexed, default: 1)"),
|
|
2420
2870
|
limit: flexNum().describe("Max lines (default: 2000, 0 = all)"),
|
|
2871
|
+
ranges: z2.union([z2.string(), z2.array(readRangeSchema)]).optional().describe('Line ranges, e.g. ["10-25", {"start":40,"end":55}]'),
|
|
2872
|
+
include_graph: flexBool().describe("Include graph annotations"),
|
|
2421
2873
|
plain: flexBool().describe("Omit hashes (lineNum|content)")
|
|
2422
2874
|
}),
|
|
2423
2875
|
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true }
|
|
2424
2876
|
}, async (rawParams) => {
|
|
2425
|
-
const { path: p, paths: multi, offset, limit, plain } = coerceParams(rawParams);
|
|
2877
|
+
const { path: p, paths: multi, offset, limit, ranges: rawRanges, include_graph, plain } = coerceParams(rawParams);
|
|
2426
2878
|
try {
|
|
2879
|
+
const ranges = parseReadRanges(rawRanges);
|
|
2427
2880
|
if (multi && multi.length > 0 && !p) {
|
|
2428
2881
|
const results = [];
|
|
2429
2882
|
for (const fp of multi) {
|
|
2430
2883
|
try {
|
|
2431
|
-
results.push(readFile2(fp, { offset, limit, plain }));
|
|
2884
|
+
results.push(readFile2(fp, { offset, limit, ranges, includeGraph: include_graph, plain }));
|
|
2432
2885
|
} catch (e) {
|
|
2433
2886
|
results.push(`File: ${fp}
|
|
2434
2887
|
|
|
@@ -2438,18 +2891,18 @@ ERROR: ${e.message}`);
|
|
|
2438
2891
|
return { content: [{ type: "text", text: results.join("\n\n---\n\n") }] };
|
|
2439
2892
|
}
|
|
2440
2893
|
if (!p) throw new Error("Either 'path' or 'paths' is required");
|
|
2441
|
-
return { content: [{ type: "text", text: readFile2(p, { offset, limit, plain }) }] };
|
|
2894
|
+
return { content: [{ type: "text", text: readFile2(p, { offset, limit, ranges, includeGraph: include_graph, plain }) }] };
|
|
2442
2895
|
} catch (e) {
|
|
2443
2896
|
return { content: [{ type: "text", text: e.message }], isError: true };
|
|
2444
2897
|
}
|
|
2445
2898
|
});
|
|
2446
2899
|
server.registerTool("edit_file", {
|
|
2447
2900
|
title: "Edit File",
|
|
2448
|
-
description: "Apply
|
|
2901
|
+
description: "Apply verified partial edits to one file.",
|
|
2449
2902
|
inputSchema: z2.object({
|
|
2450
2903
|
path: z2.string().describe("File to edit"),
|
|
2451
2904
|
edits: z2.union([z2.string(), z2.array(z2.any())]).describe(
|
|
2452
|
-
'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"}}]'
|
|
2453
2906
|
),
|
|
2454
2907
|
dry_run: flexBool().describe("Preview changes without writing"),
|
|
2455
2908
|
restore_indent: flexBool().describe("Auto-fix indentation to match anchor (default: false)"),
|
|
@@ -2467,11 +2920,10 @@ server.registerTool("edit_file", {
|
|
|
2467
2920
|
throw new Error('edits: invalid JSON. Expected: [{"set_line":{"anchor":"xx.N","new_text":"..."}}]');
|
|
2468
2921
|
}
|
|
2469
2922
|
if (!Array.isArray(parsed) || !parsed.length) throw new Error("Edits: non-empty JSON array required");
|
|
2470
|
-
const normalized = parsed.map(coerceEdit);
|
|
2471
2923
|
return {
|
|
2472
2924
|
content: [{
|
|
2473
2925
|
type: "text",
|
|
2474
|
-
text: editFile(p,
|
|
2926
|
+
text: editFile(p, parsed, {
|
|
2475
2927
|
dryRun: dry_run,
|
|
2476
2928
|
restoreIndent: restore_indent,
|
|
2477
2929
|
baseRevision: base_revision,
|
|
@@ -2504,7 +2956,7 @@ server.registerTool("write_file", {
|
|
|
2504
2956
|
});
|
|
2505
2957
|
server.registerTool("grep_search", {
|
|
2506
2958
|
title: "Search Files",
|
|
2507
|
-
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.",
|
|
2508
2960
|
inputSchema: z2.object({
|
|
2509
2961
|
pattern: z2.string().describe("Search pattern (regex by default, literal if literal:true)"),
|
|
2510
2962
|
path: z2.string().optional().describe("Search dir/file (default: cwd)"),
|
|
@@ -2565,7 +3017,7 @@ server.registerTool("grep_search", {
|
|
|
2565
3017
|
});
|
|
2566
3018
|
server.registerTool("outline", {
|
|
2567
3019
|
title: "File Outline",
|
|
2568
|
-
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).",
|
|
2569
3021
|
inputSchema: z2.object({
|
|
2570
3022
|
path: z2.string().describe("Source file path")
|
|
2571
3023
|
}),
|
|
@@ -2581,19 +3033,20 @@ server.registerTool("outline", {
|
|
|
2581
3033
|
});
|
|
2582
3034
|
server.registerTool("verify", {
|
|
2583
3035
|
title: "Verify Checksums",
|
|
2584
|
-
description: "
|
|
3036
|
+
description: "Verify held checksums without rereading the file.",
|
|
2585
3037
|
inputSchema: z2.object({
|
|
2586
3038
|
path: z2.string().describe("File path"),
|
|
2587
|
-
checksums: z2.string().describe('
|
|
3039
|
+
checksums: z2.array(z2.string()).describe('Checksum strings, e.g. ["1-50:f7e2a1b0", "51-100:abcd1234"]'),
|
|
2588
3040
|
base_revision: z2.string().optional().describe("Optional prior revision to compare against latest state.")
|
|
2589
3041
|
}),
|
|
2590
3042
|
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true }
|
|
2591
3043
|
}, async (rawParams) => {
|
|
2592
3044
|
const { path: p, checksums, base_revision } = coerceParams(rawParams);
|
|
2593
3045
|
try {
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
3046
|
+
if (!Array.isArray(checksums) || checksums.length === 0) {
|
|
3047
|
+
throw new Error("checksums must be a non-empty array of strings");
|
|
3048
|
+
}
|
|
3049
|
+
return { content: [{ type: "text", text: verifyChecksums(p, checksums, { baseRevision: base_revision }) }] };
|
|
2597
3050
|
} catch (e) {
|
|
2598
3051
|
return { content: [{ type: "text", text: e.message }], isError: true };
|
|
2599
3052
|
}
|
|
@@ -2667,7 +3120,7 @@ server.registerTool("changes", {
|
|
|
2667
3120
|
});
|
|
2668
3121
|
server.registerTool("bulk_replace", {
|
|
2669
3122
|
title: "Bulk Replace",
|
|
2670
|
-
description: "Search-and-replace across multiple files
|
|
3123
|
+
description: "Search-and-replace across multiple files with compact or full diff output.",
|
|
2671
3124
|
inputSchema: z2.object({
|
|
2672
3125
|
replacements: z2.union([z2.string(), replacementPairsSchema]).describe('JSON array of {old, new} pairs: [{"old":"foo","new":"bar"}]'),
|
|
2673
3126
|
glob: z2.string().optional().describe('File glob (default: "**/*.{md,mjs,json,yml,ts,js}")'),
|