@levnikolaevich/hex-line-mcp 1.7.0 → 1.8.1
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 +3 -19
- package/dist/hook.mjs +33 -9
- package/dist/server.mjs +110 -119
- package/output-style.md +16 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@ Every line carries an FNV-1a content hash. Every edit must present those hashes
|
|
|
11
11
|
|
|
12
12
|
## Features
|
|
13
13
|
|
|
14
|
-
###
|
|
14
|
+
### 10 MCP Tools
|
|
15
15
|
|
|
16
16
|
Core day-to-day tools:
|
|
17
17
|
|
|
@@ -28,7 +28,6 @@ Advanced / occasional:
|
|
|
28
28
|
- `directory_tree`
|
|
29
29
|
- `get_file_info`
|
|
30
30
|
- `changes`
|
|
31
|
-
- `setup_hooks`
|
|
32
31
|
|
|
33
32
|
| Tool | Description | Key Feature |
|
|
34
33
|
|------|-------------|-------------|
|
|
@@ -40,7 +39,6 @@ Advanced / occasional:
|
|
|
40
39
|
| `verify` | Check if held checksums / revision are still current | Staleness check without full re-read |
|
|
41
40
|
| `directory_tree` | Compact directory tree with root .gitignore support | Skips node_modules/.git, shows file sizes |
|
|
42
41
|
| `get_file_info` | File metadata without reading content | Size, lines, mtime, type, binary detection |
|
|
43
|
-
| `setup_hooks` | Configure Claude hooks + install output style | Gemini/Codex get guidance only; no hooks |
|
|
44
42
|
| `changes` | Compare file against git ref, shows added/removed/modified symbols | AST-level semantic diff |
|
|
45
43
|
| `bulk_replace` | Search-and-replace across multiple files by glob | Compact summary (default) or capped diffs via `format`, dry_run, max_files |
|
|
46
44
|
|
|
@@ -70,23 +68,9 @@ ripgrep is bundled via `@vscode/ripgrep` — no manual install needed for `grep_
|
|
|
70
68
|
|
|
71
69
|
### Hooks
|
|
72
70
|
|
|
73
|
-
|
|
71
|
+
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.
|
|
74
72
|
|
|
75
|
-
|
|
76
|
-
mcp__hex-line__setup_hooks(agent="claude")
|
|
77
|
-
```
|
|
78
|
-
|
|
79
|
-
Hooks are written to global `~/.claude/settings.json` with absolute path to `hook.mjs` from the global npm install. Manual configuration is not needed.
|
|
80
|
-
|
|
81
|
-
### Output Style
|
|
82
|
-
|
|
83
|
-
Optional: install a persistent Output Style that embeds tool preferences directly in Claude's system prompt. Reduces hook firings by making Claude prefer hex-line tools from the start.
|
|
84
|
-
|
|
85
|
-
```
|
|
86
|
-
mcp__hex-line__setup_hooks(agent="claude")
|
|
87
|
-
```
|
|
88
|
-
|
|
89
|
-
The `setup_hooks` tool automatically installs the output style to `~/.claude/output-styles/hex-line.md` and activates it if no other style is set. To activate manually: `/config` > Output style > hex-line.
|
|
73
|
+
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.
|
|
90
74
|
|
|
91
75
|
## Validation
|
|
92
76
|
|
package/dist/hook.mjs
CHANGED
|
@@ -89,6 +89,28 @@ var BINARY_EXT = /* @__PURE__ */ new Set([
|
|
|
89
89
|
".woff",
|
|
90
90
|
".woff2"
|
|
91
91
|
]);
|
|
92
|
+
var OUTLINEABLE_EXT = /* @__PURE__ */ new Set([
|
|
93
|
+
".js",
|
|
94
|
+
".mjs",
|
|
95
|
+
".cjs",
|
|
96
|
+
".jsx",
|
|
97
|
+
".ts",
|
|
98
|
+
".tsx",
|
|
99
|
+
".py",
|
|
100
|
+
".go",
|
|
101
|
+
".rs",
|
|
102
|
+
".java",
|
|
103
|
+
".c",
|
|
104
|
+
".h",
|
|
105
|
+
".cpp",
|
|
106
|
+
".cs",
|
|
107
|
+
".rb",
|
|
108
|
+
".php",
|
|
109
|
+
".kt",
|
|
110
|
+
".swift",
|
|
111
|
+
".sh",
|
|
112
|
+
".bash"
|
|
113
|
+
]);
|
|
92
114
|
var REVERSE_TOOL_HINTS = {
|
|
93
115
|
"mcp__hex-line__read_file": "Read (file_path, offset, limit)",
|
|
94
116
|
"mcp__hex-line__edit_file": "Edit (old_string, new_string, replace_all)",
|
|
@@ -99,8 +121,7 @@ var REVERSE_TOOL_HINTS = {
|
|
|
99
121
|
"mcp__hex-line__outline": "Read with offset/limit",
|
|
100
122
|
"mcp__hex-line__verify": "Read (re-read file to check freshness)",
|
|
101
123
|
"mcp__hex-line__changes": "Bash(git diff)",
|
|
102
|
-
"mcp__hex-line__bulk_replace": "Edit (text rename/refactor across files)"
|
|
103
|
-
"mcp__hex-line__setup_hooks": "Not available (hex-line disabled)"
|
|
124
|
+
"mcp__hex-line__bulk_replace": "Edit (text rename/refactor across files)"
|
|
104
125
|
};
|
|
105
126
|
var TOOL_HINTS = {
|
|
106
127
|
Read: "mcp__hex-line__read_file (not Read). For writing: write_file (no prior Read needed)",
|
|
@@ -118,8 +139,7 @@ var TOOL_HINTS = {
|
|
|
118
139
|
outline: "mcp__hex-line__outline (before reading large code files)",
|
|
119
140
|
verify: "mcp__hex-line__verify (staleness / revision check without re-read)",
|
|
120
141
|
changes: "mcp__hex-line__changes (git diff with change symbols)",
|
|
121
|
-
bulk: "mcp__hex-line__bulk_replace (multi-file search-replace)"
|
|
122
|
-
setup: "mcp__hex-line__setup_hooks (configure hooks for agents)"
|
|
142
|
+
bulk: "mcp__hex-line__bulk_replace (multi-file search-replace)"
|
|
123
143
|
};
|
|
124
144
|
var BASH_REDIRECTS = [
|
|
125
145
|
{ regex: /^cat\s+\S+/, key: "cat" },
|
|
@@ -183,7 +203,7 @@ var CMD_PATTERNS = [
|
|
|
183
203
|
var LINE_THRESHOLD = 50;
|
|
184
204
|
var HEAD_LINES = 15;
|
|
185
205
|
var TAIL_LINES = 15;
|
|
186
|
-
var LARGE_FILE_BYTES =
|
|
206
|
+
var LARGE_FILE_BYTES = 5 * 1024;
|
|
187
207
|
var LARGE_EDIT_CHARS = 1200;
|
|
188
208
|
function extOf(filePath) {
|
|
189
209
|
const dot = filePath.lastIndexOf(".");
|
|
@@ -328,10 +348,13 @@ function handlePreToolUse(data) {
|
|
|
328
348
|
process.exit(0);
|
|
329
349
|
}
|
|
330
350
|
if (fileSize !== null && fileSize <= LARGE_FILE_BYTES) {
|
|
331
|
-
|
|
351
|
+
const ext2 = filePath ? extOf(filePath) : "";
|
|
352
|
+
const hint = filePath && OUTLINEABLE_EXT.has(ext2) ? `mcp__hex-line__outline(path="${filePath}") gives a compact structural map. For edits, use mcp__hex-line__read_file(path="${filePath}") with ranges.` : filePath ? `NEXT READ: use mcp__hex-line__read_file(path="${filePath}"). Built-in Read allowed this time but wastes edit context.` : "NEXT READ: use mcp__hex-line__read_file. Built-in Read allowed this time but wastes edit context.";
|
|
353
|
+
advise(hint);
|
|
332
354
|
}
|
|
333
|
-
const
|
|
334
|
-
|
|
355
|
+
const ext = filePath ? extOf(filePath) : "";
|
|
356
|
+
const outlineHint = filePath && OUTLINEABLE_EXT.has(ext) ? `Use mcp__hex-line__outline(path="${filePath}") for structure, then mcp__hex-line__read_file(path="${filePath}") with ranges to read only what you need.` : filePath ? `Use mcp__hex-line__read_file(path="${filePath}") with ranges or offset/limit` : "Use mcp__hex-line__directory_tree or mcp__hex-line__read_file";
|
|
357
|
+
redirect(outlineHint, "Do not use built-in Read for full reads of large files.");
|
|
335
358
|
}
|
|
336
359
|
if (toolName === "Edit") {
|
|
337
360
|
const oldText = String(toolInput.old_string || "");
|
|
@@ -460,7 +483,8 @@ function handleSessionStart() {
|
|
|
460
483
|
process.stdout.write(JSON.stringify({ systemMessage: msg }));
|
|
461
484
|
process.exit(0);
|
|
462
485
|
}
|
|
463
|
-
|
|
486
|
+
var _norm = (p) => p.replace(/\\/g, "/");
|
|
487
|
+
if (_norm(process.argv[1]) === _norm(fileURLToPath(import.meta.url))) {
|
|
464
488
|
let input = "";
|
|
465
489
|
process.stdin.on("data", (chunk) => {
|
|
466
490
|
input += chunk;
|
package/dist/server.mjs
CHANGED
|
@@ -281,7 +281,8 @@ function validateWritePath(filePath) {
|
|
|
281
281
|
import { existsSync as existsSync2 } from "node:fs";
|
|
282
282
|
import { join as join3, dirname as dirname2, relative } from "node:path";
|
|
283
283
|
import { createRequire } from "node:module";
|
|
284
|
-
var
|
|
284
|
+
var HEX_LINE_CONTRACT_VERSION_MIN = 1;
|
|
285
|
+
var HEX_LINE_CONTRACT_VERSION_MAX = 2;
|
|
285
286
|
var _dbs = /* @__PURE__ */ new Map();
|
|
286
287
|
var _driverUnavailable = false;
|
|
287
288
|
function getGraphDB(filePath) {
|
|
@@ -306,12 +307,24 @@ function getGraphDB(filePath) {
|
|
|
306
307
|
return null;
|
|
307
308
|
}
|
|
308
309
|
}
|
|
310
|
+
var _cloneViewAvailable = /* @__PURE__ */ new WeakMap();
|
|
311
|
+
function _hasCloneView(db) {
|
|
312
|
+
if (_cloneViewAvailable.has(db)) return _cloneViewAvailable.get(db);
|
|
313
|
+
try {
|
|
314
|
+
db.prepare("SELECT node_id, norm_hash, file, line_start, display_name FROM hex_line_clone_siblings LIMIT 0").run();
|
|
315
|
+
_cloneViewAvailable.set(db, true);
|
|
316
|
+
return true;
|
|
317
|
+
} catch {
|
|
318
|
+
_cloneViewAvailable.set(db, false);
|
|
319
|
+
return false;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
309
322
|
function validateHexLineContract(db) {
|
|
310
323
|
try {
|
|
311
324
|
const contract = db.prepare("SELECT contract_version FROM hex_line_contract LIMIT 1").get();
|
|
312
|
-
if (!contract || contract.contract_version
|
|
325
|
+
if (!contract || contract.contract_version < HEX_LINE_CONTRACT_VERSION_MIN || contract.contract_version > HEX_LINE_CONTRACT_VERSION_MAX) return false;
|
|
313
326
|
db.prepare("SELECT node_id, file, line_start, line_end, display_name, kind, callees, callers FROM hex_line_symbol_annotations LIMIT 1").all();
|
|
314
|
-
db.prepare("SELECT source_id, target_id, source_file, source_line, source_display_name, target_file, target_line, target_display_name FROM hex_line_call_edges LIMIT 1").all();
|
|
327
|
+
db.prepare("SELECT source_id, target_id, source_file, source_line, source_display_name, target_file, target_line, target_display_name, confidence FROM hex_line_call_edges LIMIT 1").all();
|
|
315
328
|
return true;
|
|
316
329
|
} catch {
|
|
317
330
|
return false;
|
|
@@ -363,7 +376,8 @@ function callImpact(db, file, startLine, endLine) {
|
|
|
363
376
|
const dependents = db.prepare(
|
|
364
377
|
`SELECT source_display_name AS name, source_file AS file, source_line AS line
|
|
365
378
|
FROM hex_line_call_edges
|
|
366
|
-
WHERE target_id =
|
|
379
|
+
WHERE target_id = ?
|
|
380
|
+
AND confidence IN ('exact', 'precise')`
|
|
367
381
|
).all(node.node_id);
|
|
368
382
|
for (const dep of dependents) {
|
|
369
383
|
const key = `${dep.file}:${dep.name}`;
|
|
@@ -378,6 +392,39 @@ function callImpact(db, file, startLine, endLine) {
|
|
|
378
392
|
return [];
|
|
379
393
|
}
|
|
380
394
|
}
|
|
395
|
+
function cloneWarning(db, file, startLine, endLine) {
|
|
396
|
+
try {
|
|
397
|
+
if (!_hasCloneView(db)) return [];
|
|
398
|
+
const modified = db.prepare(
|
|
399
|
+
`SELECT node_id
|
|
400
|
+
FROM hex_line_symbol_annotations
|
|
401
|
+
WHERE file = ?
|
|
402
|
+
AND line_start <= ?
|
|
403
|
+
AND line_end >= ?`
|
|
404
|
+
).all(file, endLine, startLine);
|
|
405
|
+
if (modified.length === 0) return [];
|
|
406
|
+
const clones = [];
|
|
407
|
+
const seen = /* @__PURE__ */ new Set();
|
|
408
|
+
for (const node of modified) {
|
|
409
|
+
const siblings = db.prepare(
|
|
410
|
+
`SELECT s2.file, s2.line_start, s2.display_name
|
|
411
|
+
FROM hex_line_clone_siblings s1
|
|
412
|
+
JOIN hex_line_clone_siblings s2 ON s2.norm_hash = s1.norm_hash AND s2.node_id != s1.node_id
|
|
413
|
+
WHERE s1.node_id = ?`
|
|
414
|
+
).all(node.node_id);
|
|
415
|
+
for (const sib of siblings) {
|
|
416
|
+
const key = `${sib.file}:${sib.display_name}`;
|
|
417
|
+
if (!seen.has(key)) {
|
|
418
|
+
seen.add(key);
|
|
419
|
+
clones.push({ name: sib.display_name, file: sib.file, line: sib.line_start });
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
return clones.slice(0, 10);
|
|
424
|
+
} catch {
|
|
425
|
+
return [];
|
|
426
|
+
}
|
|
427
|
+
}
|
|
381
428
|
function matchAnnotation(db, file, line) {
|
|
382
429
|
try {
|
|
383
430
|
const node = db.prepare(
|
|
@@ -483,7 +530,7 @@ function parseChecksum(cs) {
|
|
|
483
530
|
// lib/snapshot.mjs
|
|
484
531
|
var MAX_FILES = 200;
|
|
485
532
|
var MAX_REVISIONS_PER_FILE = 5;
|
|
486
|
-
var TTL_MS =
|
|
533
|
+
var TTL_MS = 15 * 60 * 1e3;
|
|
487
534
|
var latestByFile = /* @__PURE__ */ new Map();
|
|
488
535
|
var revisionsById = /* @__PURE__ */ new Map();
|
|
489
536
|
var fileRevisionIds = /* @__PURE__ */ new Map();
|
|
@@ -1569,6 +1616,13 @@ ${serializeReadBlock(block)}`;
|
|
|
1569
1616
|
\u26A0 Call impact: ${affected.length} callers in other files
|
|
1570
1617
|
${list}`;
|
|
1571
1618
|
}
|
|
1619
|
+
const clones = cloneWarning(db, relFile, minLine, maxLine);
|
|
1620
|
+
if (clones.length > 0) {
|
|
1621
|
+
const list = clones.map((c) => `${c.file}:${c.line}`).join(", ");
|
|
1622
|
+
msg += `
|
|
1623
|
+
|
|
1624
|
+
\u26A0 ${clones.length} clone(s): ${list}`;
|
|
1625
|
+
}
|
|
1572
1626
|
}
|
|
1573
1627
|
} catch {
|
|
1574
1628
|
}
|
|
@@ -2425,6 +2479,8 @@ var __filename = fileURLToPath(import.meta.url);
|
|
|
2425
2479
|
var __dirname = dirname3(__filename);
|
|
2426
2480
|
var SOURCE_HOOK = resolve6(__dirname, "..", "hook.mjs");
|
|
2427
2481
|
var DIST_HOOK = resolve6(__dirname, "hook.mjs");
|
|
2482
|
+
var SOURCE_STYLE = resolve6(__dirname, "..", "output-style.md");
|
|
2483
|
+
var INSTALLED_STYLE = resolve6(homedir(), ".claude", "output-styles", "hex-line.md");
|
|
2428
2484
|
var HOOK_SIGNATURE = "hex-line";
|
|
2429
2485
|
var CLAUDE_HOOKS = {
|
|
2430
2486
|
SessionStart: {
|
|
@@ -2455,16 +2511,19 @@ function findEntryByCommand(entries) {
|
|
|
2455
2511
|
)
|
|
2456
2512
|
);
|
|
2457
2513
|
}
|
|
2458
|
-
function
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2514
|
+
function safeRead(filePath) {
|
|
2515
|
+
try {
|
|
2516
|
+
return readFileSync3(filePath, "utf-8");
|
|
2517
|
+
} catch {
|
|
2518
|
+
return null;
|
|
2462
2519
|
}
|
|
2520
|
+
}
|
|
2521
|
+
function writeHooksToFile(settingsPath) {
|
|
2522
|
+
const config = readJson(settingsPath) || {};
|
|
2523
|
+
if (!config.hooks || typeof config.hooks !== "object") config.hooks = {};
|
|
2463
2524
|
let changed = false;
|
|
2464
2525
|
for (const [event, desired] of Object.entries(CLAUDE_HOOKS)) {
|
|
2465
|
-
if (!Array.isArray(config.hooks[event]))
|
|
2466
|
-
config.hooks[event] = [];
|
|
2467
|
-
}
|
|
2526
|
+
if (!Array.isArray(config.hooks[event])) config.hooks[event] = [];
|
|
2468
2527
|
const entries = config.hooks[event];
|
|
2469
2528
|
const idx = findEntryByCommand(entries);
|
|
2470
2529
|
if (idx >= 0) {
|
|
@@ -2483,116 +2542,60 @@ function writeHooksToFile(settingsPath, label) {
|
|
|
2483
2542
|
config.disableAllHooks = false;
|
|
2484
2543
|
changed = true;
|
|
2485
2544
|
}
|
|
2486
|
-
if (
|
|
2487
|
-
|
|
2488
|
-
}
|
|
2489
|
-
writeJson(settingsPath, config);
|
|
2490
|
-
return `Claude (${label}): hooks -> ${STABLE_HOOK_PATH} OK`;
|
|
2545
|
+
if (changed) writeJson(settingsPath, config);
|
|
2546
|
+
return changed;
|
|
2491
2547
|
}
|
|
2492
2548
|
function cleanLocalHooks() {
|
|
2493
2549
|
const localPath = resolve6(process.cwd(), ".claude/settings.local.json");
|
|
2494
2550
|
const config = readJson(localPath);
|
|
2495
|
-
if (!config || !config.hooks || typeof config.hooks !== "object")
|
|
2496
|
-
return "local: clean";
|
|
2497
|
-
}
|
|
2551
|
+
if (!config || !config.hooks || typeof config.hooks !== "object") return;
|
|
2498
2552
|
let changed = false;
|
|
2499
2553
|
for (const event of Object.keys(CLAUDE_HOOKS)) {
|
|
2500
2554
|
if (!Array.isArray(config.hooks[event])) continue;
|
|
2501
|
-
const
|
|
2502
|
-
const idx = findEntryByCommand(entries);
|
|
2555
|
+
const idx = findEntryByCommand(config.hooks[event]);
|
|
2503
2556
|
if (idx >= 0) {
|
|
2504
|
-
|
|
2557
|
+
config.hooks[event].splice(idx, 1);
|
|
2505
2558
|
changed = true;
|
|
2506
2559
|
}
|
|
2507
|
-
if (
|
|
2508
|
-
delete config.hooks[event];
|
|
2509
|
-
}
|
|
2510
|
-
}
|
|
2511
|
-
if (Object.keys(config.hooks).length === 0) {
|
|
2512
|
-
delete config.hooks;
|
|
2513
|
-
}
|
|
2514
|
-
if (!changed) {
|
|
2515
|
-
return "local: clean";
|
|
2560
|
+
if (config.hooks[event].length === 0) delete config.hooks[event];
|
|
2516
2561
|
}
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
}
|
|
2520
|
-
function
|
|
2521
|
-
const source =
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2562
|
+
if (Object.keys(config.hooks).length === 0) delete config.hooks;
|
|
2563
|
+
if (changed) writeJson(localPath, config);
|
|
2564
|
+
}
|
|
2565
|
+
function syncOutputStyle() {
|
|
2566
|
+
const source = safeRead(SOURCE_STYLE);
|
|
2567
|
+
if (!source) return false;
|
|
2568
|
+
const installed = safeRead(INSTALLED_STYLE);
|
|
2569
|
+
if (installed === source) return false;
|
|
2570
|
+
mkdirSync(dirname3(INSTALLED_STYLE), { recursive: true });
|
|
2571
|
+
writeFileSync2(INSTALLED_STYLE, source, "utf-8");
|
|
2525
2572
|
const userSettings = resolve6(homedir(), ".claude/settings.json");
|
|
2526
2573
|
const config = readJson(userSettings) || {};
|
|
2527
|
-
|
|
2528
|
-
if (!prev) {
|
|
2574
|
+
if (!config.outputStyle) {
|
|
2529
2575
|
config.outputStyle = "hex-line";
|
|
2530
2576
|
writeJson(userSettings, config);
|
|
2531
2577
|
}
|
|
2532
|
-
|
|
2533
|
-
return msg;
|
|
2578
|
+
return true;
|
|
2534
2579
|
}
|
|
2535
|
-
function
|
|
2536
|
-
const results = [];
|
|
2580
|
+
function autoSync() {
|
|
2537
2581
|
const hookSource = existsSync5(DIST_HOOK) ? DIST_HOOK : SOURCE_HOOK;
|
|
2538
|
-
if (!existsSync5(hookSource))
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2582
|
+
if (!existsSync5(hookSource)) return;
|
|
2583
|
+
const changes = [];
|
|
2584
|
+
const srcHook = safeRead(hookSource);
|
|
2585
|
+
const dstHook = safeRead(STABLE_HOOK_PATH);
|
|
2586
|
+
if (srcHook && srcHook !== dstHook) {
|
|
2587
|
+
mkdirSync(STABLE_HOOK_DIR, { recursive: true });
|
|
2588
|
+
copyFileSync(hookSource, STABLE_HOOK_PATH);
|
|
2589
|
+
changes.push("hook");
|
|
2590
|
+
}
|
|
2591
|
+
if (syncOutputStyle()) changes.push("style");
|
|
2544
2592
|
const globalPath = resolve6(homedir(), ".claude/settings.json");
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
function setupGemini() {
|
|
2551
|
-
return "Gemini: Not supported (Gemini CLI does not support hooks. Add MCP Tool Preferences to GEMINI.md instead)";
|
|
2552
|
-
}
|
|
2553
|
-
function setupCodex() {
|
|
2554
|
-
return "Codex: Not supported (Codex CLI does not support hooks. Add MCP Tool Preferences to AGENTS.md instead)";
|
|
2555
|
-
}
|
|
2556
|
-
function uninstallClaude() {
|
|
2557
|
-
const globalPath = resolve6(homedir(), ".claude/settings.json");
|
|
2558
|
-
const config = readJson(globalPath);
|
|
2559
|
-
if (!config || !config.hooks || typeof config.hooks !== "object") {
|
|
2560
|
-
return "Claude: no hooks to remove";
|
|
2561
|
-
}
|
|
2562
|
-
let changed = false;
|
|
2563
|
-
for (const event of Object.keys(CLAUDE_HOOKS)) {
|
|
2564
|
-
if (!Array.isArray(config.hooks[event])) continue;
|
|
2565
|
-
const idx = findEntryByCommand(config.hooks[event]);
|
|
2566
|
-
if (idx >= 0) {
|
|
2567
|
-
config.hooks[event].splice(idx, 1);
|
|
2568
|
-
if (config.hooks[event].length === 0) delete config.hooks[event];
|
|
2569
|
-
changed = true;
|
|
2570
|
-
}
|
|
2571
|
-
}
|
|
2572
|
-
if (Object.keys(config.hooks).length === 0) delete config.hooks;
|
|
2573
|
-
if (!changed) return "Claude: no hex-line hooks found";
|
|
2574
|
-
writeJson(globalPath, config);
|
|
2575
|
-
return "Claude: hex-line hooks removed from global settings";
|
|
2576
|
-
}
|
|
2577
|
-
var AGENTS = { claude: setupClaude, gemini: setupGemini, codex: setupCodex };
|
|
2578
|
-
function setupHooks(agent = "all", action = "install") {
|
|
2579
|
-
const target = (agent || "all").toLowerCase();
|
|
2580
|
-
const act = (action || "install").toLowerCase();
|
|
2581
|
-
if (act === "uninstall") {
|
|
2582
|
-
const result = uninstallClaude();
|
|
2583
|
-
return `Hooks uninstalled:
|
|
2584
|
-
${result}
|
|
2585
|
-
|
|
2586
|
-
Restart Claude Code to apply changes.`;
|
|
2587
|
-
}
|
|
2588
|
-
if (target !== "all" && !AGENTS[target]) {
|
|
2589
|
-
throw new Error(`UNKNOWN_AGENT: '${agent}'. Supported: claude, gemini, codex, all`);
|
|
2593
|
+
if (writeHooksToFile(globalPath)) changes.push("settings");
|
|
2594
|
+
cleanLocalHooks();
|
|
2595
|
+
if (changes.length > 0) {
|
|
2596
|
+
process.stderr.write(`hex-line: synced ${changes.join(", ")}
|
|
2597
|
+
`);
|
|
2590
2598
|
}
|
|
2591
|
-
const targets = target === "all" ? Object.keys(AGENTS) : [target];
|
|
2592
|
-
const results = targets.map((name) => " " + AGENTS[name]());
|
|
2593
|
-
const header = `Hooks configured for ${target}:`;
|
|
2594
|
-
const footer = "\nRestart Claude Code to apply hook changes.";
|
|
2595
|
-
return [header, ...results, footer].join("\n");
|
|
2596
2599
|
}
|
|
2597
2600
|
|
|
2598
2601
|
// lib/changes.mjs
|
|
@@ -2837,7 +2840,7 @@ OUTPUT_CAPPED: Output exceeded ${MAX_BULK_OUTPUT_CHARS} chars.`;
|
|
|
2837
2840
|
}
|
|
2838
2841
|
|
|
2839
2842
|
// server.mjs
|
|
2840
|
-
var version = true ? "1.
|
|
2843
|
+
var version = true ? "1.8.1" : (await null).createRequire(import.meta.url)("./package.json").version;
|
|
2841
2844
|
var { server, StdioServerTransport } = await createServerRuntime({
|
|
2842
2845
|
name: "hex-line-mcp",
|
|
2843
2846
|
version
|
|
@@ -3086,22 +3089,6 @@ server.registerTool("get_file_info", {
|
|
|
3086
3089
|
return { content: [{ type: "text", text: e.message }], isError: true };
|
|
3087
3090
|
}
|
|
3088
3091
|
});
|
|
3089
|
-
server.registerTool("setup_hooks", {
|
|
3090
|
-
title: "Setup Hooks",
|
|
3091
|
-
description: "Install or uninstall hex-line hooks in CLI agent settings. Idempotent.",
|
|
3092
|
-
inputSchema: z2.object({
|
|
3093
|
-
agent: z2.string().optional().describe('Target agent: "claude", "gemini", "codex", or "all" (default: "all")'),
|
|
3094
|
-
action: z2.string().optional().describe('"install" (default) or "uninstall"')
|
|
3095
|
-
}),
|
|
3096
|
-
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true }
|
|
3097
|
-
}, async (rawParams) => {
|
|
3098
|
-
const { agent, action } = coerceParams(rawParams);
|
|
3099
|
-
try {
|
|
3100
|
-
return { content: [{ type: "text", text: setupHooks(agent, action) }] };
|
|
3101
|
-
} catch (e) {
|
|
3102
|
-
return { content: [{ type: "text", text: e.message }], isError: true };
|
|
3103
|
-
}
|
|
3104
|
-
});
|
|
3105
3092
|
server.registerTool("changes", {
|
|
3106
3093
|
title: "Semantic Diff",
|
|
3107
3094
|
description: "Compare file or directory against git ref (default: HEAD). Shows added/removed/modified symbols or file stats.",
|
|
@@ -3155,3 +3142,7 @@ server.registerTool("bulk_replace", {
|
|
|
3155
3142
|
var transport = new StdioServerTransport();
|
|
3156
3143
|
await server.connect(transport);
|
|
3157
3144
|
void checkForUpdates("@levnikolaevich/hex-line-mcp", version);
|
|
3145
|
+
try {
|
|
3146
|
+
autoSync();
|
|
3147
|
+
} catch {
|
|
3148
|
+
}
|
package/output-style.md
CHANGED
|
@@ -36,7 +36,8 @@ Prefer:
|
|
|
36
36
|
2. send one `edit_file` call with batched edits
|
|
37
37
|
3. carry `revision` from `read_file` into `base_revision` on follow-up edits
|
|
38
38
|
4. use `set_line`, `replace_lines`, `insert_after`, `replace_between` based on scope
|
|
39
|
-
5.
|
|
39
|
+
5. if edit returns CONFLICT, call `verify` with stale checksum — it reports VALID/STALE/INVALID without rereading the whole file
|
|
40
|
+
6. only reread (`read_file`) when `verify` confirms STALE
|
|
40
41
|
|
|
41
42
|
Post-edit output uses `block: post_edit` with checksum — use it directly for follow-up edits or verify.
|
|
42
43
|
|
|
@@ -45,6 +46,20 @@ Avoid:
|
|
|
45
46
|
- full-file rewrites for local changes
|
|
46
47
|
- using `bulk_replace` for structural block rewrites
|
|
47
48
|
|
|
49
|
+
|
|
50
|
+
## hex-graph — Code Analysis
|
|
51
|
+
|
|
52
|
+
Run `index_project` once per session before using other graph tools.
|
|
53
|
+
|
|
54
|
+
| Task | Tool | Output |
|
|
55
|
+
|------|------|--------|
|
|
56
|
+
| Refactoring / moving code | `find_references`, `find_implementations` | All usages + implementations |
|
|
57
|
+
| Code review / tech debt | `find_cycles`, `find_hotspots` | Circular deps, complexity hotspots |
|
|
58
|
+
| Dead code cleanup | `find_unused_exports` | Exports nobody imports |
|
|
59
|
+
| Architecture overview | `get_architecture`, `get_module_metrics` | Module map + coupling metrics |
|
|
60
|
+
| Duplicate detection | `find_clones` | Similar code blocks |
|
|
61
|
+
| Impact analysis | `trace_paths` | Call chains A → B |
|
|
62
|
+
| Symbol lookup | `search_symbols`, `get_symbol` | Find by name, get details |
|
|
48
63
|
# Response Style
|
|
49
64
|
|
|
50
65
|
Keep responses compact and operational. Explain only what is needed to complete the task or justify a non-obvious decision.
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@levnikolaevich/hex-line-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.1",
|
|
4
4
|
"mcpName": "io.github.levnikolaevich/hex-line-mcp",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"description": "Hash-verified file editing MCP + token efficiency hook for AI coding agents.
|
|
6
|
+
"description": "Hash-verified file editing MCP + token efficiency hook for AI coding agents. 10 tools: read, edit, write, grep, outline, verify, directory_tree, file_info, changes, bulk_replace.",
|
|
7
7
|
"main": "dist/server.mjs",
|
|
8
8
|
"bin": {
|
|
9
9
|
"hex-line-mcp": "dist/server.mjs"
|