@levnikolaevich/hex-line-mcp 1.3.6 → 1.4.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
@@ -42,7 +42,7 @@ Advanced / occasional:
42
42
  | `get_file_info` | File metadata without reading content | Size, lines, mtime, type, binary detection |
43
43
  | `setup_hooks` | Configure Claude hooks + install output style | Gemini/Codex get guidance only; no hooks |
44
44
  | `changes` | Compare file against git ref, shows added/removed/modified symbols | AST-level semantic diff |
45
- | `bulk_replace` | Search-and-replace across multiple files by glob | Per-file diffs, dry_run, max_files safety |
45
+ | `bulk_replace` | Search-and-replace across multiple files by glob | Compact summary (default) or capped diffs via `format`, dry_run, max_files |
46
46
 
47
47
  ### Hooks (PreToolUse + PostToolUse)
48
48
 
@@ -90,45 +90,33 @@ The `setup_hooks` tool automatically installs the output style to `~/.claude/out
90
90
 
91
91
  ## Benchmarking
92
92
 
93
- `hex-line-mcp` now distinguishes:
93
+ Two-tier benchmark architecture:
94
94
 
95
- - `tests`correctness and regression safety
96
- - `benchmarks` — comparative workflow efficiency against built-in tools
97
- - `diagnostics` — modeled tool-level measurements for engineering inspection
98
-
99
- Public benchmark mode reports only comparative multi-step workflows:
95
+ - `/benchmark-compare` — real 1:1 comparison (runs inside Claude Code, calls BOTH built-in and hex-line tools on same files)
96
+ - `npm run benchmark` — hex-line standalone metrics (Node.js, all real library calls, no simulations)
100
97
 
101
98
  ```bash
102
99
  npm run benchmark -- --repo /path/to/repo
103
- ```
104
-
105
- Optional diagnostics stay available separately:
106
-
107
- ```bash
108
100
  npm run benchmark:diagnostic -- --repo /path/to/repo
109
- npm run benchmark:diagnostic:graph -- --repo /path/to/repo
110
101
  ```
111
102
 
112
- The diagnostics output includes synthetic tool-level comparisons such as read, grep, verify, and graph-enrichment helpers. Those numbers are useful for inspecting output shape and token behavior, but they are not the public workflow benchmark score.
113
-
114
- Current sample run on the `hex-line-mcp` repo with session-derived workflows:
115
-
116
- | ID | Workflow | Built-in | hex-line | Savings | Ops |
117
- |----|----------|---------:|---------:|--------:|----:|
118
- | W1 | Debug hook file-listing redirect | 23,143 chars | 882 chars | 96% | 3→2 |
119
- | W2 | Adjust `setup_hooks` guidance and verify | 24,877 chars | 1,637 chars | 93% | 3→3 |
120
- | W3 | Repo-wide benchmark wording refresh | 137,796 chars | 38,918 chars | 72% | 15→1 |
121
- | W4 | Inspect large smoke test before edit | 49,566 chars | 2,104 chars | 96% | 3→3 |
103
+ Current hex-line workflow metrics on the `hex-line-mcp` repo (all real library calls):
122
104
 
123
- Workflow summary: `89%` average token savings, `24→9` tool calls (`63%` fewer).
105
+ | # | Workflow | Hex-line output | Ops |
106
+ |---|----------|---------:|----:|
107
+ | W1 | Debug hook file-listing redirect | 882 chars | 2 |
108
+ | W2 | Adjust `setup_hooks` guidance and verify | 1,719 chars | 3 |
109
+ | W3 | Repo-wide benchmark wording refresh | 213 chars | 1 |
110
+ | W4 | Inspect large smoke test before edit | 2,322 chars | 3 |
111
+ | W5 | Follow-up edit after unrelated line shift | 1,267 chars | 3 |
124
112
 
125
- These workflows are derived from recent real Claude sessions, but executed against local reproducible fixtures in the repository. They should be read as workflow-efficiency measurements, not as correctness or semantic-quality claims.
113
+ Workflow total: `6,403` chars across `12` ops. Run `/benchmark-compare` in Claude Code for full built-in vs hex-line comparison with real tool calls on both sides.
126
114
 
127
115
  ### Optional Graph Enrichment
128
116
 
129
- If a project already has `.codegraph/index.db`, `hex-line` can add lightweight graph hints to `read_file`, `outline`, `grep_search`, and `edit_file`.
117
+ If a project already has `.hex-skills/codegraph/index.db`, `hex-line` can add lightweight graph hints to `read_file`, `outline`, `grep_search`, and `edit_file`.
130
118
 
131
- - Graph enrichment is optional. If `.codegraph/index.db` is missing, `hex-line` falls back to standard behavior silently.
119
+ - Graph enrichment is optional. If `.hex-skills/codegraph/index.db` is missing, `hex-line` falls back to standard behavior silently.
132
120
  - `better-sqlite3` is optional. If it is unavailable, `hex-line` still works without graph hints.
133
121
  - `edit_file` reports **Call impact**, not full semantic blast radius. The warning uses call-graph callers only.
134
122
 
@@ -160,7 +148,7 @@ Use `replace_between` inside `edit_file` when you know stable start/end anchors
160
148
 
161
149
  ### Literal rename / refactor
162
150
 
163
- Use `bulk_replace` for text rename patterns across one or more files. Do not use it as a substitute for structured block rewrites.
151
+ Use `bulk_replace` for text rename patterns across one or more files. Returns compact summary by default; pass `format: "full"` for capped diffs. Do not use it as a substitute for structured block rewrites.
164
152
 
165
153
  ### read_file
166
154
 
package/dist/hook.mjs CHANGED
@@ -91,13 +91,13 @@ var BINARY_EXT = /* @__PURE__ */ new Set([
91
91
  ]);
92
92
  var REVERSE_TOOL_HINTS = {
93
93
  "mcp__hex-line__read_file": "Read (file_path, offset, limit)",
94
- "mcp__hex-line__edit_file": "Edit (revision-aware hash edits, block rewrite, auto-rebase)",
94
+ "mcp__hex-line__edit_file": "Edit (old_string, new_string, replace_all)",
95
95
  "mcp__hex-line__write_file": "Write (file_path, content)",
96
96
  "mcp__hex-line__grep_search": "Grep (pattern, path)",
97
97
  "mcp__hex-line__directory_tree": "Glob (pattern) or Bash(ls)",
98
98
  "mcp__hex-line__get_file_info": "Bash(stat/wc)",
99
99
  "mcp__hex-line__outline": "Read with offset/limit",
100
- "mcp__hex-line__verify": "Verify held checksums / revision without reread",
100
+ "mcp__hex-line__verify": "Read (re-read file to check freshness)",
101
101
  "mcp__hex-line__changes": "Bash(git diff)",
102
102
  "mcp__hex-line__bulk_replace": "Edit (text rename/refactor across files)",
103
103
  "mcp__hex-line__setup_hooks": "Not available (hex-line disabled)"
@@ -114,10 +114,10 @@ var TOOL_HINTS = {
114
114
  stat: "mcp__hex-line__get_file_info (not stat/wc/file)",
115
115
  grep: "mcp__hex-line__grep_search (not grep/rg). Params: output, literal, context_before, context_after, multiline",
116
116
  sed: "mcp__hex-line__edit_file for hash edits, or mcp__hex-line__bulk_replace for text rename (not sed -i)",
117
- diff: "mcp__hex-line__changes (not diff). Git-based semantic diff",
117
+ diff: "mcp__hex-line__changes (not diff). Git diff with change symbols",
118
118
  outline: "mcp__hex-line__outline (before reading large code files)",
119
119
  verify: "mcp__hex-line__verify (staleness / revision check without re-read)",
120
- changes: "mcp__hex-line__changes (semantic AST diff)",
120
+ changes: "mcp__hex-line__changes (git diff with change symbols)",
121
121
  bulk: "mcp__hex-line__bulk_replace (multi-file search-replace)",
122
122
  setup: "mcp__hex-line__setup_hooks (configure hooks for agents)"
123
123
  };
package/dist/server.mjs CHANGED
@@ -6,7 +6,7 @@ import { dirname as dirname4 } from "node:path";
6
6
  import { z as z2 } from "zod";
7
7
 
8
8
  // ../hex-common/src/runtime/mcp-bootstrap.mjs
9
- async function createServerRuntime({ name, version: version2, installDir }) {
9
+ async function createServerRuntime({ name, version: version2 }) {
10
10
  let McpServer, StdioServerTransport2;
11
11
  try {
12
12
  ({ McpServer } = await import("@modelcontextprotocol/sdk/server/mcp.js"));
@@ -14,11 +14,16 @@ async function createServerRuntime({ name, version: version2, installDir }) {
14
14
  } catch {
15
15
  process.stderr.write(
16
16
  `${name}: @modelcontextprotocol/sdk not found.
17
- Run: cd ${installDir} && npm install
17
+ Run: npm install @modelcontextprotocol/sdk
18
18
  `
19
19
  );
20
20
  process.exit(1);
21
21
  }
22
+ const shutdown = () => {
23
+ process.exit(0);
24
+ };
25
+ process.on("SIGTERM", shutdown);
26
+ process.on("SIGINT", shutdown);
22
27
  return {
23
28
  server: new McpServer({ name, version: version2 }),
24
29
  StdioServerTransport: StdioServerTransport2
@@ -250,6 +255,8 @@ function listDirectory(dirPath, opts = {}) {
250
255
  }
251
256
  var MAX_OUTPUT_CHARS = 8e4;
252
257
  var MAX_DIFF_CHARS = 3e4;
258
+ var MAX_BULK_OUTPUT_CHARS = 3e4;
259
+ var MAX_PER_FILE_DIFF_LINES = 50;
253
260
  function readText(filePath) {
254
261
  return readFileSync(filePath, "utf-8").replace(/\r\n/g, "\n");
255
262
  }
@@ -340,7 +347,7 @@ function getGraphDB(filePath) {
340
347
  try {
341
348
  const projectRoot = findProjectRoot(filePath);
342
349
  if (!projectRoot) return null;
343
- const dbPath = join3(projectRoot, ".codegraph", "index.db");
350
+ const dbPath = join3(projectRoot, ".hex-skills/codegraph", "index.db");
344
351
  if (!existsSync2(dbPath)) return null;
345
352
  if (_dbs.has(dbPath)) return _dbs.get(dbPath);
346
353
  const require2 = createRequire(import.meta.url);
@@ -454,7 +461,7 @@ function getRelativePath(filePath) {
454
461
  function findProjectRoot(filePath) {
455
462
  let dir = dirname2(filePath);
456
463
  for (let i = 0; i < 10; i++) {
457
- if (existsSync2(join3(dir, ".codegraph", "index.db"))) return dir;
464
+ if (existsSync2(join3(dir, ".hex-skills/codegraph", "index.db"))) return dir;
458
465
  const parent = dirname2(dir);
459
466
  if (parent === dir) break;
460
467
  dir = parent;
@@ -1964,19 +1971,18 @@ function fileInfo(filePath) {
1964
1971
  }
1965
1972
 
1966
1973
  // lib/setup.mjs
1967
- import { readFileSync as readFileSync4, writeFileSync as writeFileSync2, existsSync as existsSync5, mkdirSync } from "node:fs";
1968
- import { resolve as resolve6, dirname as dirname3 } from "node:path";
1974
+ import { readFileSync as readFileSync4, writeFileSync as writeFileSync2, existsSync as existsSync5, mkdirSync, copyFileSync } from "node:fs";
1975
+ import { resolve as resolve6, dirname as dirname3, join as join5 } from "node:path";
1969
1976
  import { fileURLToPath } from "node:url";
1970
1977
  import { homedir } from "node:os";
1978
+ var STABLE_HOOK_DIR = resolve6(homedir(), ".claude", "hex-line");
1979
+ var STABLE_HOOK_PATH = join5(STABLE_HOOK_DIR, "hook.mjs").replace(/\\/g, "/");
1980
+ var HOOK_COMMAND = `node ${STABLE_HOOK_PATH}`;
1971
1981
  var __filename = fileURLToPath(import.meta.url);
1972
1982
  var __dirname = dirname3(__filename);
1973
- var HOOK_SCRIPT = resolve6(__dirname, "..", "hook.mjs").replace(/\\/g, "/");
1974
- var HOOK_COMMAND = `node ${HOOK_SCRIPT}`;
1975
- var HOOK_SIGNATURE = "hex-line-mcp/hook.mjs";
1976
- var NPX_MARKERS = ["_npx", "npx-cache", ".npm/_npx"];
1977
- function isEphemeralInstall(scriptPath) {
1978
- return NPX_MARKERS.some((m) => scriptPath.includes(m));
1979
- }
1983
+ var SOURCE_HOOK = resolve6(__dirname, "..", "hook.mjs");
1984
+ var DIST_HOOK = resolve6(__dirname, "hook.mjs");
1985
+ var HOOK_SIGNATURE = "hex-line";
1980
1986
  var CLAUDE_HOOKS = {
1981
1987
  SessionStart: {
1982
1988
  matcher: "*",
@@ -2038,7 +2044,7 @@ function writeHooksToFile(settingsPath, label) {
2038
2044
  return `Claude (${label}): already configured`;
2039
2045
  }
2040
2046
  writeJson(settingsPath, config);
2041
- return `Claude (${label}): hooks -> ${HOOK_SCRIPT} OK`;
2047
+ return `Claude (${label}): hooks -> ${STABLE_HOOK_PATH} OK`;
2042
2048
  }
2043
2049
  function cleanLocalHooks() {
2044
2050
  const localPath = resolve6(process.cwd(), ".claude/settings.local.json");
@@ -2084,10 +2090,14 @@ function installOutputStyle() {
2084
2090
  return msg;
2085
2091
  }
2086
2092
  function setupClaude() {
2087
- if (isEphemeralInstall(HOOK_SCRIPT)) {
2088
- return "Claude: SKIPPED \u2014 hook.mjs is in npx cache (ephemeral). Install permanently: npm i -g @levnikolaevich/hex-line-mcp, then re-run setup_hooks.";
2089
- }
2090
2093
  const results = [];
2094
+ const hookSource = existsSync5(DIST_HOOK) ? DIST_HOOK : SOURCE_HOOK;
2095
+ if (!existsSync5(hookSource)) {
2096
+ return "Claude: FAILED \u2014 hook.mjs not found. Reinstall @levnikolaevich/hex-line-mcp.";
2097
+ }
2098
+ mkdirSync(STABLE_HOOK_DIR, { recursive: true });
2099
+ copyFileSync(hookSource, STABLE_HOOK_PATH);
2100
+ results.push(`hook.mjs -> ${STABLE_HOOK_PATH}`);
2091
2101
  const globalPath = resolve6(homedir(), ".claude/settings.json");
2092
2102
  results.push(writeHooksToFile(globalPath, "global"));
2093
2103
  results.push(cleanLocalHooks());
@@ -2266,7 +2276,7 @@ Summary: ${summary}`);
2266
2276
 
2267
2277
  // lib/bulk-replace.mjs
2268
2278
  import { writeFileSync as writeFileSync3, readdirSync as readdirSync3 } from "node:fs";
2269
- import { resolve as resolve7, relative as relative3, join as join5 } from "node:path";
2279
+ import { resolve as resolve7, relative as relative3, join as join6 } from "node:path";
2270
2280
  var ignoreMod;
2271
2281
  try {
2272
2282
  ignoreMod = await import("ignore");
@@ -2282,7 +2292,7 @@ function walkFiles(dir, rootDir, ig) {
2282
2292
  }
2283
2293
  for (const e of entries) {
2284
2294
  if (e.name === ".git" || e.name === "node_modules") continue;
2285
- const full = join5(dir, e.name);
2295
+ const full = join6(dir, e.name);
2286
2296
  const rel = relative3(rootDir, full).replace(/\\/g, "/");
2287
2297
  if (ig && ig.ignores(rel)) continue;
2288
2298
  if (e.isDirectory()) {
@@ -2294,21 +2304,21 @@ function walkFiles(dir, rootDir, ig) {
2294
2304
  return results;
2295
2305
  }
2296
2306
  function globMatch(filename, pattern) {
2297
- const re = pattern.replace(/\./g, "\\.").replace(/\*\*/g, "\0").replace(/\*/g, "[^/]*").replace(/\0/g, ".*").replace(/\?/g, ".");
2307
+ const re = pattern.replace(/\./g, "\\.").replace(/\{([^}]+)\}/g, (_, alts) => "(" + alts.split(",").join("|") + ")").replace(/\*\*/g, "\0").replace(/\*/g, "[^/]*").replace(/\0/g, ".*").replace(/\?/g, ".");
2298
2308
  return new RegExp("^" + re + "$").test(filename);
2299
2309
  }
2300
2310
  function loadGitignore2(rootDir) {
2301
2311
  if (!ignoreMod) return null;
2302
2312
  const ig = (ignoreMod.default || ignoreMod)();
2303
2313
  try {
2304
- const content = readText(join5(rootDir, ".gitignore"));
2314
+ const content = readText(join6(rootDir, ".gitignore"));
2305
2315
  ig.add(content);
2306
2316
  } catch {
2307
2317
  }
2308
2318
  return ig;
2309
2319
  }
2310
2320
  function bulkReplace(rootDir, globPattern, replacements, opts = {}) {
2311
- const { dryRun = false, maxFiles = 100 } = opts;
2321
+ const { dryRun = false, maxFiles = 100, format = "compact" } = opts;
2312
2322
  const abs = resolve7(normalizePath(rootDir));
2313
2323
  const ig = loadGitignore2(abs);
2314
2324
  const allFiles = walkFiles(abs, abs, ig);
@@ -2321,51 +2331,63 @@ function bulkReplace(rootDir, globPattern, replacements, opts = {}) {
2321
2331
  return `TOO_MANY_FILES: Found ${files.length} files, max_files is ${maxFiles}. Use more specific glob or increase max_files.`;
2322
2332
  }
2323
2333
  const results = [];
2324
- let changed = 0, skipped = 0, errors = 0;
2325
- const MAX_OUTPUT2 = MAX_OUTPUT_CHARS;
2326
- let totalChars = 0;
2334
+ let changed = 0, skipped = 0, errors = 0, totalReplacements = 0;
2327
2335
  for (const file of files) {
2328
2336
  try {
2329
2337
  const original = readText(file);
2330
2338
  let content = original;
2339
+ let replacementCount = 0;
2331
2340
  for (const { old: oldText, new: newText } of replacements) {
2332
- content = content.split(oldText).join(newText);
2341
+ if (oldText === newText) continue;
2342
+ const parts = content.split(oldText);
2343
+ replacementCount += parts.length - 1;
2344
+ content = parts.join(newText);
2333
2345
  }
2334
2346
  if (content === original) {
2335
2347
  skipped++;
2336
2348
  continue;
2337
2349
  }
2338
- const diff = simpleDiff(original.split("\n"), content.split("\n"));
2339
2350
  if (!dryRun) {
2340
2351
  writeFileSync3(file, content, "utf-8");
2341
2352
  }
2342
- const relPath = file.replace(abs, "").replace(/^[/\\]/, "");
2343
- results.push(`--- ${relPath}
2344
- ${diff || "(no visible diff)"}`);
2353
+ const relPath = relative3(abs, file).replace(/\\/g, "/");
2354
+ totalReplacements += replacementCount;
2345
2355
  changed++;
2346
- totalChars += results[results.length - 1].length;
2347
- if (totalChars > MAX_OUTPUT2) {
2348
- const remaining = files.length - files.indexOf(file) - 1;
2349
- if (remaining > 0) results.push(`OUTPUT_CAPPED: ${remaining} more files not shown. Output exceeded ${MAX_OUTPUT2} chars.`);
2350
- break;
2356
+ if (format === "full") {
2357
+ const diff = simpleDiff(original.split("\n"), content.split("\n"));
2358
+ let diffText = diff || "(no visible diff)";
2359
+ const diffLines3 = diffText.split("\n");
2360
+ if (diffLines3.length > MAX_PER_FILE_DIFF_LINES) {
2361
+ const omitted = diffLines3.length - MAX_PER_FILE_DIFF_LINES;
2362
+ diffText = diffLines3.slice(0, MAX_PER_FILE_DIFF_LINES).join("\n") + `
2363
+ --- ${omitted} lines omitted ---`;
2364
+ }
2365
+ results.push(`--- ${relPath}: ${replacementCount} replacements
2366
+ ${diffText}`);
2367
+ } else {
2368
+ results.push(`--- ${relPath}: ${replacementCount} replacements`);
2351
2369
  }
2352
2370
  } catch (e) {
2353
2371
  results.push(`ERROR: ${file}: ${e.message}`);
2354
2372
  errors++;
2355
2373
  }
2356
2374
  }
2357
- const header = `Bulk replace: ${changed} files changed, ${skipped} skipped, ${errors} errors (dry_run: ${dryRun})`;
2358
- return results.length ? `${header}
2375
+ const header = `Bulk replace: ${changed} files changed (${totalReplacements} replacements), ${skipped} skipped, ${errors} errors (dry_run: ${dryRun})`;
2376
+ let output = results.length ? `${header}
2359
2377
 
2360
2378
  ${results.join("\n\n")}` : header;
2379
+ if (output.length > MAX_BULK_OUTPUT_CHARS) {
2380
+ output = output.slice(0, MAX_BULK_OUTPUT_CHARS) + `
2381
+ OUTPUT_CAPPED: Output exceeded ${MAX_BULK_OUTPUT_CHARS} chars.`;
2382
+ }
2383
+ return output;
2361
2384
  }
2362
2385
 
2363
2386
  // server.mjs
2364
- var version = true ? "1.3.6" : (await null).createRequire(import.meta.url)("./package.json").version;
2387
+ var version = true ? "1.4.0" : (await null).createRequire(import.meta.url)("./package.json").version;
2365
2388
  var { server, StdioServerTransport } = await createServerRuntime({
2366
2389
  name: "hex-line-mcp",
2367
- version,
2368
- installDir: "mcp/hex-line-mcp"
2390
+ version
2369
2391
  });
2370
2392
  var replacementPairsSchema = z2.array(
2371
2393
  z2.object({ old: z2.string().min(1), new: z2.string() })
@@ -2645,13 +2667,14 @@ server.registerTool("changes", {
2645
2667
  });
2646
2668
  server.registerTool("bulk_replace", {
2647
2669
  title: "Bulk Replace",
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.",
2670
+ description: "Search-and-replace across multiple files. Finds files by glob, applies ordered text replacements. Default format is compact (summary only); use format:'full' for capped diffs. Use dry_run:true to preview. For single-file rename, set glob to the filename.",
2649
2671
  inputSchema: z2.object({
2650
2672
  replacements: z2.union([z2.string(), replacementPairsSchema]).describe('JSON array of {old, new} pairs: [{"old":"foo","new":"bar"}]'),
2651
2673
  glob: z2.string().optional().describe('File glob (default: "**/*.{md,mjs,json,yml,ts,js}")'),
2652
2674
  path: z2.string().optional().describe("Root directory (default: cwd)"),
2653
2675
  dry_run: flexBool().describe("Preview without writing (default: false)"),
2654
- max_files: flexNum().describe("Max files to process (default: 100)")
2676
+ max_files: flexNum().describe("Max files to process (default: 100)"),
2677
+ format: z2.enum(["compact", "full"]).optional().describe('"compact" (default) = summary only, "full" = include capped diffs')
2655
2678
  }),
2656
2679
  annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false }
2657
2680
  }, async (rawParams) => {
@@ -2669,7 +2692,7 @@ server.registerTool("bulk_replace", {
2669
2692
  params.path || process.cwd(),
2670
2693
  params.glob || "**/*.{md,mjs,json,yml,ts,js}",
2671
2694
  replacements,
2672
- { dryRun: params.dry_run || false, maxFiles: params.max_files || 100 }
2695
+ { dryRun: params.dry_run || false, maxFiles: params.max_files || 100, format: params.format }
2673
2696
  );
2674
2697
  return { content: [{ type: "text", text: result }] };
2675
2698
  } catch (e) {
package/output-style.md CHANGED
@@ -12,15 +12,17 @@ keep-coding-instructions: true
12
12
  |-----------|-----|-----|
13
13
  | Read | `mcp__hex-line__read_file` | Hash-annotated, revision-aware |
14
14
  | Edit | `mcp__hex-line__edit_file` | Hash-verified anchors + conservative auto-rebase |
15
- | Write | `mcp__hex-line__write_file` | Consistent workflow |
15
+ | Write | `mcp__hex-line__write_file` | No prior Read needed |
16
16
  | Grep | `mcp__hex-line__grep_search` | Hash-annotated matches |
17
17
  | Edit (text rename) | `mcp__hex-line__bulk_replace` | Multi-file text rename/refactor |
18
+ | Bash `find`/`tree` | `mcp__hex-line__directory_tree` | Pattern search, gitignore-aware |
18
19
 
19
20
  ## Efficient File Reading
20
21
 
21
22
  For UNFAMILIAR code files >100 lines, PREFER:
22
- 1. `outline` first (10-20 lines of structure)
23
+ 1. `outline` first (code files only — not .md/.json/.yaml)
23
24
  2. `read_file` with offset/limit for the specific section you need
25
+ 3. Batch: `paths` array reads multiple files in one call
24
26
 
25
27
  Avoid reading a large file in full — outline+targeted read saves 75% tokens.
26
28
 
@@ -33,7 +35,7 @@ Prefer:
33
35
  1. collect all known hunks for one file
34
36
  2. send one `edit_file` call with batched edits
35
37
  3. carry `revision` from `read_file` into `base_revision` on follow-up edits
36
- 4. use `replace_between` for large block rewrites
38
+ 4. edit types: `set_line` (1 line), `replace_lines` (range + checksum), `insert_after`, `replace_between` (large blocks)
37
39
  5. use `verify` before rereading a file after staleness
38
40
 
39
41
  Avoid:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@levnikolaevich/hex-line-mcp",
3
- "version": "1.3.6",
3
+ "version": "1.4.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. 11 tools: read, edit, write, grep, outline, verify, directory_tree, file_info, setup_hooks, changes, bulk_replace.",
@@ -28,7 +28,7 @@
28
28
  "_dep_notes": {
29
29
  "web-tree-sitter": "Pinned ^0.25.0: v0.26 ABI incompatible with tree-sitter-wasms 0.1.x (built with tree-sitter-cli 0.20.8). Language.load() silently fails.",
30
30
  "zod": "Pinned ^3.25.0: zod 4 breaks zod-to-json-schema (used by MCP SDK internally). Tool parameter descriptions not sent to clients. Revisit when MCP SDK switches to z.toJSONSchema().",
31
- "better-sqlite3": "Optional. Used only by lib/graph-enrich.mjs for readonly access to hex-graph .codegraph/index.db. Graceful fallback if absent."
31
+ "better-sqlite3": "Optional. Used only by lib/graph-enrich.mjs for readonly access to hex-graph .hex-skills/codegraph/index.db. Graceful fallback if absent."
32
32
  },
33
33
  "dependencies": {
34
34
  "@modelcontextprotocol/sdk": "^1.27.0",