@levnikolaevich/hex-line-mcp 1.6.0 → 1.8.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 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
- ### 11 MCP Tools
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
- Automatic setup (run once after MCP install):
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 = 15 * 1024;
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(".");
@@ -250,28 +270,47 @@ function isHexLineDisabled(configPath) {
250
270
  function _resetHexLineDisabledCache() {
251
271
  _hexLineDisabled = null;
252
272
  }
273
+ var _hookMode;
274
+ function getHookMode() {
275
+ if (_hookMode !== void 0) return _hookMode;
276
+ _hookMode = "blocking";
277
+ try {
278
+ const stateFile = resolve(process.cwd(), ".hex-skills/environment_state.json");
279
+ const data = JSON.parse(readFileSync(stateFile, "utf-8"));
280
+ if (data.hooks?.mode === "advisory") _hookMode = "advisory";
281
+ } catch {
282
+ }
283
+ return _hookMode;
284
+ }
253
285
  function block(reason, context) {
254
286
  const output = {
255
287
  hookSpecificOutput: {
256
- hookEventName: "PreToolUse",
257
- permissionDecision: "deny",
258
- permissionDecisionReason: reason
259
- }
288
+ permissionDecision: "deny"
289
+ },
290
+ systemMessage: context ? `${reason}
291
+ ${context}` : reason
260
292
  };
261
- if (context) output.hookSpecificOutput.additionalContext = context;
262
293
  process.stdout.write(JSON.stringify(output));
263
294
  process.exit(2);
264
295
  }
265
- function advise(reason) {
266
- process.stdout.write(JSON.stringify({
296
+ function advise(reason, context) {
297
+ const output = {
267
298
  hookSpecificOutput: {
268
- hookEventName: "PreToolUse",
269
- permissionDecision: "approve",
270
- permissionDecisionReason: reason
271
- }
272
- }));
299
+ permissionDecision: "allow"
300
+ },
301
+ systemMessage: context ? `${reason}
302
+ ${context}` : reason
303
+ };
304
+ process.stdout.write(JSON.stringify(output));
273
305
  process.exit(0);
274
306
  }
307
+ function redirect(reason, context) {
308
+ if (getHookMode() === "advisory") {
309
+ advise(reason, context);
310
+ } else {
311
+ block(reason, context);
312
+ }
313
+ }
275
314
  function handlePreToolUse(data) {
276
315
  const toolName = data.tool_name || "";
277
316
  const toolInput = data.tool_input || {};
@@ -309,10 +348,13 @@ function handlePreToolUse(data) {
309
348
  process.exit(0);
310
349
  }
311
350
  if (fileSize !== null && fileSize <= LARGE_FILE_BYTES) {
312
- advise("hex-line read_file returns hash-annotated lines for verified edit workflow. For code files, outline gives a compact structural map first.");
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);
313
354
  }
314
- const target = filePath ? `Use mcp__hex-line__outline or mcp__hex-line__read_file with path="${filePath}"` : "Use mcp__hex-line__directory_tree or mcp__hex-line__read_file";
315
- block(target, "For large or unknown full reads: call outline first, then read_file with offset/limit or ranges. Do not use built-in Read here.");
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.");
316
358
  }
317
359
  if (toolName === "Edit") {
318
360
  const oldText = String(toolInput.old_string || "");
@@ -321,15 +363,15 @@ function handlePreToolUse(data) {
321
363
  process.exit(0);
322
364
  }
323
365
  const target = filePath ? `Use mcp__hex-line__grep_search or mcp__hex-line__read_file, then mcp__hex-line__edit_file with path="${filePath}"` : "Use mcp__hex-line__grep_search or mcp__hex-line__read_file, then mcp__hex-line__edit_file";
324
- block(target, "For large or repeated edits: locate anchors/checksums first, then call edit_file once with batched edits.");
366
+ redirect(target, "For large or repeated edits: locate anchors/checksums first, then call edit_file once with batched edits.");
325
367
  }
326
368
  if (toolName === "Write") {
327
369
  const pathNote = filePath ? ` with path="${filePath}"` : "";
328
- block(`Use mcp__hex-line__write_file${pathNote}`, TOOL_HINTS.Write);
370
+ redirect(`Use mcp__hex-line__write_file${pathNote}`, TOOL_HINTS.Write);
329
371
  }
330
372
  if (toolName === "Grep") {
331
373
  const pathNote = filePath ? ` with path="${filePath}"` : "";
332
- block(`Use mcp__hex-line__grep_search${pathNote}`, TOOL_HINTS.Grep);
374
+ redirect(`Use mcp__hex-line__grep_search${pathNote}`, TOOL_HINTS.Grep);
333
375
  }
334
376
  }
335
377
  if (toolName === "Bash") {
@@ -352,7 +394,7 @@ function handlePreToolUse(data) {
352
394
  if (regex.test(firstCmd)) {
353
395
  const hint = TOOL_HINTS[key];
354
396
  const toolName2 = hint.split(" (")[0];
355
- block(`Use ${toolName2} instead of piped command`, hint);
397
+ redirect(`Use ${toolName2} instead of piped command`, hint);
356
398
  }
357
399
  }
358
400
  process.exit(0);
@@ -363,7 +405,7 @@ function handlePreToolUse(data) {
363
405
  const toolName2 = hint.split(" (")[0];
364
406
  const args = command.split(/\s+/).slice(1).join(" ");
365
407
  const argsNote = args ? ` \u2014 args: "${args}"` : "";
366
- block(`Use ${toolName2}${argsNote}`, hint);
408
+ redirect(`Use ${toolName2}${argsNote}`, hint);
367
409
  }
368
410
  }
369
411
  }
@@ -441,7 +483,8 @@ function handleSessionStart() {
441
483
  process.stdout.write(JSON.stringify({ systemMessage: msg }));
442
484
  process.exit(0);
443
485
  }
444
- if (process.argv[1] === fileURLToPath(import.meta.url)) {
486
+ var _norm = (p) => p.replace(/\\/g, "/");
487
+ if (_norm(process.argv[1]) === _norm(fileURLToPath(import.meta.url))) {
445
488
  let input = "";
446
489
  process.stdin.on("data", (chunk) => {
447
490
  input += chunk;
package/dist/server.mjs CHANGED
@@ -483,7 +483,7 @@ function parseChecksum(cs) {
483
483
  // lib/snapshot.mjs
484
484
  var MAX_FILES = 200;
485
485
  var MAX_REVISIONS_PER_FILE = 5;
486
- var TTL_MS = 5 * 60 * 1e3;
486
+ var TTL_MS = 15 * 60 * 1e3;
487
487
  var latestByFile = /* @__PURE__ */ new Map();
488
488
  var revisionsById = /* @__PURE__ */ new Map();
489
489
  var fileRevisionIds = /* @__PURE__ */ new Map();
@@ -2425,6 +2425,8 @@ var __filename = fileURLToPath(import.meta.url);
2425
2425
  var __dirname = dirname3(__filename);
2426
2426
  var SOURCE_HOOK = resolve6(__dirname, "..", "hook.mjs");
2427
2427
  var DIST_HOOK = resolve6(__dirname, "hook.mjs");
2428
+ var SOURCE_STYLE = resolve6(__dirname, "..", "output-style.md");
2429
+ var INSTALLED_STYLE = resolve6(homedir(), ".claude", "output-styles", "hex-line.md");
2428
2430
  var HOOK_SIGNATURE = "hex-line";
2429
2431
  var CLAUDE_HOOKS = {
2430
2432
  SessionStart: {
@@ -2455,16 +2457,19 @@ function findEntryByCommand(entries) {
2455
2457
  )
2456
2458
  );
2457
2459
  }
2458
- function writeHooksToFile(settingsPath, label) {
2459
- const config = readJson(settingsPath) || {};
2460
- if (!config.hooks || typeof config.hooks !== "object") {
2461
- config.hooks = {};
2460
+ function safeRead(filePath) {
2461
+ try {
2462
+ return readFileSync3(filePath, "utf-8");
2463
+ } catch {
2464
+ return null;
2462
2465
  }
2466
+ }
2467
+ function writeHooksToFile(settingsPath) {
2468
+ const config = readJson(settingsPath) || {};
2469
+ if (!config.hooks || typeof config.hooks !== "object") config.hooks = {};
2463
2470
  let changed = false;
2464
2471
  for (const [event, desired] of Object.entries(CLAUDE_HOOKS)) {
2465
- if (!Array.isArray(config.hooks[event])) {
2466
- config.hooks[event] = [];
2467
- }
2472
+ if (!Array.isArray(config.hooks[event])) config.hooks[event] = [];
2468
2473
  const entries = config.hooks[event];
2469
2474
  const idx = findEntryByCommand(entries);
2470
2475
  if (idx >= 0) {
@@ -2483,116 +2488,60 @@ function writeHooksToFile(settingsPath, label) {
2483
2488
  config.disableAllHooks = false;
2484
2489
  changed = true;
2485
2490
  }
2486
- if (!changed) {
2487
- return `Claude (${label}): already configured`;
2488
- }
2489
- writeJson(settingsPath, config);
2490
- return `Claude (${label}): hooks -> ${STABLE_HOOK_PATH} OK`;
2491
+ if (changed) writeJson(settingsPath, config);
2492
+ return changed;
2491
2493
  }
2492
2494
  function cleanLocalHooks() {
2493
2495
  const localPath = resolve6(process.cwd(), ".claude/settings.local.json");
2494
2496
  const config = readJson(localPath);
2495
- if (!config || !config.hooks || typeof config.hooks !== "object") {
2496
- return "local: clean";
2497
- }
2497
+ if (!config || !config.hooks || typeof config.hooks !== "object") return;
2498
2498
  let changed = false;
2499
2499
  for (const event of Object.keys(CLAUDE_HOOKS)) {
2500
2500
  if (!Array.isArray(config.hooks[event])) continue;
2501
- const entries = config.hooks[event];
2502
- const idx = findEntryByCommand(entries);
2501
+ const idx = findEntryByCommand(config.hooks[event]);
2503
2502
  if (idx >= 0) {
2504
- entries.splice(idx, 1);
2503
+ config.hooks[event].splice(idx, 1);
2505
2504
  changed = true;
2506
2505
  }
2507
- if (entries.length === 0) {
2508
- delete config.hooks[event];
2509
- }
2510
- }
2511
- if (Object.keys(config.hooks).length === 0) {
2512
- delete config.hooks;
2506
+ if (config.hooks[event].length === 0) delete config.hooks[event];
2513
2507
  }
2514
- if (!changed) {
2515
- return "local: clean";
2516
- }
2517
- writeJson(localPath, config);
2518
- return "local: removed old hex-line hooks";
2519
- }
2520
- function installOutputStyle() {
2521
- const source = resolve6(dirname3(fileURLToPath(import.meta.url)), "..", "output-style.md");
2522
- const target = resolve6(homedir(), ".claude", "output-styles", "hex-line.md");
2523
- mkdirSync(dirname3(target), { recursive: true });
2524
- writeFileSync2(target, readFileSync3(source, "utf-8"), "utf-8");
2508
+ if (Object.keys(config.hooks).length === 0) delete config.hooks;
2509
+ if (changed) writeJson(localPath, config);
2510
+ }
2511
+ function syncOutputStyle() {
2512
+ const source = safeRead(SOURCE_STYLE);
2513
+ if (!source) return false;
2514
+ const installed = safeRead(INSTALLED_STYLE);
2515
+ if (installed === source) return false;
2516
+ mkdirSync(dirname3(INSTALLED_STYLE), { recursive: true });
2517
+ writeFileSync2(INSTALLED_STYLE, source, "utf-8");
2525
2518
  const userSettings = resolve6(homedir(), ".claude/settings.json");
2526
2519
  const config = readJson(userSettings) || {};
2527
- const prev = config.outputStyle;
2528
- if (!prev) {
2520
+ if (!config.outputStyle) {
2529
2521
  config.outputStyle = "hex-line";
2530
2522
  writeJson(userSettings, config);
2531
2523
  }
2532
- const msg = prev ? `Output style file installed. Existing style '${prev}' preserved (not overridden)` : "Output style 'hex-line' installed and activated globally";
2533
- return msg;
2524
+ return true;
2534
2525
  }
2535
- function setupClaude() {
2536
- const results = [];
2526
+ function autoSync() {
2537
2527
  const hookSource = existsSync5(DIST_HOOK) ? DIST_HOOK : SOURCE_HOOK;
2538
- if (!existsSync5(hookSource)) {
2539
- return "Claude: FAILED \u2014 hook.mjs not found. Reinstall @levnikolaevich/hex-line-mcp.";
2540
- }
2541
- mkdirSync(STABLE_HOOK_DIR, { recursive: true });
2542
- copyFileSync(hookSource, STABLE_HOOK_PATH);
2543
- results.push(`hook.mjs -> ${STABLE_HOOK_PATH}`);
2544
- const globalPath = resolve6(homedir(), ".claude/settings.json");
2545
- results.push(writeHooksToFile(globalPath, "global"));
2546
- results.push(cleanLocalHooks());
2547
- results.push(installOutputStyle());
2548
- return results.join(" | ");
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() {
2528
+ if (!existsSync5(hookSource)) return;
2529
+ const changes = [];
2530
+ const srcHook = safeRead(hookSource);
2531
+ const dstHook = safeRead(STABLE_HOOK_PATH);
2532
+ if (srcHook && srcHook !== dstHook) {
2533
+ mkdirSync(STABLE_HOOK_DIR, { recursive: true });
2534
+ copyFileSync(hookSource, STABLE_HOOK_PATH);
2535
+ changes.push("hook");
2536
+ }
2537
+ if (syncOutputStyle()) changes.push("style");
2557
2538
  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`);
2539
+ if (writeHooksToFile(globalPath)) changes.push("settings");
2540
+ cleanLocalHooks();
2541
+ if (changes.length > 0) {
2542
+ process.stderr.write(`hex-line: synced ${changes.join(", ")}
2543
+ `);
2590
2544
  }
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
2545
  }
2597
2546
 
2598
2547
  // lib/changes.mjs
@@ -2837,7 +2786,7 @@ OUTPUT_CAPPED: Output exceeded ${MAX_BULK_OUTPUT_CHARS} chars.`;
2837
2786
  }
2838
2787
 
2839
2788
  // server.mjs
2840
- var version = true ? "1.6.0" : (await null).createRequire(import.meta.url)("./package.json").version;
2789
+ var version = true ? "1.8.0" : (await null).createRequire(import.meta.url)("./package.json").version;
2841
2790
  var { server, StdioServerTransport } = await createServerRuntime({
2842
2791
  name: "hex-line-mcp",
2843
2792
  version
@@ -3086,22 +3035,6 @@ server.registerTool("get_file_info", {
3086
3035
  return { content: [{ type: "text", text: e.message }], isError: true };
3087
3036
  }
3088
3037
  });
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
3038
  server.registerTool("changes", {
3106
3039
  title: "Semantic Diff",
3107
3040
  description: "Compare file or directory against git ref (default: HEAD). Shows added/removed/modified symbols or file stats.",
@@ -3155,3 +3088,7 @@ server.registerTool("bulk_replace", {
3155
3088
  var transport = new StdioServerTransport();
3156
3089
  await server.connect(transport);
3157
3090
  void checkForUpdates("@levnikolaevich/hex-line-mcp", version);
3091
+ try {
3092
+ autoSync();
3093
+ } catch {
3094
+ }
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. use `verify` before rereading after staleness
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.6.0",
3
+ "version": "1.8.0",
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. 11 tools: read, edit, write, grep, outline, verify, directory_tree, file_info, setup_hooks, changes, bulk_replace.",
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"