@levnikolaevich/hex-line-mcp 1.9.0 → 1.11.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
- ### 10 MCP Tools
14
+ ### 9 MCP Tools
15
15
 
16
16
  Core day-to-day tools:
17
17
 
@@ -25,20 +25,18 @@ Core day-to-day tools:
25
25
  Advanced / occasional:
26
26
 
27
27
  - `write_file`
28
- - `directory_tree`
29
- - `get_file_info`
28
+ - `inspect_path`
30
29
  - `changes`
31
30
 
32
31
  | Tool | Description | Key Feature |
33
32
  |------|-------------|-------------|
34
- | `read_file` | Read file with hash-annotated lines, checksums, and revision | Partial reads via `offset`/`limit` or `ranges`, compact output by default |
33
+ | `read_file` | Read file with hash-annotated lines, checksums, revision, and automatic graph hints when available | Partial reads via `offset`/`limit` or `ranges`, compact output by default |
35
34
  | `edit_file` | Revision-aware anchor edits (`set_line`, `replace_lines`, `insert_after`, `replace_between`) | Batched same-file edits + conservative auto-rebase |
36
35
  | `write_file` | Create new file or overwrite, auto-creates parent dirs | Path validation, no hash overhead |
37
36
  | `grep_search` | Search with ripgrep, 3 output modes, per-group checksums | Plain `files`/`count`, compact edit-ready `content` |
38
- | `outline` | AST-based structural overview with hash anchors via tree-sitter WASM. Supports code (15+ langs) and fence-aware markdown headings | 95% token reduction, direct edit anchors |
37
+ | `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 |
39
38
  | `verify` | Check if held checksums / revision are still current | Staleness check without full re-read |
40
- | `directory_tree` | Compact directory tree with root .gitignore support | Skips node_modules/.git, shows file sizes |
41
- | `get_file_info` | File metadata without reading content | Size, lines, mtime, type, binary detection |
39
+ | `inspect_path` | Unified file-or-directory inspection | File metadata for files, tree or pattern search for directories |
42
40
  | `changes` | Compare file against git ref, shows added/removed/modified symbols | AST-level semantic diff |
43
41
  | `bulk_replace` | Search-and-replace across multiple files by glob | Compact summary (default) or capped diffs via `format`, dry_run, max_files |
44
42
 
@@ -93,17 +91,20 @@ Comparative built-in vs hex-line benchmarks are maintained outside this package.
93
91
 
94
92
  ### Optional Graph Enrichment
95
93
 
96
- 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`.
94
+ If a project already has `.hex-skills/codegraph/index.db`, `hex-line` automatically adds lightweight graph hints to `read_file`, `outline`, `grep_search`, `edit_file`, and `changes`.
97
95
 
98
- - Graph enrichment is optional. If `.hex-skills/codegraph/index.db` is missing, `hex-line` falls back to standard behavior silently.
96
+ - Graph enrichment is optional. If `.hex-skills/codegraph/index.db` is missing, stale, or unreadable, `hex-line` falls back to standard behavior silently.
99
97
  - `better-sqlite3` is optional. If it is unavailable, `hex-line` still works without graph hints.
100
- - `edit_file` reports **Call impact**, not full semantic blast radius. The warning uses call-graph callers only.
98
+ - `read_file`, `outline`, and `grep_search` stay compact: they only surface high-signal local facts such as `api`, framework entrypoints, callers, flow, and clone hints.
99
+ - `edit_file` and `changes` surface the deeper review layer: external callers, downstream return/property flow, clone peers, public API risk, framework entrypoint risk, and same-name sibling warnings when present.
101
100
 
102
101
  `hex-line` does not read `hex-graph` internals directly anymore. The integration uses a small read-only contract exposed by `hex-graph-mcp`:
103
102
 
104
- - `hex_line_contract`
105
- - `hex_line_symbol_annotations`
106
- - `hex_line_call_edges`
103
+ - `hex_line_symbols`
104
+ - `hex_line_line_facts`
105
+ - `hex_line_edit_impacts`
106
+ - `hex_line_edit_impact_facts`
107
+ - `hex_line_clone_siblings`
107
108
 
108
109
  ## Tools Reference
109
110
 
@@ -131,16 +132,15 @@ Use `bulk_replace` for text rename patterns across one or more files. Returns co
131
132
 
132
133
  ### read_file
133
134
 
134
- Read a file as canonical edit-ready blocks. Each valid range becomes a `read_range` block with absolute span, line entries, and a checksum covering exactly the emitted lines. Invalid ranges become explicit diagnostic blocks. Supports batch reads, multi-range reads, and directory listing.
135
+ Read a file as canonical edit-ready blocks. Each valid range becomes a `read_range` block with absolute span, line entries, and a checksum covering exactly the emitted lines. Invalid ranges become explicit diagnostic blocks. Supports batch reads and multi-range reads. Directories go through `inspect_path`.
135
136
 
136
137
  | Parameter | Type | Required | Description |
137
138
  |-----------|------|----------|-------------|
138
- | `path` | string | yes | File or directory path |
139
+ | `path` | string | yes | File path |
139
140
  | `paths` | string[] | no | Array of file paths to read (batch mode) |
140
141
  | `offset` | number | no | Start line, 1-indexed (default: 1) |
141
142
  | `limit` | number | no | Max lines to return (default: 2000, 0 = all) |
142
143
  | `ranges` | array | no | Explicit line ranges, e.g. `[{ "start": 10, "end": 30 }]` |
143
- | `include_graph` | boolean | no | Opt in to graph annotations when the graph index exists |
144
144
  | `plain` | boolean | no | Omit hashes, output `lineNum\|content` instead |
145
145
 
146
146
  Default output is compact but block-structured:
@@ -227,13 +227,13 @@ Search file contents using ripgrep. Three output modes: `content` (canonical `se
227
227
 
228
228
  ### outline
229
229
 
230
- AST-based structural outline with hash anchors for direct `edit_file` usage. Supports code files (15+ languages) and fence-aware markdown heading navigation (`.md`/`.mdx`). Each entry includes a hash tag for immediate anchor use without intermediate `read_file`.
230
+ AST-based structural outline with hash anchors for direct `edit_file` usage. Supports JavaScript/TypeScript, Python, C#, PHP, and fence-aware markdown heading navigation (`.md`/`.mdx`). Each entry includes a hash tag for immediate anchor use without intermediate `read_file`.
231
231
 
232
232
  | Parameter | Type | Required | Description |
233
233
  |-----------|------|----------|-------------|
234
234
  | `path` | string | yes | Source file path |
235
235
 
236
- Supported languages: JavaScript, TypeScript (JSX/TSX), Python, Go, Rust, Java, C, C++, C#, Ruby, PHP, Kotlin, Swift, Bash -- 15+ via tree-sitter WASM.
236
+ Supported languages: JavaScript (`.js`, `.mjs`, `.cjs`, `.jsx`), TypeScript (`.ts`, `.tsx`), Python (`.py`), C# (`.cs`), and PHP (`.php`) via tree-sitter WASM.
237
237
 
238
238
  Not for `.json`, `.yaml`, `.txt` -- use `read_file` directly for those.
239
239
 
@@ -260,30 +260,22 @@ changed_ranges: 10-12(replace)
260
260
  STALE 10-12 checksum: 10-12:oldc0de0 current=10-12:newc0de0
261
261
  ```
262
262
 
263
- ### directory_tree
263
+ ### inspect_path
264
264
 
265
- Compact directory tree with root .gitignore support (path-based rules, negation, dir-only). Nested .gitignore files are not loaded.
265
+ Inspect a file or directory path without guessing which low-level tool to call first.
266
266
 
267
267
  | Parameter | Type | Required | Description |
268
268
  |-----------|------|----------|-------------|
269
- | `path` | string | yes | Directory path |
269
+ | `path` | string | yes | File or directory path |
270
270
  | `pattern` | string | no | Glob filter on names (e.g. `"*-mcp"`, `"*.mjs"`). Returns flat match list instead of tree |
271
271
  | `type` | string | no | `"file"`, `"dir"`, or `"all"` (default). Like `find -type f/d` |
272
272
  | `max_depth` | number | no | Max recursion depth (default: 3, or 20 in pattern mode) |
273
273
  | `gitignore` | boolean | no | Respect root .gitignore patterns (default: true). Nested .gitignore not supported |
274
- | `format` | string | no | `"compact"` = names only, no sizes, depth 1. `"full"` = default with sizes |
275
-
276
- Skips `node_modules`, `.git`, `dist`, `build`, `__pycache__`, `.next`, `coverage` by default.
277
-
278
- ### get_file_info
279
-
280
- File metadata without reading content.
281
-
282
- | Parameter | Type | Required | Description |
283
- |-----------|------|----------|-------------|
284
- | `path` | string | yes | File path |
274
+ | `format` | string | no | `"compact"` = shorter path view. `"full"` = include sizes / metadata where available |
285
275
 
286
- Returns: size, line count, modification time (absolute + relative), file type, binary detection.
276
+ - For regular files it returns compact metadata: size, line count when cheap, modification time, type, and binary flag.
277
+ - For directories it returns a gitignore-aware tree.
278
+ - With `pattern`, it switches to flat match mode and works as the preferred replacement for `find` / recursive `ls`.
287
279
 
288
280
  ## Hook
289
281
 
@@ -322,11 +314,10 @@ Injects a short operational workflow into agent context at session start: no `To
322
314
 
323
315
  ```
324
316
  hex-line-mcp/
325
- server.mjs MCP server (stdio transport, 11 tools)
317
+ server.mjs MCP server (stdio transport, 9 tools)
326
318
  hook.mjs Unified hook (PreToolUse + PostToolUse + SessionStart)
327
319
  package.json
328
320
  lib/
329
- hash.mjs FNV-1a hashing, 2-char tags, range checksums
330
321
  read.mjs File reading with hash annotation
331
322
  edit.mjs Anchor-based edits, diff output
332
323
  search.mjs ripgrep wrapper with hash-annotated results
@@ -334,13 +325,15 @@ hex-line-mcp/
334
325
  verify.mjs Range checksum verification
335
326
  info.mjs File metadata (size, lines, mtime, type)
336
327
  tree.mjs Directory tree with .gitignore support
328
+ inspect-path.mjs Unified file/directory inspection
337
329
  changes.mjs Semantic git diff via AST
338
330
  bulk-replace.mjs Multi-file search-and-replace
339
331
  setup.mjs Claude hook installation + output style setup
340
332
  format.mjs Output formatting utilities
341
- coerce.mjs Parameter pass-through (identity)
342
333
  security.mjs Path validation, binary detection, size limits
343
- normalize.mjs Output normalization, deduplication, truncation
334
+ @levnikolaevich/hex-common/
335
+ text-protocol/hash.mjs Shared FNV-1a hashing and checksum protocol
336
+ output/normalize.mjs Shared output normalization helpers
344
337
  ```
345
338
 
346
339
  ### Hash Format
@@ -390,7 +383,7 @@ The edit is rejected with an error showing which lines changed since the last re
390
383
  <details>
391
384
  <summary><b>Is outline available for all file types?</b></summary>
392
385
 
393
- Outline works on code files (15+ languages via tree-sitter WASM) and markdown heading navigation (`.md`/`.mdx`, fenced code blocks ignored). For JSON, YAML, and text files use `read_file` directly. Each outline entry includes a hash anchor (`tag.line-range: symbol`) for direct use in `edit_file`.
386
+ Outline works on JavaScript/TypeScript, Python, C#, PHP, and markdown heading navigation (`.md`/`.mdx`, fenced code blocks ignored). For JSON, YAML, and text files use `read_file` directly. Each outline entry includes a hash anchor (`tag.line-range: symbol`) for direct use in `edit_file`.
394
387
 
395
388
  </details>
396
389
 
@@ -0,0 +1,57 @@
1
+ {
2
+ "generated_from": {
3
+ "source": "tree-sitter-cli-build",
4
+ "tree_sitter_cli_version": "0.26.8",
5
+ "note": "Repo-owned grammar WASM artifacts built from pinned npm grammar packages with the current tree-sitter CLI."
6
+ },
7
+ "grammars": [
8
+ {
9
+ "grammar": "javascript",
10
+ "file": "tree-sitter-javascript.wasm",
11
+ "package": "tree-sitter-javascript",
12
+ "version": "0.25.0",
13
+ "source_subdir": null,
14
+ "sha256": "29d37c59b7797fa1c56419816d1730d5ff5e2ed2077bc44c5d6f50082be09136"
15
+ },
16
+ {
17
+ "grammar": "typescript",
18
+ "file": "tree-sitter-typescript.wasm",
19
+ "package": "tree-sitter-typescript",
20
+ "version": "0.23.2",
21
+ "source_subdir": "typescript",
22
+ "sha256": "f7c442260d8bf63b89762af2663655d153f84d169b4b8fedd15a5ba9b29ed3a4"
23
+ },
24
+ {
25
+ "grammar": "tsx",
26
+ "file": "tree-sitter-tsx.wasm",
27
+ "package": "tree-sitter-typescript",
28
+ "version": "0.23.2",
29
+ "source_subdir": "tsx",
30
+ "sha256": "4f795eb1fd00d14b7d284add59df8796d3c4b30e49d27979cba70c02298d5f67"
31
+ },
32
+ {
33
+ "grammar": "python",
34
+ "file": "tree-sitter-python.wasm",
35
+ "package": "tree-sitter-python",
36
+ "version": "0.25.0",
37
+ "source_subdir": null,
38
+ "sha256": "6183aa51eecace847badb487766e33a0f7725c257b479a0b89c9ff1bcec9c610"
39
+ },
40
+ {
41
+ "grammar": "c_sharp",
42
+ "file": "tree-sitter-c_sharp.wasm",
43
+ "package": "tree-sitter-c-sharp",
44
+ "version": "0.23.1",
45
+ "source_subdir": null,
46
+ "sha256": "974cb3e3302c04a5b5e6734494af2ed2fd0c284c38693316ef013922639aecf2"
47
+ },
48
+ {
49
+ "grammar": "php",
50
+ "file": "tree-sitter-php.wasm",
51
+ "package": "tree-sitter-php",
52
+ "version": "0.24.2",
53
+ "source_subdir": "php",
54
+ "sha256": "393e65be27e256ed2f1a43e0e70e43de05f77b5563d7dd877bdb44760d5560a6"
55
+ }
56
+ ]
57
+ }
package/dist/hook.mjs CHANGED
@@ -54,7 +54,7 @@ function normalizeOutput(text, opts = {}) {
54
54
  }
55
55
 
56
56
  // hook.mjs
57
- import { readFileSync, statSync } from "node:fs";
57
+ import { readFileSync, statSync, writeSync } from "node:fs";
58
58
  import { resolve } from "node:path";
59
59
  import { homedir } from "node:os";
60
60
  import { fileURLToPath } from "node:url";
@@ -97,27 +97,15 @@ var OUTLINEABLE_EXT = /* @__PURE__ */ new Set([
97
97
  ".ts",
98
98
  ".tsx",
99
99
  ".py",
100
- ".go",
101
- ".rs",
102
- ".java",
103
- ".c",
104
- ".h",
105
- ".cpp",
106
100
  ".cs",
107
- ".rb",
108
- ".php",
109
- ".kt",
110
- ".swift",
111
- ".sh",
112
- ".bash"
101
+ ".php"
113
102
  ]);
114
103
  var REVERSE_TOOL_HINTS = {
115
104
  "mcp__hex-line__read_file": "Read (file_path, offset, limit)",
116
105
  "mcp__hex-line__edit_file": "Edit (old_string, new_string, replace_all)",
117
106
  "mcp__hex-line__write_file": "Write (file_path, content)",
118
107
  "mcp__hex-line__grep_search": "Grep (pattern, path)",
119
- "mcp__hex-line__directory_tree": "Glob (pattern) or Bash(ls)",
120
- "mcp__hex-line__get_file_info": "Bash(stat/wc)",
108
+ "mcp__hex-line__inspect_path": "Path info / tree / Bash(ls,stat)",
121
109
  "mcp__hex-line__outline": "Read with offset/limit",
122
110
  "mcp__hex-line__verify": "Read (re-read file to check freshness)",
123
111
  "mcp__hex-line__changes": "Bash(git diff)",
@@ -131,8 +119,8 @@ var TOOL_HINTS = {
131
119
  cat: "mcp__hex-line__read_file (not cat/head/tail/less/more)",
132
120
  head: "mcp__hex-line__read_file with limit param (not head)",
133
121
  tail: "mcp__hex-line__read_file with offset param (not tail)",
134
- ls: "mcp__hex-line__directory_tree with pattern param (not ls/find/tree). E.g. pattern='*-mcp' type='dir'",
135
- stat: "mcp__hex-line__get_file_info (not stat/wc/file)",
122
+ ls: "mcp__hex-line__inspect_path for tree or pattern search (not ls/find/tree). E.g. pattern='*-mcp' type='dir'",
123
+ stat: "mcp__hex-line__inspect_path for compact file metadata (not stat/wc/file)",
136
124
  grep: "mcp__hex-line__grep_search (not grep/rg). Params: output, literal, context_before, context_after, multiline",
137
125
  sed: "mcp__hex-line__edit_file for hash edits, or mcp__hex-line__bulk_replace for text rename (not sed -i)",
138
126
  diff: "mcp__hex-line__changes (not diff). Git diff with change symbols",
@@ -141,6 +129,7 @@ var TOOL_HINTS = {
141
129
  changes: "mcp__hex-line__changes (git diff with change symbols)",
142
130
  bulk: "mcp__hex-line__bulk_replace (multi-file search-replace)"
143
131
  };
132
+ var DEFERRED_HINT = "If schemas not loaded: ToolSearch('+hex-line read edit')";
144
133
  var BASH_REDIRECTS = [
145
134
  { regex: /^cat\s+\S+/, key: "cat" },
146
135
  { regex: /^head\s+/, key: "head" },
@@ -282,16 +271,25 @@ function getHookMode() {
282
271
  }
283
272
  return _hookMode;
284
273
  }
274
+ function safeExit(fd, data, code) {
275
+ writeSync(fd, data);
276
+ process.exit(code);
277
+ }
278
+ function debugLog(action, reason) {
279
+ writeSync(2, `[hex-hook] ${action}: ${reason}
280
+ `);
281
+ }
285
282
  function block(reason, context) {
283
+ const msg = context ? `${reason}
284
+ ${context}` : reason;
286
285
  const output = {
287
286
  hookSpecificOutput: {
288
287
  permissionDecision: "deny"
289
288
  },
290
- systemMessage: context ? `${reason}
291
- ${context}` : reason
289
+ systemMessage: msg
292
290
  };
293
- process.stdout.write(JSON.stringify(output));
294
- process.exit(2);
291
+ debugLog("BLOCK", reason);
292
+ safeExit(1, JSON.stringify(output), 2);
295
293
  }
296
294
  function advise(reason, context) {
297
295
  const output = {
@@ -301,8 +299,7 @@ function advise(reason, context) {
301
299
  systemMessage: context ? `${reason}
302
300
  ${context}` : reason
303
301
  };
304
- process.stdout.write(JSON.stringify(output));
305
- process.exit(0);
302
+ safeExit(1, JSON.stringify(output), 0);
306
303
  }
307
304
  function redirect(reason, context) {
308
305
  if (getHookMode() === "advisory") {
@@ -324,24 +321,20 @@ function handlePreToolUse(data) {
324
321
  if (BINARY_EXT.has(extOf(filePath))) {
325
322
  process.exit(0);
326
323
  }
327
- const normalPath = filePath.replace(/\\/g, "/");
328
- if (normalPath.includes(".claude/plans/") || normalPath.includes("AppData")) {
324
+ const resolvedNorm = resolveToolPath(filePath).replace(/\\/g, "/");
325
+ const cwdNorm = process.cwd().replace(/\\/g, "/");
326
+ const homeNorm = homedir().replace(/\\/g, "/");
327
+ const claudeAllow = [
328
+ cwdNorm + "/.claude/settings.json",
329
+ cwdNorm + "/.claude/settings.local.json",
330
+ homeNorm + "/.claude/settings.json",
331
+ homeNorm + "/.claude/settings.local.json"
332
+ ];
333
+ if (claudeAllow.some((p) => resolvedNorm.toLowerCase() === p.toLowerCase())) {
329
334
  process.exit(0);
330
335
  }
331
- const ALLOWED_CONFIGS = /* @__PURE__ */ new Set(["settings.json", "settings.local.json"]);
332
- const fileName = normalPath.split("/").pop();
333
- if (ALLOWED_CONFIGS.has(fileName)) {
334
- let candidate = filePath;
335
- if (candidate.startsWith("~/")) {
336
- candidate = homedir().replace(/\\/g, "/") + candidate.slice(1);
337
- }
338
- const absPath = resolve(process.cwd(), candidate).replace(/\\/g, "/");
339
- const projectClaude = resolve(process.cwd(), ".claude").replace(/\\/g, "/") + "/";
340
- const globalClaude = resolve(homedir(), ".claude").replace(/\\/g, "/") + "/";
341
- const cmp = process.platform === "win32" ? (a, b) => a.toLowerCase().startsWith(b.toLowerCase()) : (a, b) => a.startsWith(b);
342
- if (cmp(absPath, projectClaude) || cmp(absPath, globalClaude)) {
343
- process.exit(0);
344
- }
336
+ if (resolvedNorm.includes("/.claude/")) {
337
+ redirect("Protected .claude/ path. Use built-in tools for .claude/ config files.");
345
338
  }
346
339
  if (toolName === "Read") {
347
340
  if (isPartialRead(toolInput)) {
@@ -349,29 +342,30 @@ function handlePreToolUse(data) {
349
342
  }
350
343
  if (fileSize !== null && fileSize <= LARGE_FILE_BYTES) {
351
344
  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);
345
+ const hint = filePath && OUTLINEABLE_EXT.has(ext2) ? `Use mcp__hex-line__outline(path="${filePath}") for structure, then mcp__hex-line__read_file(path="${filePath}") with ranges.` : filePath ? `Use mcp__hex-line__read_file(path="${filePath}"). Built-in Read wastes edit context.` : "Use mcp__hex-line__read_file. Built-in Read wastes edit context.";
346
+ advise(hint, DEFERRED_HINT);
354
347
  }
355
348
  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.");
349
+ 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__inspect_path or mcp__hex-line__read_file";
350
+ redirect(outlineHint, "Do not use built-in Read for full reads of large files.\n" + DEFERRED_HINT);
358
351
  }
359
352
  if (toolName === "Edit") {
360
353
  const oldText = String(toolInput.old_string || "");
361
354
  const isLargeEdit = Boolean(toolInput.replace_all) || oldText.length > LARGE_EDIT_CHARS || fileSize !== null && fileSize > LARGE_FILE_BYTES;
362
355
  if (!isLargeEdit) {
363
- process.exit(0);
356
+ const editHint = filePath ? `Prefer mcp__hex-line__edit_file(path="${filePath}") for hash-verified edits.` : "Prefer mcp__hex-line__edit_file for hash-verified edits.";
357
+ advise(editHint);
364
358
  }
365
359
  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";
366
- redirect(target, "For large or repeated edits: locate anchors/checksums first, then call edit_file once with batched edits.");
360
+ redirect(target, "For large or repeated edits: locate anchors/checksums first, then call edit_file once with batched edits.\n" + DEFERRED_HINT);
367
361
  }
368
362
  if (toolName === "Write") {
369
363
  const pathNote = filePath ? ` with path="${filePath}"` : "";
370
- redirect(`Use mcp__hex-line__write_file${pathNote}`, TOOL_HINTS.Write);
364
+ redirect(`Use mcp__hex-line__write_file${pathNote}`, TOOL_HINTS.Write + "\n" + DEFERRED_HINT);
371
365
  }
372
366
  if (toolName === "Grep") {
373
367
  const pathNote = filePath ? ` with path="${filePath}"` : "";
374
- redirect(`Use mcp__hex-line__grep_search${pathNote}`, TOOL_HINTS.Grep);
368
+ redirect(`Use mcp__hex-line__grep_search${pathNote}`, TOOL_HINTS.Grep + "\n" + DEFERRED_HINT);
375
369
  }
376
370
  }
377
371
  if (toolName === "Bash") {
@@ -457,8 +451,7 @@ function handlePostToolUse(data) {
457
451
  `Original: ${originalCount} lines | Filtered: ${filteredCount} lines`,
458
452
  "=".repeat(50)
459
453
  ].join("\n");
460
- process.stderr.write(output);
461
- process.exit(2);
454
+ safeExit(2, output, 2);
462
455
  }
463
456
  function handleSessionStart() {
464
457
  const settingsFiles = [
@@ -479,9 +472,8 @@ function handleSessionStart() {
479
472
  }
480
473
  }
481
474
  const prefix = styleActive ? "Hex-line MCP available. Output style active.\n" : "Hex-line MCP available.\n";
482
- const msg = prefix + "Call hex-line tools directly. Do not use ToolSearch for hex-line tools.\nWorkflow:\n- Discovery: outline for code and markdown files, read_file for targeted reads, grep_search for symbol/text lookup\n- Read cheaply: prefer offset/limit or ranges; avoid full-file Read on large files\n- Edit safely: read/grep first, then one batched edit_file call per file with base_revision when available\n- Verify before reread: use verify to check checksums or revision freshness\n- Multi-file rename/refactor: use bulk_replace\n- New files: use write_file\nExceptions: images, PDFs, notebooks, .claude/settings.json, .claude/settings.local.json use built-in Read. Glob is always OK.";
483
- process.stdout.write(JSON.stringify({ systemMessage: msg }));
484
- process.exit(0);
475
+ const msg = prefix + "<hex-line_instructions>\n <deferred_loading>If hex-line schemas not loaded, run: ToolSearch('+hex-line read edit')</deferred_loading>\n <exploration>\n <rule>Use outline for structure (code + markdown), not Read. ~10-20 lines vs hundreds.</rule>\n <rule>Use read_file with offset/limit or ranges for targeted reads.</rule>\n <rule>Use grep_search before editing to get hash anchors.</rule>\n </exploration>\n <editing>\n <path name='surgical'>grep_search \u2192 edit_file (fastest: hash-verified, no full read needed)</path>\n <path name='exploratory'>outline \u2192 read_file (ranges) \u2192 edit_file with base_revision</path>\n <path name='multi-file'>bulk_replace for text rename/refactor across files</path>\n </editing>\n <tips>\n <tip>Carry revision from read_file into base_revision on edit_file.</tip>\n <tip>If edit returns CONFLICT, call verify \u2014 only reread when STALE.</tip>\n <tip>Batch multiple edits to same file in one edit_file call.</tip>\n <tip>Use write_file for new files (no prior Read needed).</tip>\n </tips>\n <exceptions>Built-in Read OK for: images, PDFs, notebooks, Glob (always), .claude/settings.json</exceptions>\n</hex-line_instructions>";
476
+ safeExit(1, JSON.stringify({ systemMessage: msg }), 0);
485
477
  }
486
478
  var _norm = (p) => p.replace(/\\/g, "/");
487
479
  if (_norm(process.argv[1]) === _norm(fileURLToPath(import.meta.url))) {