@levnikolaevich/hex-line-mcp 1.3.5 → 1.3.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/server.mjs +219 -183
- package/package.json +1 -1
package/dist/server.mjs
CHANGED
|
@@ -249,6 +249,7 @@ function listDirectory(dirPath, opts = {}) {
|
|
|
249
249
|
return { text: lines.join("\n"), total };
|
|
250
250
|
}
|
|
251
251
|
var MAX_OUTPUT_CHARS = 8e4;
|
|
252
|
+
var MAX_DIFF_CHARS = 3e4;
|
|
252
253
|
function readText(filePath) {
|
|
253
254
|
return readFileSync(filePath, "utf-8").replace(/\r\n/g, "\n");
|
|
254
255
|
}
|
|
@@ -1036,125 +1037,131 @@ function editFile(filePath, edits, opts = {}) {
|
|
|
1036
1037
|
autoRebased = true;
|
|
1037
1038
|
return null;
|
|
1038
1039
|
};
|
|
1039
|
-
for (
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
if (!txt && txt !== 0) {
|
|
1048
|
-
lines.splice(idx, 1);
|
|
1049
|
-
} else {
|
|
1050
|
-
const origLine = [lines[idx]];
|
|
1051
|
-
const raw = String(txt).split("\n");
|
|
1052
|
-
const newLines = opts.restoreIndent ? restoreIndent(origLine, raw) : raw;
|
|
1053
|
-
lines.splice(idx, 1, ...newLines);
|
|
1054
|
-
}
|
|
1055
|
-
continue;
|
|
1056
|
-
}
|
|
1057
|
-
if (e.insert_after) {
|
|
1058
|
-
const { tag, line } = parseRef(e.insert_after.anchor);
|
|
1059
|
-
const idx = locateOrConflict({ tag, line });
|
|
1060
|
-
if (typeof idx === "string") return idx;
|
|
1061
|
-
const conflict = ensureRevisionContext(idx + 1, idx + 1, idx);
|
|
1062
|
-
if (conflict) return conflict;
|
|
1063
|
-
let insertLines = e.insert_after.text.split("\n");
|
|
1064
|
-
if (opts.restoreIndent) insertLines = restoreIndent([lines[idx]], insertLines);
|
|
1065
|
-
lines.splice(idx + 1, 0, ...insertLines);
|
|
1066
|
-
continue;
|
|
1067
|
-
}
|
|
1068
|
-
if (e.replace_lines) {
|
|
1069
|
-
const s = parseRef(e.replace_lines.start_anchor);
|
|
1070
|
-
const en = parseRef(e.replace_lines.end_anchor);
|
|
1071
|
-
const si = locateOrConflict(s);
|
|
1072
|
-
if (typeof si === "string") return si;
|
|
1073
|
-
const ei = locateOrConflict(en);
|
|
1074
|
-
if (typeof ei === "string") return ei;
|
|
1075
|
-
const actualStart = si + 1;
|
|
1076
|
-
const actualEnd = ei + 1;
|
|
1077
|
-
const rc = e.replace_lines.range_checksum;
|
|
1078
|
-
if (!rc) throw new Error("range_checksum required for replace_lines. Read the range first via read_file, then pass its checksum.");
|
|
1079
|
-
if (staleRevision && conflictPolicy === "conservative") {
|
|
1080
|
-
const conflict = ensureRevisionContext(actualStart, actualEnd, si);
|
|
1040
|
+
for (let _ei = 0; _ei < sorted.length; _ei++) {
|
|
1041
|
+
const e = sorted[_ei];
|
|
1042
|
+
try {
|
|
1043
|
+
if (e.set_line) {
|
|
1044
|
+
const { tag, line } = parseRef(e.set_line.anchor);
|
|
1045
|
+
const idx = locateOrConflict({ tag, line });
|
|
1046
|
+
if (typeof idx === "string") return idx;
|
|
1047
|
+
const conflict = ensureRevisionContext(idx + 1, idx + 1, idx);
|
|
1081
1048
|
if (conflict) return conflict;
|
|
1082
|
-
const
|
|
1083
|
-
if (!
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
);
|
|
1049
|
+
const txt = e.set_line.new_text;
|
|
1050
|
+
if (!txt && txt !== 0) {
|
|
1051
|
+
lines.splice(idx, 1);
|
|
1052
|
+
} else {
|
|
1053
|
+
const origLine = [lines[idx]];
|
|
1054
|
+
const raw = String(txt).split("\n");
|
|
1055
|
+
const newLines = opts.restoreIndent ? restoreIndent(origLine, raw) : raw;
|
|
1056
|
+
lines.splice(idx, 1, ...newLines);
|
|
1090
1057
|
}
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1058
|
+
continue;
|
|
1059
|
+
}
|
|
1060
|
+
if (e.insert_after) {
|
|
1061
|
+
const { tag, line } = parseRef(e.insert_after.anchor);
|
|
1062
|
+
const idx = locateOrConflict({ tag, line });
|
|
1063
|
+
if (typeof idx === "string") return idx;
|
|
1064
|
+
const conflict = ensureRevisionContext(idx + 1, idx + 1, idx);
|
|
1065
|
+
if (conflict) return conflict;
|
|
1066
|
+
let insertLines = e.insert_after.text.split("\n");
|
|
1067
|
+
if (opts.restoreIndent) insertLines = restoreIndent([lines[idx]], insertLines);
|
|
1068
|
+
lines.splice(idx + 1, 0, ...insertLines);
|
|
1069
|
+
continue;
|
|
1070
|
+
}
|
|
1071
|
+
if (e.replace_lines) {
|
|
1072
|
+
const s = parseRef(e.replace_lines.start_anchor);
|
|
1073
|
+
const en = parseRef(e.replace_lines.end_anchor);
|
|
1074
|
+
const si = locateOrConflict(s);
|
|
1075
|
+
if (typeof si === "string") return si;
|
|
1076
|
+
const ei = locateOrConflict(en);
|
|
1077
|
+
if (typeof ei === "string") return ei;
|
|
1078
|
+
const actualStart = si + 1;
|
|
1079
|
+
const actualEnd = ei + 1;
|
|
1080
|
+
const rc = e.replace_lines.range_checksum;
|
|
1081
|
+
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).");
|
|
1082
|
+
if (staleRevision && conflictPolicy === "conservative") {
|
|
1083
|
+
const conflict = ensureRevisionContext(actualStart, actualEnd, si);
|
|
1084
|
+
if (conflict) return conflict;
|
|
1085
|
+
const baseCheck = hasBaseSnapshot ? verifyChecksumAgainstSnapshot(baseSnapshot, rc) : null;
|
|
1086
|
+
if (!baseCheck?.ok) {
|
|
1087
|
+
return conflictIfNeeded(
|
|
1088
|
+
"stale_checksum",
|
|
1089
|
+
si,
|
|
1090
|
+
baseCheck?.actual || null,
|
|
1091
|
+
baseCheck?.actual ? `Provided checksum ${rc} does not match base revision ${opts.baseRevision}.` : `Checksum range from ${rc} is outside the available base revision.`
|
|
1092
|
+
);
|
|
1093
|
+
}
|
|
1094
|
+
} else {
|
|
1095
|
+
const { start: csStart, end: csEnd, hex: csHex } = parseChecksum(rc);
|
|
1096
|
+
if (csStart > actualStart || csEnd < actualEnd) {
|
|
1097
|
+
const snip = buildErrorSnippet(origLines, actualStart - 1);
|
|
1098
|
+
throw new Error(
|
|
1099
|
+
`CHECKSUM_RANGE_GAP: checksum covers lines ${csStart}-${csEnd} but edit spans ${actualStart}-${actualEnd} (inclusive). Checksum range must fully contain the anchor range.
|
|
1097
1100
|
|
|
1098
1101
|
Current content (lines ${snip.start}-${snip.end}):
|
|
1099
1102
|
${snip.text}
|
|
1100
1103
|
|
|
1101
1104
|
Tip: Use updated hashes above for retry.`
|
|
1102
|
-
|
|
1103
|
-
}
|
|
1104
|
-
const actual = buildRangeChecksum(currentSnapshot, csStart, csEnd);
|
|
1105
|
-
const actualHex = actual?.split(":")[1];
|
|
1106
|
-
if (!actual || csHex !== actualHex) {
|
|
1107
|
-
const details = `CHECKSUM_MISMATCH: expected ${rc}, got ${actual}. File changed \u2014 re-read lines ${csStart}-${csEnd}.`;
|
|
1108
|
-
if (conflictPolicy === "conservative") {
|
|
1109
|
-
return conflictIfNeeded("stale_checksum", csStart - 1, actual, details);
|
|
1105
|
+
);
|
|
1110
1106
|
}
|
|
1111
|
-
const
|
|
1112
|
-
|
|
1113
|
-
|
|
1107
|
+
const actual = buildRangeChecksum(currentSnapshot, csStart, csEnd);
|
|
1108
|
+
const actualHex = actual?.split(":")[1];
|
|
1109
|
+
if (!actual || csHex !== actualHex) {
|
|
1110
|
+
const details = `CHECKSUM_MISMATCH: expected ${rc}, got ${actual}. Content at lines ${csStart}-${csEnd} differs from when you read it \u2014 re-read before editing.`;
|
|
1111
|
+
if (conflictPolicy === "conservative") {
|
|
1112
|
+
return conflictIfNeeded("stale_checksum", csStart - 1, actual, details);
|
|
1113
|
+
}
|
|
1114
|
+
const snip = buildErrorSnippet(origLines, csStart - 1);
|
|
1115
|
+
throw new Error(
|
|
1116
|
+
`${details}
|
|
1114
1117
|
|
|
1115
1118
|
Current content (lines ${snip.start}-${snip.end}):
|
|
1116
1119
|
${snip.text}
|
|
1117
1120
|
|
|
1118
1121
|
Retry with fresh checksum ${actual}, or use set_line with hashes above.`
|
|
1119
|
-
|
|
1122
|
+
);
|
|
1123
|
+
}
|
|
1120
1124
|
}
|
|
1125
|
+
const txt = e.replace_lines.new_text;
|
|
1126
|
+
if (!txt && txt !== 0) {
|
|
1127
|
+
lines.splice(si, ei - si + 1);
|
|
1128
|
+
} else {
|
|
1129
|
+
const origRange = lines.slice(si, ei + 1);
|
|
1130
|
+
let newLines = String(txt).split("\n");
|
|
1131
|
+
if (opts.restoreIndent) newLines = restoreIndent(origRange, newLines);
|
|
1132
|
+
lines.splice(si, ei - si + 1, ...newLines);
|
|
1133
|
+
}
|
|
1134
|
+
continue;
|
|
1121
1135
|
}
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1136
|
+
if (e.replace_between) {
|
|
1137
|
+
const boundaryMode = e.replace_between.boundary_mode || "inclusive";
|
|
1138
|
+
if (boundaryMode !== "inclusive" && boundaryMode !== "exclusive") {
|
|
1139
|
+
throw new Error(`BAD_INPUT: replace_between boundary_mode must be inclusive or exclusive, got ${boundaryMode}`);
|
|
1140
|
+
}
|
|
1141
|
+
const s = parseRef(e.replace_between.start_anchor);
|
|
1142
|
+
const en = parseRef(e.replace_between.end_anchor);
|
|
1143
|
+
const si = locateOrConflict(s);
|
|
1144
|
+
if (typeof si === "string") return si;
|
|
1145
|
+
const ei = locateOrConflict(en);
|
|
1146
|
+
if (typeof ei === "string") return ei;
|
|
1147
|
+
if (si > ei) {
|
|
1148
|
+
throw new Error(`BAD_INPUT: replace_between start anchor resolves after end anchor (${si + 1} > ${ei + 1})`);
|
|
1149
|
+
}
|
|
1150
|
+
const targetRange = targetRangeForReplaceBetween(si, ei, boundaryMode);
|
|
1151
|
+
const conflict = ensureRevisionContext(targetRange.start, targetRange.end, si);
|
|
1152
|
+
if (conflict) return conflict;
|
|
1153
|
+
const txt = e.replace_between.new_text;
|
|
1154
|
+
let newLines = String(txt ?? "").split("\n");
|
|
1155
|
+
const sliceStart = boundaryMode === "exclusive" ? si + 1 : si;
|
|
1156
|
+
const removeCount = boundaryMode === "exclusive" ? Math.max(0, ei - si - 1) : ei - si + 1;
|
|
1157
|
+
const origRange = lines.slice(sliceStart, sliceStart + removeCount);
|
|
1158
|
+
if (opts.restoreIndent && origRange.length > 0) newLines = restoreIndent(origRange, newLines);
|
|
1159
|
+
if (txt === "" || txt === null) newLines = [];
|
|
1160
|
+
lines.splice(sliceStart, removeCount, ...newLines);
|
|
1146
1161
|
}
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
const txt = e.replace_between.new_text;
|
|
1151
|
-
let newLines = String(txt ?? "").split("\n");
|
|
1152
|
-
const sliceStart = boundaryMode === "exclusive" ? si + 1 : si;
|
|
1153
|
-
const removeCount = boundaryMode === "exclusive" ? Math.max(0, ei - si - 1) : ei - si + 1;
|
|
1154
|
-
const origRange = lines.slice(sliceStart, sliceStart + removeCount);
|
|
1155
|
-
if (opts.restoreIndent && origRange.length > 0) newLines = restoreIndent(origRange, newLines);
|
|
1156
|
-
if (txt === "" || txt === null) newLines = [];
|
|
1157
|
-
lines.splice(sliceStart, removeCount, ...newLines);
|
|
1162
|
+
} catch (editErr) {
|
|
1163
|
+
if (sorted.length > 1) editErr.message = `Edit ${_ei + 1}/${sorted.length}: ${editErr.message}`;
|
|
1164
|
+
throw editErr;
|
|
1158
1165
|
}
|
|
1159
1166
|
}
|
|
1160
1167
|
let content = lines.join("\n");
|
|
@@ -1163,10 +1170,23 @@ Retry with fresh checksum ${actual}, or use set_line with hashes above.`
|
|
|
1163
1170
|
if (original === content) {
|
|
1164
1171
|
throw new Error("NOOP_EDIT: File already contains the desired content. No changes needed.");
|
|
1165
1172
|
}
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1173
|
+
const fullDiff = simpleDiff(origLines, content.split("\n"));
|
|
1174
|
+
let displayDiff = fullDiff;
|
|
1175
|
+
if (displayDiff && displayDiff.length > MAX_DIFF_CHARS) {
|
|
1176
|
+
displayDiff = displayDiff.slice(0, MAX_DIFF_CHARS) + `
|
|
1177
|
+
... (diff truncated, ${displayDiff.length} chars total)`;
|
|
1178
|
+
}
|
|
1179
|
+
const newLinesAll = content.split("\n");
|
|
1180
|
+
let minLine = Infinity, maxLine = 0;
|
|
1181
|
+
if (fullDiff) {
|
|
1182
|
+
for (const dl of fullDiff.split("\n")) {
|
|
1183
|
+
const m = dl.match(/^[+-](\d+)\|/);
|
|
1184
|
+
if (m) {
|
|
1185
|
+
const n = +m[1];
|
|
1186
|
+
if (n < minLine) minLine = n;
|
|
1187
|
+
if (n > maxLine) maxLine = n;
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1170
1190
|
}
|
|
1171
1191
|
if (opts.dryRun) {
|
|
1172
1192
|
let msg2 = `status: ${autoRebased ? "AUTO_REBASED" : "OK"}
|
|
@@ -1175,11 +1195,11 @@ file: ${currentSnapshot.fileChecksum}
|
|
|
1175
1195
|
Dry run: ${filePath} would change (${content.split("\n").length} lines)`;
|
|
1176
1196
|
if (staleRevision && hasBaseSnapshot) msg2 += `
|
|
1177
1197
|
changed_ranges: ${describeChangedRanges(changedRanges)}`;
|
|
1178
|
-
if (
|
|
1198
|
+
if (displayDiff) msg2 += `
|
|
1179
1199
|
|
|
1180
1200
|
Diff:
|
|
1181
1201
|
\`\`\`diff
|
|
1182
|
-
${
|
|
1202
|
+
${displayDiff}
|
|
1183
1203
|
\`\`\``;
|
|
1184
1204
|
return msg2;
|
|
1185
1205
|
}
|
|
@@ -1195,78 +1215,55 @@ changed_ranges: ${describeChangedRanges(changedRanges)}`;
|
|
|
1195
1215
|
}
|
|
1196
1216
|
msg += `
|
|
1197
1217
|
Updated ${filePath} (${content.split("\n").length} lines)`;
|
|
1198
|
-
if (
|
|
1218
|
+
if (fullDiff && minLine <= maxLine) {
|
|
1219
|
+
const ctxStart = Math.max(0, minLine - 6);
|
|
1220
|
+
const ctxEnd = Math.min(newLinesAll.length, maxLine + 5);
|
|
1221
|
+
const ctxLines = [];
|
|
1222
|
+
const ctxHashes = [];
|
|
1223
|
+
for (let i = ctxStart; i < ctxEnd; i++) {
|
|
1224
|
+
const h = fnv1a(newLinesAll[i]);
|
|
1225
|
+
ctxHashes.push(h);
|
|
1226
|
+
ctxLines.push(`${lineTag(h)}.${i + 1} ${newLinesAll[i]}`);
|
|
1227
|
+
}
|
|
1228
|
+
const ctxCs = rangeChecksum(ctxHashes, ctxStart + 1, ctxEnd);
|
|
1229
|
+
msg += `
|
|
1199
1230
|
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
${
|
|
1203
|
-
|
|
1231
|
+
Post-edit (lines ${ctxStart + 1}-${ctxEnd}):
|
|
1232
|
+
${ctxLines.join("\n")}
|
|
1233
|
+
checksum: ${ctxCs}`;
|
|
1234
|
+
}
|
|
1204
1235
|
try {
|
|
1205
1236
|
const db = getGraphDB(real);
|
|
1206
1237
|
const relFile = db ? getRelativePath(real) : null;
|
|
1207
|
-
if (db && relFile &&
|
|
1208
|
-
const
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
if (m) {
|
|
1213
|
-
const n = +m[1];
|
|
1214
|
-
if (n < minLine) minLine = n;
|
|
1215
|
-
if (n > maxLine) maxLine = n;
|
|
1216
|
-
}
|
|
1217
|
-
}
|
|
1218
|
-
if (minLine <= maxLine) {
|
|
1219
|
-
const affected = callImpact(db, relFile, minLine, maxLine);
|
|
1220
|
-
if (affected.length > 0) {
|
|
1221
|
-
const list = affected.map((a) => `${a.name} (${a.file}:${a.line})`).join(", ");
|
|
1222
|
-
msg += `
|
|
1238
|
+
if (db && relFile && fullDiff && minLine <= maxLine) {
|
|
1239
|
+
const affected = callImpact(db, relFile, minLine, maxLine);
|
|
1240
|
+
if (affected.length > 0) {
|
|
1241
|
+
const list = affected.map((a) => `${a.name} (${a.file}:${a.line})`).join(", ");
|
|
1242
|
+
msg += `
|
|
1223
1243
|
|
|
1224
1244
|
\u26A0 Call impact: ${affected.length} callers in other files
|
|
1225
1245
|
${list}`;
|
|
1226
|
-
}
|
|
1227
1246
|
}
|
|
1228
1247
|
}
|
|
1229
1248
|
} catch {
|
|
1230
1249
|
}
|
|
1231
|
-
|
|
1232
|
-
if (diff) {
|
|
1233
|
-
const diffArr = diff.split("\n");
|
|
1234
|
-
let minLine = Infinity, maxLine = 0;
|
|
1235
|
-
for (const dl of diffArr) {
|
|
1236
|
-
const m = dl.match(/^[+-](\d+)\|/);
|
|
1237
|
-
if (m) {
|
|
1238
|
-
const n = +m[1];
|
|
1239
|
-
if (n < minLine) minLine = n;
|
|
1240
|
-
if (n > maxLine) maxLine = n;
|
|
1241
|
-
}
|
|
1242
|
-
}
|
|
1243
|
-
if (minLine <= maxLine) {
|
|
1244
|
-
const ctxStart = Math.max(0, minLine - 6);
|
|
1245
|
-
const ctxEnd = Math.min(newLinesAll.length, maxLine + 5);
|
|
1246
|
-
const ctxLines = [];
|
|
1247
|
-
const ctxHashes = [];
|
|
1248
|
-
for (let i = ctxStart; i < ctxEnd; i++) {
|
|
1249
|
-
const h = fnv1a(newLinesAll[i]);
|
|
1250
|
-
ctxHashes.push(h);
|
|
1251
|
-
ctxLines.push(`${lineTag(h)}.${i + 1} ${newLinesAll[i]}`);
|
|
1252
|
-
}
|
|
1253
|
-
const ctxCs = rangeChecksum(ctxHashes, ctxStart + 1, ctxEnd);
|
|
1254
|
-
msg += `
|
|
1250
|
+
if (displayDiff) msg += `
|
|
1255
1251
|
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
}
|
|
1252
|
+
Diff:
|
|
1253
|
+
\`\`\`diff
|
|
1254
|
+
${displayDiff}
|
|
1255
|
+
\`\`\``;
|
|
1261
1256
|
return msg;
|
|
1262
1257
|
}
|
|
1263
1258
|
|
|
1264
1259
|
// lib/search.mjs
|
|
1265
1260
|
import { spawn } from "node:child_process";
|
|
1266
|
-
import { resolve as resolve2 } from "node:path";
|
|
1261
|
+
import { resolve as resolve2, isAbsolute as isAbsolute2 } from "node:path";
|
|
1262
|
+
import { existsSync as existsSync3 } from "node:fs";
|
|
1267
1263
|
var rgBin = "rg";
|
|
1268
1264
|
try {
|
|
1269
1265
|
rgBin = (await import("@vscode/ripgrep")).rgPath;
|
|
1266
|
+
if (isAbsolute2(rgBin) && !existsSync3(rgBin)) rgBin = "rg";
|
|
1270
1267
|
} catch {
|
|
1271
1268
|
}
|
|
1272
1269
|
var DEFAULT_LIMIT2 = 100;
|
|
@@ -1292,7 +1289,13 @@ function spawnRg(args) {
|
|
|
1292
1289
|
stderrBuf += chunk.toString("utf-8");
|
|
1293
1290
|
});
|
|
1294
1291
|
child.on("error", (err) => {
|
|
1295
|
-
|
|
1292
|
+
if (err.code === "ENOENT") {
|
|
1293
|
+
reject(new Error(
|
|
1294
|
+
`ripgrep not available. Reinstall dependencies so @vscode/ripgrep can provide its binary, or install system rg and add it to PATH. Attempted binary: "${rgBin}".`
|
|
1295
|
+
));
|
|
1296
|
+
} else {
|
|
1297
|
+
reject(new Error(`rg spawn error: ${err.message}`));
|
|
1298
|
+
}
|
|
1296
1299
|
});
|
|
1297
1300
|
child.on("close", (code) => {
|
|
1298
1301
|
resolve_({ stdout, code, stderr: stderrBuf, killed });
|
|
@@ -1694,7 +1697,7 @@ file: ${current.fileChecksum}`;
|
|
|
1694
1697
|
}
|
|
1695
1698
|
|
|
1696
1699
|
// lib/tree.mjs
|
|
1697
|
-
import { readdirSync as readdirSync2, readFileSync as readFileSync3, statSync as statSync6, existsSync as
|
|
1700
|
+
import { readdirSync as readdirSync2, readFileSync as readFileSync3, statSync as statSync6, existsSync as existsSync4 } from "node:fs";
|
|
1698
1701
|
import { resolve as resolve4, basename, join as join4, relative as relative2 } from "node:path";
|
|
1699
1702
|
import ignore from "ignore";
|
|
1700
1703
|
var SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
@@ -1713,7 +1716,7 @@ function globToRegex(pat) {
|
|
|
1713
1716
|
}
|
|
1714
1717
|
function loadGitignore(rootDir) {
|
|
1715
1718
|
const gi = join4(rootDir, ".gitignore");
|
|
1716
|
-
if (!
|
|
1719
|
+
if (!existsSync4(gi)) return null;
|
|
1717
1720
|
try {
|
|
1718
1721
|
const content = readFileSync3(gi, "utf-8");
|
|
1719
1722
|
return ignore().add(content);
|
|
@@ -1730,7 +1733,7 @@ function findByPattern(dirPath, opts) {
|
|
|
1730
1733
|
const filterType = opts.type || "all";
|
|
1731
1734
|
const maxDepth = opts.max_depth ?? 20;
|
|
1732
1735
|
const abs = resolve4(normalizePath(dirPath));
|
|
1733
|
-
if (!
|
|
1736
|
+
if (!existsSync4(abs)) throw new Error(`DIRECTORY_NOT_FOUND: ${abs}`);
|
|
1734
1737
|
if (!statSync6(abs).isDirectory()) throw new Error(`Not a directory: ${abs}`);
|
|
1735
1738
|
const ig = opts.gitignore ?? true ? loadGitignore(abs) : null;
|
|
1736
1739
|
const matches = [];
|
|
@@ -1771,7 +1774,7 @@ function directoryTree(dirPath, opts = {}) {
|
|
|
1771
1774
|
const compact = opts.format === "compact";
|
|
1772
1775
|
const maxDepth = compact ? 1 : opts.max_depth ?? 3;
|
|
1773
1776
|
const abs = resolve4(normalizePath(dirPath));
|
|
1774
|
-
if (!
|
|
1777
|
+
if (!existsSync4(abs)) throw new Error(`DIRECTORY_NOT_FOUND: ${abs}. Check path or use directory_tree on parent directory.`);
|
|
1775
1778
|
const rootStat = statSync6(abs);
|
|
1776
1779
|
if (!rootStat.isDirectory()) throw new Error(`Not a directory: ${abs}`);
|
|
1777
1780
|
const ig = opts.gitignore ?? true ? loadGitignore(abs) : null;
|
|
@@ -1868,7 +1871,7 @@ ${lines.join("\n")}`;
|
|
|
1868
1871
|
|
|
1869
1872
|
// lib/info.mjs
|
|
1870
1873
|
import { statSync as statSync7, openSync as openSync2, readSync as readSync2, closeSync as closeSync2 } from "node:fs";
|
|
1871
|
-
import { resolve as resolve5, isAbsolute as
|
|
1874
|
+
import { resolve as resolve5, isAbsolute as isAbsolute3, extname as extname2, basename as basename2 } from "node:path";
|
|
1872
1875
|
var MAX_LINE_COUNT_SIZE = 10 * 1024 * 1024;
|
|
1873
1876
|
var EXT_NAMES = {
|
|
1874
1877
|
".ts": "TypeScript source",
|
|
@@ -1937,7 +1940,7 @@ function detectBinary(filePath, size) {
|
|
|
1937
1940
|
function fileInfo(filePath) {
|
|
1938
1941
|
if (!filePath) throw new Error("Empty file path");
|
|
1939
1942
|
const normalized = normalizePath(filePath);
|
|
1940
|
-
const abs =
|
|
1943
|
+
const abs = isAbsolute3(normalized) ? normalized : resolve5(process.cwd(), normalized);
|
|
1941
1944
|
const stat = statSync7(abs);
|
|
1942
1945
|
if (!stat.isFile()) throw new Error(`Not a regular file: ${abs}`);
|
|
1943
1946
|
const size = stat.size;
|
|
@@ -1961,7 +1964,7 @@ function fileInfo(filePath) {
|
|
|
1961
1964
|
}
|
|
1962
1965
|
|
|
1963
1966
|
// lib/setup.mjs
|
|
1964
|
-
import { readFileSync as readFileSync4, writeFileSync as writeFileSync2, existsSync as
|
|
1967
|
+
import { readFileSync as readFileSync4, writeFileSync as writeFileSync2, existsSync as existsSync5, mkdirSync } from "node:fs";
|
|
1965
1968
|
import { resolve as resolve6, dirname as dirname3 } from "node:path";
|
|
1966
1969
|
import { fileURLToPath } from "node:url";
|
|
1967
1970
|
import { homedir } from "node:os";
|
|
@@ -1989,7 +1992,7 @@ var CLAUDE_HOOKS = {
|
|
|
1989
1992
|
}
|
|
1990
1993
|
};
|
|
1991
1994
|
function readJson(filePath) {
|
|
1992
|
-
if (!
|
|
1995
|
+
if (!existsSync5(filePath)) return null;
|
|
1993
1996
|
return JSON.parse(readFileSync4(filePath, "utf-8"));
|
|
1994
1997
|
}
|
|
1995
1998
|
function writeJson(filePath, data) {
|
|
@@ -2358,12 +2361,33 @@ ${results.join("\n\n")}` : header;
|
|
|
2358
2361
|
}
|
|
2359
2362
|
|
|
2360
2363
|
// server.mjs
|
|
2361
|
-
var version = true ? "1.3.
|
|
2364
|
+
var version = true ? "1.3.6" : (await null).createRequire(import.meta.url)("./package.json").version;
|
|
2362
2365
|
var { server, StdioServerTransport } = await createServerRuntime({
|
|
2363
2366
|
name: "hex-line-mcp",
|
|
2364
2367
|
version,
|
|
2365
2368
|
installDir: "mcp/hex-line-mcp"
|
|
2366
2369
|
});
|
|
2370
|
+
var replacementPairsSchema = z2.array(
|
|
2371
|
+
z2.object({ old: z2.string().min(1), new: z2.string() })
|
|
2372
|
+
).min(1);
|
|
2373
|
+
function coerceEdit(e) {
|
|
2374
|
+
if (!e || typeof e !== "object" || Array.isArray(e)) return e;
|
|
2375
|
+
if (e.set_line || e.replace_lines || e.insert_after || e.replace_between || e.replace) return e;
|
|
2376
|
+
if (e.anchor && !e.start_anchor && !e.end_anchor && !e.boundary_mode && !e.range_checksum) {
|
|
2377
|
+
const raw = e.new_text ?? e.updated_lines ?? e.content ?? e.line;
|
|
2378
|
+
if (raw !== void 0) {
|
|
2379
|
+
const text = Array.isArray(raw) ? raw.join("\n") : raw;
|
|
2380
|
+
return { set_line: { anchor: e.anchor, new_text: text } };
|
|
2381
|
+
}
|
|
2382
|
+
}
|
|
2383
|
+
if (e.start_anchor && e.end_anchor && e.boundary_mode && e.new_text !== void 0) {
|
|
2384
|
+
return { replace_between: { start_anchor: e.start_anchor, end_anchor: e.end_anchor, new_text: e.new_text, boundary_mode: e.boundary_mode } };
|
|
2385
|
+
}
|
|
2386
|
+
if (e.start_anchor && e.end_anchor && e.new_text !== void 0) {
|
|
2387
|
+
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 } : {} } };
|
|
2388
|
+
}
|
|
2389
|
+
return e;
|
|
2390
|
+
}
|
|
2367
2391
|
server.registerTool("read_file", {
|
|
2368
2392
|
title: "Read File",
|
|
2369
2393
|
description: "Read a file with hash-annotated lines, range checksums, and current revision. Use offset/limit for targeted reads; use outline first for large code files.",
|
|
@@ -2402,8 +2426,8 @@ server.registerTool("edit_file", {
|
|
|
2402
2426
|
description: "Apply revision-aware partial edits to one file. Prefer one batched call per file. Supports set_line, replace_lines, insert_after, and replace_between. For text rename/refactor use bulk_replace.",
|
|
2403
2427
|
inputSchema: z2.object({
|
|
2404
2428
|
path: z2.string().describe("File to edit"),
|
|
2405
|
-
edits: z2.string().describe(
|
|
2406
|
-
'JSON array.
|
|
2429
|
+
edits: z2.union([z2.string(), z2.array(z2.any())]).describe(
|
|
2430
|
+
'JSON array. Types: set_line, replace_lines, insert_after, replace_between.\n[{"set_line":{"anchor":"ab.12","new_text":"x"}}]\n[{"replace_lines":{"start_anchor":"ab.10","end_anchor":"cd.15","new_text":"x","range_checksum":"10-15:a1b2"}}]\n[{"replace_between":{"start_anchor":"ab.10","end_anchor":"cd.40","new_text":"x","boundary_mode":"inclusive"}}]\n[{"insert_after":{"anchor":"ab.20","text":"x"}}]'
|
|
2407
2431
|
),
|
|
2408
2432
|
dry_run: flexBool().describe("Preview changes without writing"),
|
|
2409
2433
|
restore_indent: flexBool().describe("Auto-fix indentation to match anchor (default: false)"),
|
|
@@ -2414,12 +2438,18 @@ server.registerTool("edit_file", {
|
|
|
2414
2438
|
}, async (rawParams) => {
|
|
2415
2439
|
const { path: p, edits: json, dry_run, restore_indent, base_revision, conflict_policy } = coerceParams(rawParams);
|
|
2416
2440
|
try {
|
|
2417
|
-
|
|
2441
|
+
let parsed;
|
|
2442
|
+
try {
|
|
2443
|
+
parsed = typeof json === "string" ? JSON.parse(json) : json;
|
|
2444
|
+
} catch {
|
|
2445
|
+
throw new Error('edits: invalid JSON. Expected: [{"set_line":{"anchor":"xx.N","new_text":"..."}}]');
|
|
2446
|
+
}
|
|
2418
2447
|
if (!Array.isArray(parsed) || !parsed.length) throw new Error("Edits: non-empty JSON array required");
|
|
2448
|
+
const normalized = parsed.map(coerceEdit);
|
|
2419
2449
|
return {
|
|
2420
2450
|
content: [{
|
|
2421
2451
|
type: "text",
|
|
2422
|
-
text: editFile(p,
|
|
2452
|
+
text: editFile(p, normalized, {
|
|
2423
2453
|
dryRun: dry_run,
|
|
2424
2454
|
restoreIndent: restore_indent,
|
|
2425
2455
|
baseRevision: base_revision,
|
|
@@ -2452,7 +2482,7 @@ server.registerTool("write_file", {
|
|
|
2452
2482
|
});
|
|
2453
2483
|
server.registerTool("grep_search", {
|
|
2454
2484
|
title: "Search Files",
|
|
2455
|
-
description: "Search file contents with ripgrep. Returns hash-annotated matches with
|
|
2485
|
+
description: "Search file contents with ripgrep. Returns hash-annotated matches with checksums. Modes: content (default), files, count. Use checksums with set_line/replace_lines. Prefer over shell grep/rg.",
|
|
2456
2486
|
inputSchema: z2.object({
|
|
2457
2487
|
pattern: z2.string().describe("Search pattern (regex by default, literal if literal:true)"),
|
|
2458
2488
|
path: z2.string().optional().describe("Search dir/file (default: cwd)"),
|
|
@@ -2513,7 +2543,7 @@ server.registerTool("grep_search", {
|
|
|
2513
2543
|
});
|
|
2514
2544
|
server.registerTool("outline", {
|
|
2515
2545
|
title: "File Outline",
|
|
2516
|
-
description: "AST-based structural outline: functions, classes, interfaces with line ranges.
|
|
2546
|
+
description: "AST-based structural outline: functions, classes, interfaces with line ranges. Use before reading large code files. Not for .md/.json/.yaml.",
|
|
2517
2547
|
inputSchema: z2.object({
|
|
2518
2548
|
path: z2.string().describe("Source file path")
|
|
2519
2549
|
}),
|
|
@@ -2548,7 +2578,7 @@ server.registerTool("verify", {
|
|
|
2548
2578
|
});
|
|
2549
2579
|
server.registerTool("directory_tree", {
|
|
2550
2580
|
title: "Directory Tree",
|
|
2551
|
-
description: "
|
|
2581
|
+
description: "Directory tree with .gitignore support. Pattern glob to find files/dirs by name. Skips node_modules, .git, dist.",
|
|
2552
2582
|
inputSchema: z2.object({
|
|
2553
2583
|
path: z2.string().describe("Directory path"),
|
|
2554
2584
|
pattern: z2.string().optional().describe('Glob filter on names (e.g. "*-mcp", "*.mjs"). Returns flat match list instead of tree'),
|
|
@@ -2583,7 +2613,7 @@ server.registerTool("get_file_info", {
|
|
|
2583
2613
|
});
|
|
2584
2614
|
server.registerTool("setup_hooks", {
|
|
2585
2615
|
title: "Setup Hooks",
|
|
2586
|
-
description: "Install or uninstall hex-line hooks in CLI agent settings.
|
|
2616
|
+
description: "Install or uninstall hex-line hooks in CLI agent settings. Idempotent.",
|
|
2587
2617
|
inputSchema: z2.object({
|
|
2588
2618
|
agent: z2.string().optional().describe('Target agent: "claude", "gemini", "codex", or "all" (default: "all")'),
|
|
2589
2619
|
action: z2.string().optional().describe('"install" (default) or "uninstall"')
|
|
@@ -2599,7 +2629,7 @@ server.registerTool("setup_hooks", {
|
|
|
2599
2629
|
});
|
|
2600
2630
|
server.registerTool("changes", {
|
|
2601
2631
|
title: "Semantic Diff",
|
|
2602
|
-
description: "Compare file or directory against git ref (default: HEAD).
|
|
2632
|
+
description: "Compare file or directory against git ref (default: HEAD). Shows added/removed/modified symbols or file stats.",
|
|
2603
2633
|
inputSchema: z2.object({
|
|
2604
2634
|
path: z2.string().describe("File or directory path"),
|
|
2605
2635
|
compare_against: z2.string().optional().describe('Git ref to compare against (default: "HEAD")')
|
|
@@ -2617,7 +2647,7 @@ server.registerTool("bulk_replace", {
|
|
|
2617
2647
|
title: "Bulk Replace",
|
|
2618
2648
|
description: "Search-and-replace across multiple files. Finds files by glob, applies ordered text replacements, returns per-file diffs. Use dry_run:true to preview. For single-file rename, set glob to the filename.",
|
|
2619
2649
|
inputSchema: z2.object({
|
|
2620
|
-
replacements: z2.string().describe('JSON array of {old, new} pairs: [{"old":"foo","new":"bar"}]'),
|
|
2650
|
+
replacements: z2.union([z2.string(), replacementPairsSchema]).describe('JSON array of {old, new} pairs: [{"old":"foo","new":"bar"}]'),
|
|
2621
2651
|
glob: z2.string().optional().describe('File glob (default: "**/*.{md,mjs,json,yml,ts,js}")'),
|
|
2622
2652
|
path: z2.string().optional().describe("Root directory (default: cwd)"),
|
|
2623
2653
|
dry_run: flexBool().describe("Preview without writing (default: false)"),
|
|
@@ -2627,8 +2657,14 @@ server.registerTool("bulk_replace", {
|
|
|
2627
2657
|
}, async (rawParams) => {
|
|
2628
2658
|
try {
|
|
2629
2659
|
const params = coerceParams(rawParams);
|
|
2630
|
-
const
|
|
2631
|
-
|
|
2660
|
+
const raw = params.replacements;
|
|
2661
|
+
let replacementsInput;
|
|
2662
|
+
try {
|
|
2663
|
+
replacementsInput = typeof raw === "string" ? JSON.parse(raw) : raw;
|
|
2664
|
+
} catch {
|
|
2665
|
+
throw new Error('replacements: invalid JSON. Expected: [{"old":"text","new":"replacement"}]');
|
|
2666
|
+
}
|
|
2667
|
+
const replacements = replacementPairsSchema.parse(replacementsInput);
|
|
2632
2668
|
const result = bulkReplace(
|
|
2633
2669
|
params.path || process.cwd(),
|
|
2634
2670
|
params.glob || "**/*.{md,mjs,json,yml,ts,js}",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@levnikolaevich/hex-line-mcp",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.6",
|
|
4
4
|
"mcpName": "io.github.levnikolaevich/hex-line-mcp",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Hash-verified file editing MCP + token efficiency hook for AI coding agents. 11 tools: read, edit, write, grep, outline, verify, directory_tree, file_info, setup_hooks, changes, bulk_replace.",
|