@levnikolaevich/hex-line-mcp 1.11.1 → 1.12.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 CHANGED
@@ -38,16 +38,16 @@ Advanced / occasional:
38
38
  | `verify` | Check if held checksums / revision are still current | Staleness check without full re-read |
39
39
  | `inspect_path` | Unified file-or-directory inspection | File metadata for files, tree or pattern search for directories |
40
40
  | `changes` | Compare file against git ref, shows added/removed/modified symbols | AST-level semantic diff |
41
- | `bulk_replace` | Search-and-replace across multiple files by glob | Compact summary (default) or capped diffs via `format`, dry_run, max_files |
41
+ | `bulk_replace` | Search-and-replace across multiple files inside an explicit root path | Compact summary (default) or capped diffs via `format`, dry_run, max_files |
42
42
 
43
- ### Hooks (PreToolUse + PostToolUse)
43
+ ### Hooks (SessionStart + PreToolUse + PostToolUse)
44
44
 
45
45
  | Event | Trigger | Action |
46
46
  |-------|---------|--------|
47
- | **PreToolUse** | Read/Edit/Write/Grep on text files | Size-aware redirect: cheap small operations may pass, expensive ones are redirected |
47
+ | **PreToolUse** | Read/Edit/Write/Grep on text files | Redirect-first policy for text files; built-in tools stay reserved for binary/media and `.claude/settings*.json` exceptions |
48
48
  | **PreToolUse** | Bash with dangerous commands | Blocks `rm -rf /`, `git push --force`, etc. Agent must confirm with user |
49
49
  | **PostToolUse** | Bash with 50+ lines output | RTK: deduplicates, truncates, shows filtered summary to Claude as feedback |
50
- | **SessionStart** | Session begins | Injects a short no-discovery workflow for hex-line tools |
50
+ | **SessionStart** | Session begins | Injects a short bootstrap hint; defers to the active output style when `hex-line` style is enabled |
51
51
 
52
52
 
53
53
  ### Bash Redirects
@@ -64,12 +64,16 @@ claude mcp add -s user hex-line -- hex-line-mcp
64
64
 
65
65
  ripgrep is bundled via `@vscode/ripgrep` — no manual install needed for `grep_search`.
66
66
 
67
+ Requires Node.js >= 20.19.0.
68
+
67
69
  ### Hooks
68
70
 
69
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.
70
72
 
71
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.
72
74
 
75
+ No extra manual setup is required after install. The startup sync uses the current Node runtime and a stable hook path under `~/.claude/hex-line`, so the hook command survives spaces in the home directory on Windows, macOS, and Linux.
76
+
73
77
  ## Validation
74
78
 
75
79
  Use the normal package checks:
@@ -94,6 +98,8 @@ Comparative built-in vs hex-line benchmarks are maintained outside this package.
94
98
  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`.
95
99
 
96
100
  - Graph enrichment is optional. If `.hex-skills/codegraph/index.db` is missing, stale, or unreadable, `hex-line` falls back to standard behavior silently.
101
+ - Graph enrichment is project-deterministic. `hex-line` only uses the graph database that belongs to the resolved current project scope.
102
+ - Nested projects do not inherit graph hints from a parent repo index once a nested project boundary is detected.
97
103
  - `better-sqlite3` is optional. If it is unavailable, `hex-line` still works without graph hints.
98
104
  - `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
105
  - `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.
@@ -128,7 +134,7 @@ Use `replace_between` inside `edit_file` when you know stable start/end anchors
128
134
 
129
135
  ### Literal rename / refactor
130
136
 
131
- 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.
137
+ Use `bulk_replace` for text rename patterns across one or more files inside a known project root or directory scope. Pass `path` explicitly. In normal agent workflows that scope should be auto-filled from the current project root, not typed manually. Returns compact summary by default; pass `format: "full"` for capped diffs. Do not use it as a substitute for structured block rewrites.
132
138
 
133
139
  ### read_file
134
140
 
@@ -183,14 +189,29 @@ Edit operations (JSON array):
183
189
  ]
184
190
  ```
185
191
 
192
+ Discipline:
193
+
194
+ - Never invent `range_checksum`. Copy it from `read_file` or `grep_search(output:"content")`.
195
+ - First mutation in a file: prefer `grep_search` for narrow targets, or `outline -> read_file(ranges)` for structural edits.
196
+ - Prefer 1-2 hunks on the first pass. Once `edit_file` returns a fresh `revision`, continue from that state.
197
+
186
198
  Result footer includes:
187
199
 
188
200
  - `status: OK | AUTO_REBASED | CONFLICT`
201
+ - `reason: ...` as the canonical machine-readable cause for the current status
189
202
  - `revision: ...`
190
203
  - `file: ...`
191
204
  - `changed_ranges: ...` when relevant
205
+ - `recovery_ranges: ...` with the narrowest recommended `read_file` ranges for retry
206
+ - `next_action: ...` as the canonical immediate choice: `apply_retry_edit`, `apply_retry_batch`, or `reread_then_retry`
192
207
  - `remapped_refs: ...` when stale anchors were uniquely relocated
193
- - `retry_checksum: ...` on local conflicts
208
+ - `retry_checksum: ...` on local conflicts, narrowed to the exact target range when possible
209
+ - `retry_edit: ...` when the server can synthesize a ready-to-retry edit skeleton from current local state
210
+ - `retry_edits: ...` on conservative batch conflicts when every conflicted edit can be retried directly
211
+ - `suggested_read_call: ...` when rereading is the safest next step
212
+ - `retry_plan: ...` with a compact machine-readable next-call plan
213
+ - `summary: ...` and `snippet: ...` instead of long prose blocks
214
+ - `edit_conflicts: N` on conservative multi-edit preflight conflicts
194
215
 
195
216
  ### write_file
196
217
 
@@ -239,7 +260,7 @@ Not for `.json`, `.yaml`, `.txt` -- use `read_file` directly for those.
239
260
 
240
261
  ### verify
241
262
 
242
- Check if range checksums from prior read/search blocks are still valid, optionally relative to a prior `base_revision`. Returns a deterministic verification report with `status`, `summary`, and one line per checksum entry.
263
+ Check if range checksums from prior read/search blocks are still valid, optionally relative to a prior `base_revision`. Returns a deterministic verification report with canonical `status`, `summary`, `next_action`, and compact entry lines.
243
264
 
244
265
  | Parameter | Type | Required | Description |
245
266
  |-----------|------|----------|-------------|
@@ -251,13 +272,16 @@ Example output:
251
272
 
252
273
  ```text
253
274
  status: STALE
275
+ reason: checksums_stale
254
276
  revision: rev-17-deadbeef
255
277
  file: 1-120:abc123ef
256
278
  summary: valid=0 stale=1 invalid=0
279
+ next_action: reread_ranges
257
280
  base_revision: rev-16-feedcafe
258
281
  changed_ranges: 10-12(replace)
282
+ suggested_read_call: {"tool":"mcp__hex-line__read_file","arguments":{"path":"/repo/file.ts","ranges":["10-12"]}}
259
283
 
260
- STALE 10-12 checksum: 10-12:oldc0de0 current=10-12:newc0de0
284
+ entry: 1/1 | status: STALE | span: 10-12 | checksum: 10-12:oldc0de0 | current_checksum: 10-12:newc0de0 | next_action: reread_range | summary: content changed since checksum capture
261
285
  ```
262
286
 
263
287
  ### inspect_path
@@ -279,15 +303,15 @@ Inspect a file or directory path without guessing which low-level tool to call f
279
303
 
280
304
  ## Hook
281
305
 
282
- The unified hook (`hook.mjs`) handles four events:
306
+ The unified hook (`hook.mjs`) handles three Claude hook events:
283
307
 
284
308
  ### PreToolUse: Tool Redirect
285
309
 
286
- Blocks built-in `Read`, `Edit`, `Write`, `Grep` on text files and redirects to hex-line equivalents. Binary files (images, PDFs, notebooks, archives, executables, fonts, media) are excluded.
310
+ Applies redirect-first steering to built-in `Read`, `Edit`, `Write`, and `Grep` on text files. Binary/media files (images, PDFs, notebooks, archives, executables, fonts, media) stay on built-in tools. `.claude/settings.json` and `.claude/settings.local.json` at project root or home are also allowed on built-in tools.
287
311
 
288
312
  ### PreToolUse: Bash Redirect + Dangerous Blocker
289
313
 
290
- Intercepts simple Bash commands (`cat`, `head`, `tail`, `ls`, `find`, `grep`, `sed -i`, etc.) and redirects to hex-line tools. Blocks dangerous commands (`rm -rf /`, `git push --force`, `git reset --hard`, `DROP TABLE`, `chmod 777`, `mkfs`, `dd`).
314
+ Intercepts simple Bash commands (`cat`, `head`, `tail`, `tree`, `find`, `stat`, `wc -l`, `grep`, `rg`, `sed -i`, etc.) and redirects covered cases to hex-line tools. `ls`/`dir` are redirected only for recursive listing. Dangerous commands (`rm -rf /`, `git push --force`, `git reset --hard`, `DROP TABLE`, `chmod 777`, `mkfs`, `dd`) are blocked.
291
315
 
292
316
  ### PostToolUse: RTK Output Filter
293
317
 
@@ -298,7 +322,11 @@ Triggers on `Bash` tool output exceeding 50 lines. Pipeline:
298
322
  3. **Deduplicate** -- collapses identical normalized lines with `(xN)` counts
299
323
  4. **Truncate** -- keeps first 15 + last 15 lines, omits the middle
300
324
 
301
- Configuration constants in `hook.mjs`:
325
+ ### SessionStart: Bootstrap Hint
326
+
327
+ Injects a compact startup reminder. If the `hex-line` output style is active, the hook emits only a minimal bootstrap hint plus `ToolSearch('+hex-line read edit')` fallback. Otherwise it injects the short preferred read/edit workflow directly, including the scope rule: use file paths for file tools and the current project root for repo-wide tools such as `bulk_replace`.
328
+
329
+ Hook policy constants in `lib/hook-policy.mjs`:
302
330
 
303
331
  | Constant | Default | Purpose |
304
332
  |----------|---------|--------|
@@ -308,7 +336,7 @@ Configuration constants in `hook.mjs`:
308
336
 
309
337
  ### SessionStart: Tool Preferences
310
338
 
311
- Injects a short operational workflow into agent context at session start: no `ToolSearch`, prefer `outline -> read_file -> edit_file -> verify`, and use targeted reads over full-file reads.
339
+ Injects a short operational workflow into agent context at session start. If schemas are not loaded yet, it includes the `ToolSearch('+hex-line read edit')` fallback. Primary flow stays `outline -> read_file -> edit_file -> verify`, with targeted reads over full-file reads.
312
340
 
313
341
  ## Architecture
314
342
 
@@ -318,6 +346,8 @@ hex-line-mcp/
318
346
  hook.mjs Unified hook (PreToolUse + PostToolUse + SessionStart)
319
347
  package.json
320
348
  lib/
349
+ hook-policy.mjs Shared hook policy: redirects, thresholds, danger patterns
350
+ setup.mjs Startup autosync for hook + output style
321
351
  read.mjs File reading with hash annotation
322
352
  edit.mjs Anchor-based edits, diff output
323
353
  search.mjs ripgrep wrapper with hash-annotated results
@@ -397,7 +427,7 @@ The PostToolUse hook normalizes Bash output (replaces UUIDs, timestamps, IPs wit
397
427
  <details>
398
428
  <summary><b>Can I disable the built-in tool blocking?</b></summary>
399
429
 
400
- Yes. Remove the PreToolUse hook from `.claude/settings.local.json`. The MCP tools will still work, but agents will be free to use built-in Read/Edit/Write/Grep alongside hex-line tools.
430
+ Yes. To downgrade redirects to advice, set `.hex-skills/environment_state.json` to `{ "hooks": { "mode": "advisory" } }`. To remove the hook entirely, delete the `hex-line` hook entries from `~/.claude/settings.json`. To disable the MCP server for one project, add `hex-line` to `~/.claude.json -> projects.{cwd}.disabledMcpServers`.
401
431
 
402
432
  </details>
403
433
 
package/dist/hook.mjs CHANGED
@@ -54,10 +54,12 @@ function normalizeOutput(text, opts = {}) {
54
54
  }
55
55
 
56
56
  // hook.mjs
57
- import { readFileSync, statSync, writeSync } from "node:fs";
58
- import { resolve } from "node:path";
57
+ import { readFileSync, writeSync } from "node:fs";
58
+ import { resolve as resolve2 } from "node:path";
59
59
  import { homedir } from "node:os";
60
- import { fileURLToPath } from "node:url";
60
+
61
+ // lib/hook-policy.mjs
62
+ import { resolve } from "node:path";
61
63
  var BINARY_EXT = /* @__PURE__ */ new Set([
62
64
  ".png",
63
65
  ".jpg",
@@ -98,7 +100,9 @@ var OUTLINEABLE_EXT = /* @__PURE__ */ new Set([
98
100
  ".tsx",
99
101
  ".py",
100
102
  ".cs",
101
- ".php"
103
+ ".php",
104
+ ".md",
105
+ ".mdx"
102
106
  ]);
103
107
  var REVERSE_TOOL_HINTS = {
104
108
  "mcp__hex-line__read_file": "Read (file_path, offset, limit)",
@@ -109,7 +113,7 @@ var REVERSE_TOOL_HINTS = {
109
113
  "mcp__hex-line__outline": "Read with offset/limit",
110
114
  "mcp__hex-line__verify": "Read (re-read file to check freshness)",
111
115
  "mcp__hex-line__changes": "Bash(git diff)",
112
- "mcp__hex-line__bulk_replace": "Edit (text rename/refactor across files)"
116
+ "mcp__hex-line__bulk_replace": "Edit (text rename/refactor across files inside an explicit root path)"
113
117
  };
114
118
  var TOOL_HINTS = {
115
119
  Read: "mcp__hex-line__read_file (not Read). For writing: write_file (no prior Read needed)",
@@ -122,12 +126,12 @@ var TOOL_HINTS = {
122
126
  ls: "mcp__hex-line__inspect_path for tree or pattern search (not ls/find/tree). E.g. pattern='*-mcp' type='dir'",
123
127
  stat: "mcp__hex-line__inspect_path for compact file metadata (not stat/wc/file)",
124
128
  grep: "mcp__hex-line__grep_search (not grep/rg). Params: output, literal, context_before, context_after, multiline",
125
- sed: "mcp__hex-line__edit_file for hash edits, or mcp__hex-line__bulk_replace for text rename (not sed -i)",
129
+ sed: "mcp__hex-line__edit_file for hash edits, or mcp__hex-line__bulk_replace with path=<project root> for text rename (not sed -i)",
126
130
  diff: "mcp__hex-line__changes (not diff). Git diff with change symbols",
127
131
  outline: "mcp__hex-line__outline (before reading large code files)",
128
132
  verify: "mcp__hex-line__verify (staleness / revision check without re-read)",
129
133
  changes: "mcp__hex-line__changes (git diff with change symbols)",
130
- bulk: "mcp__hex-line__bulk_replace (multi-file search-replace)"
134
+ bulk: "mcp__hex-line__bulk_replace with path=<project root> (multi-file search-replace)"
131
135
  };
132
136
  var DEFERRED_HINT = "If schemas not loaded: ToolSearch('+hex-line read edit')";
133
137
  var BASH_REDIRECTS = [
@@ -136,9 +140,7 @@ var BASH_REDIRECTS = [
136
140
  { regex: /^tail\s+(?!-[fF])/, key: "tail" },
137
141
  { regex: /^(less|more)\s+/, key: "cat" },
138
142
  { regex: /^ls\s+-\S*R(\s|$)/, key: "ls" },
139
- // ls -R, ls -laR (recursive only)
140
143
  { regex: /^dir\s+\/[sS](\s|$)/, key: "ls" },
141
- // dir /s, dir /S (recursive only)
142
144
  { regex: /^tree\s+/, key: "ls" },
143
145
  { regex: /^find\s+/, key: "ls" },
144
146
  { regex: /^(stat|wc)\s+/, key: "stat" },
@@ -152,34 +154,13 @@ var TOOL_REDIRECT_MAP = {
152
154
  Grep: "Grep"
153
155
  };
154
156
  var DANGEROUS_PATTERNS = [
155
- {
156
- regex: /rm\s+(-[rf]+\s+)*[/~]/,
157
- reason: "rm -rf on root/home directory"
158
- },
159
- {
160
- regex: /git\s+push\s+(-f|--force)/,
161
- reason: "force push can overwrite remote history"
162
- },
163
- {
164
- regex: /git\s+reset\s+--hard/,
165
- reason: "hard reset discards uncommitted changes"
166
- },
167
- {
168
- regex: /DROP\s+(TABLE|DATABASE)/i,
169
- reason: "DROP destroys data permanently"
170
- },
171
- {
172
- regex: /chmod\s+777/,
173
- reason: "chmod 777 removes all access restrictions"
174
- },
175
- {
176
- regex: /mkfs/,
177
- reason: "filesystem format destroys all data"
178
- },
179
- {
180
- regex: /dd\s+if=\/dev\/zero/,
181
- reason: "direct disk write destroys data"
182
- }
157
+ { regex: /rm\s+(-[rf]+\s+)*[/~]/, reason: "rm -rf on root/home directory" },
158
+ { regex: /git\s+push\s+(-f|--force)/, reason: "force push can overwrite remote history" },
159
+ { regex: /git\s+reset\s+--hard/, reason: "hard reset discards uncommitted changes" },
160
+ { regex: /DROP\s+(TABLE|DATABASE)/i, reason: "DROP destroys data permanently" },
161
+ { regex: /chmod\s+777/, reason: "chmod 777 removes all access restrictions" },
162
+ { regex: /mkfs/, reason: "filesystem format destroys all data" },
163
+ { regex: /dd\s+if=\/dev\/zero/, reason: "direct disk write destroys data" }
183
164
  ];
184
165
  var COMPOUND_OPERATORS = /[|]|>>?|&&|\|\||;/;
185
166
  var CMD_PATTERNS = [
@@ -189,11 +170,24 @@ var CMD_PATTERNS = [
189
170
  [/pip install/i, "pip-install"],
190
171
  [/git (log|diff|status)/i, "git"]
191
172
  ];
192
- var LINE_THRESHOLD = 50;
193
- var HEAD_LINES = 15;
194
- var TAIL_LINES = 15;
195
- var LARGE_FILE_BYTES = 5 * 1024;
196
- var LARGE_EDIT_CHARS = 1200;
173
+ var HOOK_OUTPUT_POLICY = {
174
+ lineThreshold: 50,
175
+ headLines: 15,
176
+ tailLines: 15
177
+ };
178
+ function buildAllowedClaudeSettingsPaths(cwd, home) {
179
+ const cwdNorm = cwd.replace(/\\/g, "/");
180
+ const homeNorm = home.replace(/\\/g, "/");
181
+ return [
182
+ resolve(cwdNorm, ".claude/settings.json"),
183
+ resolve(cwdNorm, ".claude/settings.local.json"),
184
+ resolve(homeNorm, ".claude/settings.json"),
185
+ resolve(homeNorm, ".claude/settings.local.json")
186
+ ].map((entry) => entry.replace(/\\/g, "/").toLowerCase());
187
+ }
188
+
189
+ // hook.mjs
190
+ import { fileURLToPath } from "node:url";
197
191
  function extOf(filePath) {
198
192
  const dot = filePath.lastIndexOf(".");
199
193
  return dot !== -1 ? filePath.slice(dot).toLowerCase() : "";
@@ -203,16 +197,8 @@ function getFilePath(toolInput) {
203
197
  }
204
198
  function resolveToolPath(filePath) {
205
199
  if (!filePath) return "";
206
- if (filePath.startsWith("~/")) return resolve(homedir(), filePath.slice(2));
207
- return resolve(process.cwd(), filePath);
208
- }
209
- function getFileSize(filePath) {
210
- if (!filePath) return null;
211
- try {
212
- return statSync(resolveToolPath(filePath)).size;
213
- } catch {
214
- return null;
215
- }
200
+ if (filePath.startsWith("~/")) return resolve2(homedir(), filePath.slice(2));
201
+ return resolve2(process.cwd(), filePath);
216
202
  }
217
203
  function isPartialRead(toolInput) {
218
204
  return [toolInput.offset, toolInput.limit, toolInput.start_line, toolInput.end_line, toolInput.ranges].some((value) => value !== void 0 && value !== null && value !== "");
@@ -238,7 +224,7 @@ function isHexLineDisabled(configPath) {
238
224
  if (_hexLineDisabled !== null) return _hexLineDisabled;
239
225
  _hexLineDisabled = false;
240
226
  try {
241
- const p = configPath || resolve(homedir(), ".claude.json");
227
+ const p = configPath || resolve2(homedir(), ".claude.json");
242
228
  const claudeJson = JSON.parse(readFileSync(p, "utf-8"));
243
229
  const projects = claudeJson.projects;
244
230
  if (!projects || typeof projects !== "object") return _hexLineDisabled;
@@ -264,7 +250,7 @@ function getHookMode() {
264
250
  if (_hookMode !== void 0) return _hookMode;
265
251
  _hookMode = "blocking";
266
252
  try {
267
- const stateFile = resolve(process.cwd(), ".hex-skills/environment_state.json");
253
+ const stateFile = resolve2(process.cwd(), ".hex-skills/environment_state.json");
268
254
  const data = JSON.parse(readFileSync(stateFile, "utf-8"));
269
255
  if (data.hooks?.mode === "advisory") _hookMode = "advisory";
270
256
  } catch {
@@ -317,47 +303,26 @@ function handlePreToolUse(data) {
317
303
  const hintKey = TOOL_REDIRECT_MAP[toolName];
318
304
  if (hintKey) {
319
305
  const filePath = getFilePath(toolInput);
320
- const fileSize = getFileSize(filePath);
321
306
  if (BINARY_EXT.has(extOf(filePath))) {
322
307
  process.exit(0);
323
308
  }
324
309
  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())) {
310
+ const claudeAllow = buildAllowedClaudeSettingsPaths(process.cwd(), homedir());
311
+ if (claudeAllow.includes(resolvedNorm.toLowerCase())) {
334
312
  process.exit(0);
335
313
  }
336
314
  if (resolvedNorm.includes("/.claude/")) {
337
315
  redirect("Protected .claude/ path. Use built-in tools for .claude/ config files.");
338
316
  }
339
317
  if (toolName === "Read") {
340
- if (isPartialRead(toolInput)) {
341
- process.exit(0);
342
- }
343
- if (fileSize !== null && fileSize <= LARGE_FILE_BYTES) {
344
- const ext2 = filePath ? extOf(filePath) : "";
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);
347
- }
348
318
  const ext = filePath ? extOf(filePath) : "";
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);
319
+ const rangeHint = isPartialRead(toolInput) ? " Preserve the same offset/limit or ranges in read_file." : "";
320
+ 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.${rangeHint}` : filePath ? `Use mcp__hex-line__read_file(path="${filePath}") with ranges or offset/limit.${rangeHint}` : "Use mcp__hex-line__inspect_path or mcp__hex-line__read_file";
321
+ redirect(outlineHint, "Use hex-line for text-file reads to keep hashes, revision metadata, and graph hints in one flow.\n" + DEFERRED_HINT);
351
322
  }
352
323
  if (toolName === "Edit") {
353
- const oldText = String(toolInput.old_string || "");
354
- const isLargeEdit = Boolean(toolInput.replace_all) || oldText.length > LARGE_EDIT_CHARS || fileSize !== null && fileSize > LARGE_FILE_BYTES;
355
- if (!isLargeEdit) {
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);
358
- }
359
324
  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";
360
- redirect(target, "For large or repeated edits: locate anchors/checksums first, then call edit_file once with batched edits.\n" + DEFERRED_HINT);
325
+ redirect(target, "Use hash-verified edits for text files. Locate anchors/checksums first, then call edit_file once with batched edits.\n" + DEFERRED_HINT);
361
326
  }
362
327
  if (toolName === "Write") {
363
328
  const pathNote = filePath ? ` with path="${filePath}"` : "";
@@ -433,11 +398,14 @@ function handlePostToolUse(data) {
433
398
  }
434
399
  const lines = rawText.split("\n");
435
400
  const originalCount = lines.length;
436
- if (originalCount < LINE_THRESHOLD) {
401
+ if (originalCount < HOOK_OUTPUT_POLICY.lineThreshold) {
437
402
  process.exit(0);
438
403
  }
439
404
  const type = detectCommandType(command);
440
- const filtered = normalizeOutput(lines.join("\n"), { headLines: HEAD_LINES, tailLines: TAIL_LINES });
405
+ const filtered = normalizeOutput(lines.join("\n"), {
406
+ headLines: HOOK_OUTPUT_POLICY.headLines,
407
+ tailLines: HOOK_OUTPUT_POLICY.tailLines
408
+ });
441
409
  const filteredCount = filtered.split("\n").length;
442
410
  const header = `RTK FILTERED: ${type} (${originalCount} lines -> ${filteredCount} lines)`;
443
411
  const output = [
@@ -455,9 +423,9 @@ function handlePostToolUse(data) {
455
423
  }
456
424
  function handleSessionStart() {
457
425
  const settingsFiles = [
458
- resolve(process.cwd(), ".claude/settings.local.json"),
459
- resolve(process.cwd(), ".claude/settings.json"),
460
- resolve(homedir(), ".claude/settings.json")
426
+ resolve2(process.cwd(), ".claude/settings.local.json"),
427
+ resolve2(process.cwd(), ".claude/settings.json"),
428
+ resolve2(homedir(), ".claude/settings.json")
461
429
  ];
462
430
  let styleActive = false;
463
431
  for (const f of settingsFiles) {
@@ -471,8 +439,7 @@ function handleSessionStart() {
471
439
  } catch {
472
440
  }
473
441
  }
474
- const prefix = styleActive ? "Hex-line MCP available. Output style active.\n" : "Hex-line MCP available.\n";
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>";
442
+ const msg = styleActive ? "Hex-line MCP available. Output style active.\n<hex-line_instructions>\n <deferred_loading>If hex-line schemas not loaded, run: ToolSearch('+hex-line read edit')</deferred_loading>\n <note>Follow the active hex-line output style for primary tool choices.</note>\n <exceptions>Built-in tools stay OK for images, PDFs, notebooks, Glob, .claude/settings.json, and .claude/settings.local.json.</exceptions>\n</hex-line_instructions>" : "Hex-line MCP available.\n<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(path=&quot;&lt;project root&gt;&quot;) for text rename/refactor across files</path>\n </editing>\n <tips>\n <tip>Auto-fill path from the active file or project root. Do not leave repo scope implicit.</tip>\n <tip>Never invent range_checksum. Copy it from fresh read_file or grep_search blocks.</tip>\n <tip>Prefer set_line or insert_after for small local changes and replace_between for larger bounded rewrites.</tip>\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>Avoid large first-pass edit batches. Start with 1-2 hunks, then continue from the returned revision.</tip>\n <tip>Use write_file for new files (no prior Read needed).</tip>\n </tips>\n <exceptions>Built-in tools stay OK for images, PDFs, notebooks, Glob, .claude/settings.json, and .claude/settings.local.json.</exceptions>\n</hex-line_instructions>";
476
443
  safeExit(1, JSON.stringify({ systemMessage: msg }), 0);
477
444
  }
478
445
  var _norm = (p) => p.replace(/\\/g, "/");