@levnikolaevich/hex-line-mcp 1.30.0 → 1.31.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 +6 -4
- package/dist/hook.mjs +34 -8
- package/dist/server.mjs +126 -31
- package/output-style.md +3 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -32,8 +32,8 @@ Advanced / occasional:
|
|
|
32
32
|
|
|
33
33
|
| Tool | Description | Key Feature |
|
|
34
34
|
|------|-------------|-------------|
|
|
35
|
-
| `read_file` | Read file with progressive disclosure, optional edit-ready metadata, and automatic graph hints when available | Minimal plain discovery by default
|
|
36
|
-
| `edit_file` | Revision-aware anchor edits (`set_line`, `replace_lines`, `insert_after`, `replace_between`) |
|
|
35
|
+
| `read_file` | Read file with progressive disclosure, optional edit-ready metadata, and automatic graph hints when available | Minimal plain discovery by default (no hash overhead); per-line hashes are added only under `edit_ready`/`verbosity=full` for verified edits |
|
|
36
|
+
| `edit_file` | Revision-aware anchor edits (`set_line`, `replace_lines`, `insert_after`, `replace_between`) | Batch all hunks for one file in a single call (avoids stale-conflict churn); forgiving anchors (`tag.N`, a bare line number, or unique line content) and `range_checksum: "auto"` |
|
|
37
37
|
| `write_file` | Create new file or overwrite, auto-creates parent dirs | Path validation, no hash overhead |
|
|
38
38
|
| `grep_search` | Search with ripgrep, summary-first discovery, and optional edit-ready hunks | `summary` by default, capped `content` mode with explicit `allow_large_output` escape hatch |
|
|
39
39
|
| `outline` | AST-based structural overview with hash anchors via tree-sitter WASM. Supports JavaScript/TypeScript, Python, C#, PHP, and fence-aware markdown headings | 95% token reduction, direct edit anchors |
|
|
@@ -47,7 +47,7 @@ Advanced / occasional:
|
|
|
47
47
|
| Event | Trigger | Action |
|
|
48
48
|
|-------|---------|--------|
|
|
49
49
|
| **PreToolUse** | Read/Edit/Write/Grep/Glob on project text scope | Advises hex-line by default for project-scoped text files and file discovery; explicit `hooks.mode: "blocking"` hard redirects. Built-in tools stay available for binary/media, plan files in Plan Mode, and text paths outside the current project root |
|
|
50
|
-
| **PreToolUse** | Bash with dangerous commands | Blocks `rm -rf /`, `git push --force`, etc. Agent must confirm with user |
|
|
50
|
+
| **PreToolUse** | Bash with dangerous commands | Blocks `rm -rf /`, `git push --force`, `git clean -f`, `git branch -D`, `git rebase -i`, `truncate -s`, `docker prune`, `docker rmi -f`, `chmod 777`, `DROP TABLE`, `mkfs`, `dd if=/dev/zero`, etc. Agent must confirm with user |
|
|
51
51
|
| **PostToolUse** | Bash with 50+ lines output | RTK: deduplicates, truncates, shows filtered summary to Claude as feedback |
|
|
52
52
|
| **SessionStart** | Session begins | Injects a short bootstrap hint; defers to the active output style when `hex-line` style is enabled |
|
|
53
53
|
|
|
@@ -70,12 +70,14 @@ Requires Node.js >= 20.19.0.
|
|
|
70
70
|
|
|
71
71
|
### Hooks
|
|
72
72
|
|
|
73
|
-
Hooks and output style are auto-synced on every MCP server startup. The server compares installed files with bundled versions and updates only when content differs. First run after `npm i -g` triggers full install automatically.
|
|
73
|
+
Hooks and output style are auto-synced on every MCP server startup (unless disabled — see below). The server compares installed files with bundled versions and updates only when content differs. First run after `npm i -g` triggers full install automatically.
|
|
74
74
|
|
|
75
75
|
Hooks are written to global `~/.claude/settings.json` with absolute path to `hook.mjs`. Output style is installed to `~/.claude/output-styles/hex-line.md` and activated if no other style is set. To activate manually: `/config` > Output style > hex-line.
|
|
76
76
|
|
|
77
77
|
No extra manual setup is required after install. The startup sync uses the current Node runtime and a stable hook path under `~/.claude/hex-line`, so the hook command survives spaces in the home directory on Windows, macOS, and Linux.
|
|
78
78
|
|
|
79
|
+
To opt out of startup syncing entirely (hook copy, output-style install/activation, and `~/.claude/settings.json` writes), set `hooks.auto_sync: false` in the project's `.hex-skills/environment_state.json`. The output style is only auto-activated when no other style is set; it never overrides a style you chose.
|
|
80
|
+
|
|
79
81
|
## Validation
|
|
80
82
|
|
|
81
83
|
Use the normal package checks:
|
package/dist/hook.mjs
CHANGED
|
@@ -54,8 +54,8 @@ function normalizeOutput(text, opts = {}) {
|
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
// hook.mjs
|
|
57
|
-
import { readFileSync, writeSync } from "node:fs";
|
|
58
|
-
import { resolve } from "node:path";
|
|
57
|
+
import { readFileSync, writeSync, realpathSync, existsSync } from "node:fs";
|
|
58
|
+
import { resolve, dirname, basename, join } from "node:path";
|
|
59
59
|
import { homedir } from "node:os";
|
|
60
60
|
|
|
61
61
|
// lib/hook-policy.mjs
|
|
@@ -160,9 +160,15 @@ var DANGEROUS_PATTERNS = [
|
|
|
160
160
|
{ regex: /git\s+push\s+(-f|--force)/, reason: "force push can overwrite remote history" },
|
|
161
161
|
{ regex: /git\s+reset\s+--hard/, reason: "hard reset discards uncommitted changes" },
|
|
162
162
|
{ regex: /DROP\s+(TABLE|DATABASE)/i, reason: "DROP destroys data permanently" },
|
|
163
|
-
{ regex: /chmod\s+777
|
|
163
|
+
{ regex: /chmod\s+(-[a-zA-Z]+\s+)*(777|a\+rwx)\b/i, reason: "chmod 777 / a+rwx removes all access restrictions" },
|
|
164
164
|
{ regex: /mkfs/, reason: "filesystem format destroys all data" },
|
|
165
|
-
{ regex: /dd\s+if=\/dev\/zero/, reason: "direct disk write destroys data" }
|
|
165
|
+
{ regex: /dd\s+if=\/dev\/zero/, reason: "direct disk write destroys data" },
|
|
166
|
+
{ regex: /git\s+branch\s+-D\b/, reason: "git branch -D force-deletes branches" },
|
|
167
|
+
{ regex: /git\s+clean\s+(-[a-z]*f|--force)/i, reason: "git clean -f deletes untracked files permanently" },
|
|
168
|
+
{ regex: /git\s+rebase\s+(-i|--interactive)\b/, reason: "interactive rebase rewrites history and blocks on an editor" },
|
|
169
|
+
{ regex: /\btruncate\s+-s/i, reason: "truncate -s can zero out file contents" },
|
|
170
|
+
{ regex: /docker\s+(system|volume|image|container|network)\s+prune\b/i, reason: "docker prune destroys unused data permanently" },
|
|
171
|
+
{ regex: /docker\s+rmi\s+(-f|--force)/i, reason: "docker rmi -f force-removes images" }
|
|
166
172
|
];
|
|
167
173
|
var COMPOUND_OPERATORS = /[|]|>>?|&&|\|\||;/;
|
|
168
174
|
var CMD_PATTERNS = [
|
|
@@ -197,8 +203,8 @@ var HEX_LINE_MUTATING = /* @__PURE__ */ new Set([
|
|
|
197
203
|
"mcp__hex-line__bulk_replace"
|
|
198
204
|
]);
|
|
199
205
|
var PLAN_SAFE_FOLDERS = [
|
|
200
|
-
".hex-skills
|
|
201
|
-
".claude
|
|
206
|
+
".hex-skills",
|
|
207
|
+
".claude"
|
|
202
208
|
];
|
|
203
209
|
function extOf(filePath) {
|
|
204
210
|
const dot = filePath.lastIndexOf(".");
|
|
@@ -284,8 +290,27 @@ function collectPositionals(tokens, { optionValueFlags = [], slashOptions = fals
|
|
|
284
290
|
function resolveCandidatePaths(pathTokens) {
|
|
285
291
|
return pathTokens.map(resolveToolPath).filter(Boolean).map(normalizePolicyPath);
|
|
286
292
|
}
|
|
293
|
+
function canonicalizeForScope(absPath) {
|
|
294
|
+
try {
|
|
295
|
+
return realpathSync(absPath);
|
|
296
|
+
} catch {
|
|
297
|
+
try {
|
|
298
|
+
let dir = absPath;
|
|
299
|
+
const tail = [];
|
|
300
|
+
while (dir && dir !== dirname(dir) && !existsSync(dir)) {
|
|
301
|
+
tail.unshift(basename(dir));
|
|
302
|
+
dir = dirname(dir);
|
|
303
|
+
}
|
|
304
|
+
if (existsSync(dir)) return tail.length ? join(realpathSync(dir), ...tail) : realpathSync(dir);
|
|
305
|
+
} catch {
|
|
306
|
+
}
|
|
307
|
+
return absPath;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
287
310
|
function isProjectScopedPath(filePath) {
|
|
288
|
-
|
|
311
|
+
if (!filePath) return false;
|
|
312
|
+
const resolved = canonicalizeForScope(resolveToolPath(filePath));
|
|
313
|
+
return isWithinDir(resolved, canonicalizeForScope(process.cwd()));
|
|
289
314
|
}
|
|
290
315
|
function getBashPathCandidates(spec, tokens, isFirstSegment) {
|
|
291
316
|
const command = (tokens[0] || "").toLowerCase();
|
|
@@ -453,7 +478,8 @@ function handlePreToolUse(data) {
|
|
|
453
478
|
const toolInput = data.tool_input || {};
|
|
454
479
|
if (data.permission_mode === "plan" && HEX_LINE_MUTATING.has(toolName)) {
|
|
455
480
|
const targetPath = getMutatingTargetPath(toolInput).replace(/\\/g, "/");
|
|
456
|
-
const
|
|
481
|
+
const planParts = targetPath.split("/").filter(Boolean);
|
|
482
|
+
const isPlanSafe = PLAN_SAFE_FOLDERS.some((folder) => planParts.includes(folder));
|
|
457
483
|
if (!isPlanSafe) {
|
|
458
484
|
block(
|
|
459
485
|
"PLAN_MODE: You are in planning mode. Write your plan to the plan file, then call ExitPlanMode to get approval before making changes.",
|
package/dist/server.mjs
CHANGED
|
@@ -1680,6 +1680,11 @@ function requireOptionalString(value, path) {
|
|
|
1680
1680
|
throw badInput(`${path} must be a string when provided`);
|
|
1681
1681
|
}
|
|
1682
1682
|
}
|
|
1683
|
+
function requireChecksumField(value, path) {
|
|
1684
|
+
if (typeof value !== "string") {
|
|
1685
|
+
throw badInput(`${path} must be a string: either "<start>-<end>:<hex>" copied from a fresh read, or "auto" to compute it for the current anchor range`);
|
|
1686
|
+
}
|
|
1687
|
+
}
|
|
1683
1688
|
function validateEditShape(edit) {
|
|
1684
1689
|
requirePlainObject(edit, "edit");
|
|
1685
1690
|
const kinds = ["set_line", "replace_lines", "insert_after", "replace_between"].filter((kind2) => edit[kind2] !== void 0);
|
|
@@ -1707,7 +1712,7 @@ function validateEditShape(edit) {
|
|
|
1707
1712
|
requirePlainObject(edit.replace_lines, "replace_lines");
|
|
1708
1713
|
requireString(edit.replace_lines.start_anchor, "replace_lines.start_anchor");
|
|
1709
1714
|
requireString(edit.replace_lines.end_anchor, "replace_lines.end_anchor");
|
|
1710
|
-
|
|
1715
|
+
requireChecksumField(edit.replace_lines.range_checksum, "replace_lines.range_checksum");
|
|
1711
1716
|
requireString(edit.replace_lines.new_text, "replace_lines.new_text");
|
|
1712
1717
|
return;
|
|
1713
1718
|
}
|
|
@@ -2937,6 +2942,92 @@ Recovery: read_file path ranges=["${csStart}-${csEnd}"], then retry edit with fr
|
|
|
2937
2942
|
}
|
|
2938
2943
|
return null;
|
|
2939
2944
|
}
|
|
2945
|
+
var CANONICAL_REF_RE = /^[a-z2-7]{2}\.\d+$/;
|
|
2946
|
+
var CANONICAL_CHECKSUM_RE = /^\d+-\d+:[0-9a-f]{8}$/;
|
|
2947
|
+
function uniqueLineIndex(lines, pred) {
|
|
2948
|
+
let found = -1;
|
|
2949
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2950
|
+
if (!pred(lines[i])) continue;
|
|
2951
|
+
if (found >= 0) return -1;
|
|
2952
|
+
found = i;
|
|
2953
|
+
}
|
|
2954
|
+
return found;
|
|
2955
|
+
}
|
|
2956
|
+
function refLineOrNull(anchor) {
|
|
2957
|
+
const m = typeof anchor === "string" ? anchor.trim().match(/^[a-z2-7]{2}\.(\d+)$/) : null;
|
|
2958
|
+
return m ? parseInt(m[1], 10) : null;
|
|
2959
|
+
}
|
|
2960
|
+
function lineContentMatches(line, candidate) {
|
|
2961
|
+
if (line === candidate) return true;
|
|
2962
|
+
const c = candidate.trim();
|
|
2963
|
+
return c.length > 0 && line.replace(/\s+/g, "") === candidate.replace(/\s+/g, "");
|
|
2964
|
+
}
|
|
2965
|
+
function reconcileAnchorRef(rawAnchor, lines) {
|
|
2966
|
+
if (typeof rawAnchor !== "string") return rawAnchor;
|
|
2967
|
+
const s = rawAnchor.trim();
|
|
2968
|
+
if (!s || CANONICAL_REF_RE.test(s)) return rawAnchor;
|
|
2969
|
+
const tagAt = (n) => `${lineTag(fnv1a(lines[n - 1]))}.${n}`;
|
|
2970
|
+
if (/^\d+$/.test(s)) {
|
|
2971
|
+
const n = parseInt(s, 10);
|
|
2972
|
+
return n >= 1 && n <= lines.length ? tagAt(n) : rawAnchor;
|
|
2973
|
+
}
|
|
2974
|
+
const dotN = s.match(/^([\s\S]*)\.(\d+)$/);
|
|
2975
|
+
if (dotN) {
|
|
2976
|
+
const n = parseInt(dotN[2], 10);
|
|
2977
|
+
if (n >= 1 && n <= lines.length && lineContentMatches(lines[n - 1], dotN[1])) return tagAt(n);
|
|
2978
|
+
}
|
|
2979
|
+
const exact = uniqueLineIndex(lines, (l) => l === s);
|
|
2980
|
+
if (exact >= 0) return tagAt(exact + 1);
|
|
2981
|
+
const compact = s.replace(/\s+/g, "");
|
|
2982
|
+
if (compact) {
|
|
2983
|
+
const stripped = uniqueLineIndex(lines, (l) => l.replace(/\s+/g, "") === compact);
|
|
2984
|
+
if (stripped >= 0) return tagAt(stripped + 1);
|
|
2985
|
+
}
|
|
2986
|
+
return rawAnchor;
|
|
2987
|
+
}
|
|
2988
|
+
function reconcileRangeChecksum(rawChecksum, startLine, endLine, snapshot) {
|
|
2989
|
+
if (rawChecksum === void 0) return rawChecksum;
|
|
2990
|
+
const s = String(rawChecksum).trim();
|
|
2991
|
+
if (CANONICAL_CHECKSUM_RE.test(s)) return rawChecksum;
|
|
2992
|
+
if (s === "" || /^(?:\d+-\d+:)?auto$/i.test(s)) {
|
|
2993
|
+
return buildRangeChecksum(snapshot, startLine, endLine) || rawChecksum;
|
|
2994
|
+
}
|
|
2995
|
+
return rawChecksum;
|
|
2996
|
+
}
|
|
2997
|
+
function reconcileEdits(edits, snapshot, corrections) {
|
|
2998
|
+
if (!Array.isArray(edits)) return edits;
|
|
2999
|
+
const lines = snapshot.lines;
|
|
3000
|
+
const note = (from, to) => {
|
|
3001
|
+
if (to !== from && typeof from === "string" && from.trim() !== to) corrections.push({ from: from.trim(), to });
|
|
3002
|
+
};
|
|
3003
|
+
return edits.map((edit) => {
|
|
3004
|
+
if (!edit || typeof edit !== "object" || Array.isArray(edit)) return edit;
|
|
3005
|
+
const out = { ...edit };
|
|
3006
|
+
for (const kind of ["set_line", "insert_after"]) {
|
|
3007
|
+
const blk = out[kind];
|
|
3008
|
+
if (!blk || typeof blk !== "object" || Array.isArray(blk)) continue;
|
|
3009
|
+
const anchor = reconcileAnchorRef(blk.anchor, lines);
|
|
3010
|
+
note(blk.anchor, anchor);
|
|
3011
|
+
out[kind] = { ...blk, anchor };
|
|
3012
|
+
}
|
|
3013
|
+
for (const kind of ["replace_lines", "replace_between"]) {
|
|
3014
|
+
const blk = out[kind];
|
|
3015
|
+
if (!blk || typeof blk !== "object" || Array.isArray(blk)) continue;
|
|
3016
|
+
const start_anchor = reconcileAnchorRef(blk.start_anchor, lines);
|
|
3017
|
+
const end_anchor = reconcileAnchorRef(blk.end_anchor, lines);
|
|
3018
|
+
note(blk.start_anchor, start_anchor);
|
|
3019
|
+
note(blk.end_anchor, end_anchor);
|
|
3020
|
+
const next = { ...blk, start_anchor, end_anchor };
|
|
3021
|
+
const sLine = refLineOrNull(start_anchor);
|
|
3022
|
+
const eLine = refLineOrNull(end_anchor);
|
|
3023
|
+
if (sLine && eLine && (kind === "replace_lines" || blk.range_checksum !== void 0)) {
|
|
3024
|
+
next.range_checksum = reconcileRangeChecksum(blk.range_checksum, sLine, eLine, snapshot);
|
|
3025
|
+
}
|
|
3026
|
+
out[kind] = next;
|
|
3027
|
+
}
|
|
3028
|
+
return out;
|
|
3029
|
+
});
|
|
3030
|
+
}
|
|
2940
3031
|
function editFile(filePath, edits, opts = {}) {
|
|
2941
3032
|
filePath = normalizePath(filePath);
|
|
2942
3033
|
const real = validatePath(filePath);
|
|
@@ -2955,7 +3046,15 @@ function editFile(filePath, edits, opts = {}) {
|
|
|
2955
3046
|
let autoRebased = false;
|
|
2956
3047
|
const remaps = [];
|
|
2957
3048
|
const remapKeys = /* @__PURE__ */ new Set();
|
|
2958
|
-
const
|
|
3049
|
+
const reconcileCorrections = [];
|
|
3050
|
+
const reconciledEdits = reconcileEdits(edits, currentSnapshot, reconcileCorrections);
|
|
3051
|
+
for (const c of reconcileCorrections) {
|
|
3052
|
+
const key = `${c.from}->${c.to}`;
|
|
3053
|
+
if (remapKeys.has(key)) continue;
|
|
3054
|
+
remapKeys.add(key);
|
|
3055
|
+
remaps.push(c);
|
|
3056
|
+
}
|
|
3057
|
+
const anchored = normalizeAnchoredEdits(reconciledEdits);
|
|
2959
3058
|
assertNonOverlappingTargets(collectEditTargets(anchored));
|
|
2960
3059
|
const sorted = sortEditsForApply(anchored);
|
|
2961
3060
|
const buildStrictConflictError = (reason, centerIdx, details, recoveryRanges = null, retryChecksum = null, retryEdit = null) => {
|
|
@@ -3325,14 +3424,18 @@ function applyListModeTotalLimit(lines, totalLimit) {
|
|
|
3325
3424
|
visible.push(`OUTPUT_CAPPED: ${lines.length - totalLimit} more result line(s) omitted. Narrow with path= or glob=, or raise head_limit.`);
|
|
3326
3425
|
return visible.join("\n");
|
|
3327
3426
|
}
|
|
3427
|
+
function appendCommonRgFlags(args, opts) {
|
|
3428
|
+
if (opts.caseInsensitive) args.push("-i");
|
|
3429
|
+
else if (opts.smartCase) args.push("-S");
|
|
3430
|
+
if (opts.literal) args.push("-F");
|
|
3431
|
+
if (opts.multiline) args.push("-U", "--multiline-dotall");
|
|
3432
|
+
if (opts.glob) args.push("--glob", opts.glob);
|
|
3433
|
+
if (opts.type) args.push("--type", opts.type);
|
|
3434
|
+
return args;
|
|
3435
|
+
}
|
|
3328
3436
|
async function filesMode(pattern, target, opts, totalLimit) {
|
|
3329
3437
|
const realArgs = ["-l"];
|
|
3330
|
-
|
|
3331
|
-
else if (opts.smartCase) realArgs.push("-S");
|
|
3332
|
-
if (opts.literal) realArgs.push("-F");
|
|
3333
|
-
if (opts.multiline) realArgs.push("-U", "--multiline-dotall");
|
|
3334
|
-
if (opts.glob) realArgs.push("--glob", opts.glob);
|
|
3335
|
-
if (opts.type) realArgs.push("--type", opts.type);
|
|
3438
|
+
appendCommonRgFlags(realArgs, opts);
|
|
3336
3439
|
realArgs.push("--", pattern, target);
|
|
3337
3440
|
const { stdout, code, stderr, killed } = await spawnRg(realArgs);
|
|
3338
3441
|
if (killed) return "GREP_OUTPUT_TRUNCATED: exceeded 10MB. Use specific glob/path.";
|
|
@@ -3344,12 +3447,7 @@ async function filesMode(pattern, target, opts, totalLimit) {
|
|
|
3344
3447
|
}
|
|
3345
3448
|
async function countMode(pattern, target, opts, totalLimit) {
|
|
3346
3449
|
const realArgs = ["-c"];
|
|
3347
|
-
|
|
3348
|
-
else if (opts.smartCase) realArgs.push("-S");
|
|
3349
|
-
if (opts.literal) realArgs.push("-F");
|
|
3350
|
-
if (opts.multiline) realArgs.push("-U", "--multiline-dotall");
|
|
3351
|
-
if (opts.glob) realArgs.push("--glob", opts.glob);
|
|
3352
|
-
if (opts.type) realArgs.push("--type", opts.type);
|
|
3450
|
+
appendCommonRgFlags(realArgs, opts);
|
|
3353
3451
|
realArgs.push("--", pattern, target);
|
|
3354
3452
|
const { stdout, code, stderr, killed } = await spawnRg(realArgs);
|
|
3355
3453
|
if (killed) return "GREP_OUTPUT_TRUNCATED: exceeded 10MB. Use specific glob/path.";
|
|
@@ -3361,12 +3459,7 @@ async function countMode(pattern, target, opts, totalLimit) {
|
|
|
3361
3459
|
}
|
|
3362
3460
|
async function summaryMode(pattern, target, opts, totalLimit) {
|
|
3363
3461
|
const realArgs = ["-n", "-H", "--no-heading", "--color", "never"];
|
|
3364
|
-
|
|
3365
|
-
else if (opts.smartCase) realArgs.push("-S");
|
|
3366
|
-
if (opts.literal) realArgs.push("-F");
|
|
3367
|
-
if (opts.multiline) realArgs.push("-U", "--multiline-dotall");
|
|
3368
|
-
if (opts.glob) realArgs.push("--glob", opts.glob);
|
|
3369
|
-
if (opts.type) realArgs.push("--type", opts.type);
|
|
3462
|
+
appendCommonRgFlags(realArgs, opts);
|
|
3370
3463
|
const limit = opts.limit && opts.limit > 0 ? opts.limit : 20;
|
|
3371
3464
|
realArgs.push("-m", String(limit));
|
|
3372
3465
|
realArgs.push("--", pattern, target);
|
|
@@ -3415,12 +3508,7 @@ async function contentMode(pattern, target, opts, plain, editReady, totalLimit,
|
|
|
3415
3508
|
const shouldUseGraph = editReady && !plain;
|
|
3416
3509
|
const contentBlockLimit = allowLargeOutput ? Number.POSITIVE_INFINITY : DEFAULT_CONTENT_BLOCK_LIMIT;
|
|
3417
3510
|
const outputCharBudget = allowLargeOutput ? MAX_SEARCH_OUTPUT_CHARS : DEFAULT_CONTENT_OUTPUT_CHARS;
|
|
3418
|
-
|
|
3419
|
-
else if (opts.smartCase) realArgs.push("-S");
|
|
3420
|
-
if (opts.literal) realArgs.push("-F");
|
|
3421
|
-
if (opts.multiline) realArgs.push("-U", "--multiline-dotall");
|
|
3422
|
-
if (opts.glob) realArgs.push("--glob", opts.glob);
|
|
3423
|
-
if (opts.type) realArgs.push("--type", opts.type);
|
|
3511
|
+
appendCommonRgFlags(realArgs, opts);
|
|
3424
3512
|
if (opts.context && opts.context > 0) realArgs.push("-C", String(opts.context));
|
|
3425
3513
|
if (opts.contextBefore && opts.contextBefore > 0) realArgs.push("-B", String(opts.contextBefore));
|
|
3426
3514
|
if (opts.contextAfter && opts.contextAfter > 0) realArgs.push("-A", String(opts.contextAfter));
|
|
@@ -4389,6 +4477,11 @@ function safeRead(filePath) {
|
|
|
4389
4477
|
return null;
|
|
4390
4478
|
}
|
|
4391
4479
|
}
|
|
4480
|
+
function isAutoSyncDisabled() {
|
|
4481
|
+
const stateFile = resolve7(process.cwd(), ".hex-skills/environment_state.json");
|
|
4482
|
+
const config = readJson(stateFile);
|
|
4483
|
+
return config?.hooks?.auto_sync === false;
|
|
4484
|
+
}
|
|
4392
4485
|
function writeHooksToFile(settingsPath) {
|
|
4393
4486
|
const config = readJson(settingsPath) || {};
|
|
4394
4487
|
if (!config.hooks || typeof config.hooks !== "object") config.hooks = {};
|
|
@@ -4456,6 +4549,7 @@ function syncOutputStyle() {
|
|
|
4456
4549
|
function autoSync() {
|
|
4457
4550
|
const hookSource = existsSync6(DIST_HOOK) ? DIST_HOOK : existsSync6(BUILT_HOOK) ? BUILT_HOOK : null;
|
|
4458
4551
|
if (!hookSource) return;
|
|
4552
|
+
if (isAutoSyncDisabled()) return;
|
|
4459
4553
|
const changes = [];
|
|
4460
4554
|
const srcHook = safeRead(hookSource);
|
|
4461
4555
|
const dstHook = safeRead(STABLE_HOOK_PATH);
|
|
@@ -5090,14 +5184,15 @@ function classifyMcpFailure(input = {}) {
|
|
|
5090
5184
|
|
|
5091
5185
|
// ../hex-common/src/runtime/results.mjs
|
|
5092
5186
|
var LARGE_RESULT_META = { "anthropic/maxResultSizeChars": 5e5 };
|
|
5093
|
-
function result(structured, { large = false } = {}) {
|
|
5187
|
+
function result(structured, { large = false, isError = null, errorStatuses = ["ERROR"] } = {}) {
|
|
5094
5188
|
const text = JSON.stringify(structured);
|
|
5095
5189
|
const response = {
|
|
5096
5190
|
content: [{ type: "text", text }],
|
|
5097
5191
|
structuredContent: structured
|
|
5098
5192
|
};
|
|
5099
5193
|
if (large) response._meta = LARGE_RESULT_META;
|
|
5100
|
-
|
|
5194
|
+
const resolvedError = isError === null ? new Set(errorStatuses).has(structured?.status) : isError;
|
|
5195
|
+
if (resolvedError) response.isError = true;
|
|
5101
5196
|
return response;
|
|
5102
5197
|
}
|
|
5103
5198
|
function errorResult(code, message, recovery, { large = false, extra = null } = {}) {
|
|
@@ -5129,7 +5224,7 @@ function errorResult(code, message, recovery, { large = false, extra = null } =
|
|
|
5129
5224
|
}
|
|
5130
5225
|
|
|
5131
5226
|
// server.mjs
|
|
5132
|
-
var version = true ? "1.
|
|
5227
|
+
var version = true ? "1.31.0" : (await null).createRequire(import.meta.url)("./package.json").version;
|
|
5133
5228
|
var STATUS_ENUM = z2.enum(STATUS_VALUES);
|
|
5134
5229
|
var ERROR_SHAPE = z2.object({ code: z2.string(), message: z2.string(), recovery: z2.string() }).optional();
|
|
5135
5230
|
var ERROR_RESULT_FIELDS = {
|
|
@@ -5323,11 +5418,11 @@ ERROR: ${e.message}`);
|
|
|
5323
5418
|
});
|
|
5324
5419
|
server.registerTool("edit_file", {
|
|
5325
5420
|
title: "Edit File",
|
|
5326
|
-
description:
|
|
5421
|
+
description: 'Apply hash-verified partial edits to one file. Batch ALL hunks for the same file into ONE call via the `edits` array -- separate sequential edit_file calls on one file go stale and conflict (the most common edit failure). Carry base_revision only for a genuinely later follow-up after the file changed. Anchors accept tag.N, a bare line number, or unique line content (auto-resolved); range_checksum accepts "auto" to compute it for the current range. Preserves existing line endings and trailing-newline shape; conservative conflicts return retry helpers. boundary_mode=inclusive deletes the anchor lines themselves; new_text must close any delimiter whose opening falls inside the replaced range.',
|
|
5327
5422
|
inputSchema: z2.object({
|
|
5328
5423
|
file_path: z2.string().describe("File to edit"),
|
|
5329
5424
|
edits: z2.union([z2.string(), z2.array(z2.any())]).describe(
|
|
5330
|
-
'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","range_checksum":"10-40:a1b2"}}]\nPrefer replace_lines with range_checksum when either anchor is a lone delimiter (}, ), ]) \u2014 replace_between anchors use short line-content hashes and may fuzzy-match a sibling delimiter.'
|
|
5425
|
+
'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","range_checksum":"10-40:a1b2"}}]\nPrefer replace_lines with range_checksum when either anchor is a lone delimiter (}, ), ]) \u2014 replace_between anchors use short line-content hashes and may fuzzy-match a sibling delimiter. Anchors also accept a bare line number or exact line content (auto-resolved to tag.N); range_checksum accepts "auto" to compute it for the current anchor range.'
|
|
5331
5426
|
),
|
|
5332
5427
|
dry_run: flexBool().describe("Preview changes without writing"),
|
|
5333
5428
|
restore_indent: flexBool().describe("Auto-fix indentation to match anchor (default: false)"),
|
package/output-style.md
CHANGED
|
@@ -43,15 +43,15 @@ Prefer `hex-line` for text files you may inspect or modify. Hash-annotated reads
|
|
|
43
43
|
|
|
44
44
|
## Edit Discipline
|
|
45
45
|
|
|
46
|
-
-
|
|
46
|
+
- Prefer a real `range_checksum` copied from a fresh `read_file` or `grep_search(output_mode:"content", edit_ready=true)` block. If you don't have one for the exact range, pass `"auto"` and the server computes it for the current anchor range.
|
|
47
47
|
- First mutation in a file: use `grep_search(output_mode="summary")` for narrow targets, or `outline -> read_file(ranges)` for structural edits. Escalate to `grep_search(output_mode="content", edit_ready=true)` only when the next edit needs canonical hunks.
|
|
48
48
|
- Preserve file conventions mentally: `hex-line` hashes normalized logical text, but `edit_file` preserves the file's existing line endings and trailing-newline shape on write.
|
|
49
49
|
- Prefer `set_line` or `insert_after` for small local changes. Prefer `replace_between` for larger bounded block rewrites.
|
|
50
50
|
- When either anchor of `replace_between` is a lone delimiter (`}`, `)`, `]`, `});`), switch to `replace_lines` with `range_checksum`, or pass `range_checksum` to `replace_between` directly. `replace_between` anchors use short line-content hashes and may fuzzy-match a sibling closing delimiter.
|
|
51
51
|
- For inclusive `replace_between`: enumerate every `{`, `(`, `[` opened inside the replaced range and ensure `new_text` closes them all. If the range crosses a method/class/namespace boundary, prefer `set_line` + `insert_after` for each hunk.
|
|
52
52
|
- After `replace_between` on C#/Java/Go/C++/Rust files, run the language build or type-check once before proceeding. Brace drift is invisible at edit time.
|
|
53
|
-
- Use `replace_lines`
|
|
54
|
-
-
|
|
53
|
+
- Use `replace_lines` for bounded block replacement; supply its inclusive `range_checksum`, or pass `"auto"` to have the server compute it.
|
|
54
|
+
- Batch ALL hunks for the same file into ONE `edit_file` call (the `edits` array). Separate sequential edits on one file go stale and conflict — this is the single most common edit failure. Use `base_revision` only for a genuinely later follow-up after the file already changed.
|
|
55
55
|
- Before a delayed follow-up edit, a formatter pass, or any mixed-tool workflow on the same file, run `verify` with the last checksums and `base_revision`.
|
|
56
56
|
- If `edit_file` returns `retry_edit`, `retry_edits`, or `retry_plan`, reuse those directly instead of rebuilding anchors/checksums by hand.
|
|
57
57
|
- Reuse `retry_checksum` when it is returned for the exact same target range.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@levnikolaevich/hex-line-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.31.0",
|
|
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. 9 tools: inspect_path, read, edit, write, grep, outline, verify, changes, bulk_replace.",
|