@levnikolaevich/hex-line-mcp 1.11.0 → 1.12.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
@@ -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
@@ -70,6 +70,8 @@ Hooks and output style are auto-synced on every MCP server startup. The server c
70
70
 
71
71
  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
72
 
73
+ 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.
74
+
73
75
  ## Validation
74
76
 
75
77
  Use the normal package checks:
@@ -94,6 +96,8 @@ Comparative built-in vs hex-line benchmarks are maintained outside this package.
94
96
  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
97
 
96
98
  - Graph enrichment is optional. If `.hex-skills/codegraph/index.db` is missing, stale, or unreadable, `hex-line` falls back to standard behavior silently.
99
+ - Graph enrichment is project-deterministic. `hex-line` only uses the graph database that belongs to the resolved current project scope.
100
+ - Nested projects do not inherit graph hints from a parent repo index once a nested project boundary is detected.
97
101
  - `better-sqlite3` is optional. If it is unavailable, `hex-line` still works without graph hints.
98
102
  - `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
103
  - `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 +132,7 @@ Use `replace_between` inside `edit_file` when you know stable start/end anchors
128
132
 
129
133
  ### Literal rename / refactor
130
134
 
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.
135
+ 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
136
 
133
137
  ### read_file
134
138
 
@@ -183,14 +187,29 @@ Edit operations (JSON array):
183
187
  ]
184
188
  ```
185
189
 
190
+ Discipline:
191
+
192
+ - Never invent `range_checksum`. Copy it from `read_file` or `grep_search(output:"content")`.
193
+ - First mutation in a file: prefer `grep_search` for narrow targets, or `outline -> read_file(ranges)` for structural edits.
194
+ - Prefer 1-2 hunks on the first pass. Once `edit_file` returns a fresh `revision`, continue from that state.
195
+
186
196
  Result footer includes:
187
197
 
188
198
  - `status: OK | AUTO_REBASED | CONFLICT`
199
+ - `reason: ...` as the canonical machine-readable cause for the current status
189
200
  - `revision: ...`
190
201
  - `file: ...`
191
202
  - `changed_ranges: ...` when relevant
203
+ - `recovery_ranges: ...` with the narrowest recommended `read_file` ranges for retry
204
+ - `next_action: ...` as the canonical immediate choice: `apply_retry_edit`, `apply_retry_batch`, or `reread_then_retry`
192
205
  - `remapped_refs: ...` when stale anchors were uniquely relocated
193
- - `retry_checksum: ...` on local conflicts
206
+ - `retry_checksum: ...` on local conflicts, narrowed to the exact target range when possible
207
+ - `retry_edit: ...` when the server can synthesize a ready-to-retry edit skeleton from current local state
208
+ - `retry_edits: ...` on conservative batch conflicts when every conflicted edit can be retried directly
209
+ - `suggested_read_call: ...` when rereading is the safest next step
210
+ - `retry_plan: ...` with a compact machine-readable next-call plan
211
+ - `summary: ...` and `snippet: ...` instead of long prose blocks
212
+ - `edit_conflicts: N` on conservative multi-edit preflight conflicts
194
213
 
195
214
  ### write_file
196
215
 
@@ -239,7 +258,7 @@ Not for `.json`, `.yaml`, `.txt` -- use `read_file` directly for those.
239
258
 
240
259
  ### verify
241
260
 
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.
261
+ 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
262
 
244
263
  | Parameter | Type | Required | Description |
245
264
  |-----------|------|----------|-------------|
@@ -251,13 +270,16 @@ Example output:
251
270
 
252
271
  ```text
253
272
  status: STALE
273
+ reason: checksums_stale
254
274
  revision: rev-17-deadbeef
255
275
  file: 1-120:abc123ef
256
276
  summary: valid=0 stale=1 invalid=0
277
+ next_action: reread_ranges
257
278
  base_revision: rev-16-feedcafe
258
279
  changed_ranges: 10-12(replace)
280
+ suggested_read_call: {"tool":"mcp__hex-line__read_file","arguments":{"path":"/repo/file.ts","ranges":["10-12"]}}
259
281
 
260
- STALE 10-12 checksum: 10-12:oldc0de0 current=10-12:newc0de0
282
+ 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
283
  ```
262
284
 
263
285
  ### inspect_path
@@ -279,15 +301,15 @@ Inspect a file or directory path without guessing which low-level tool to call f
279
301
 
280
302
  ## Hook
281
303
 
282
- The unified hook (`hook.mjs`) handles four events:
304
+ The unified hook (`hook.mjs`) handles three Claude hook events:
283
305
 
284
306
  ### PreToolUse: Tool Redirect
285
307
 
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.
308
+ 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
309
 
288
310
  ### PreToolUse: Bash Redirect + Dangerous Blocker
289
311
 
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`).
312
+ 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
313
 
292
314
  ### PostToolUse: RTK Output Filter
293
315
 
@@ -298,7 +320,11 @@ Triggers on `Bash` tool output exceeding 50 lines. Pipeline:
298
320
  3. **Deduplicate** -- collapses identical normalized lines with `(xN)` counts
299
321
  4. **Truncate** -- keeps first 15 + last 15 lines, omits the middle
300
322
 
301
- Configuration constants in `hook.mjs`:
323
+ ### SessionStart: Bootstrap Hint
324
+
325
+ 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`.
326
+
327
+ Hook policy constants in `lib/hook-policy.mjs`:
302
328
 
303
329
  | Constant | Default | Purpose |
304
330
  |----------|---------|--------|
@@ -308,7 +334,7 @@ Configuration constants in `hook.mjs`:
308
334
 
309
335
  ### SessionStart: Tool Preferences
310
336
 
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.
337
+ 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
338
 
313
339
  ## Architecture
314
340
 
@@ -318,6 +344,8 @@ hex-line-mcp/
318
344
  hook.mjs Unified hook (PreToolUse + PostToolUse + SessionStart)
319
345
  package.json
320
346
  lib/
347
+ hook-policy.mjs Shared hook policy: redirects, thresholds, danger patterns
348
+ setup.mjs Startup autosync for hook + output style
321
349
  read.mjs File reading with hash annotation
322
350
  edit.mjs Anchor-based edits, diff output
323
351
  search.mjs ripgrep wrapper with hash-annotated results
@@ -397,7 +425,7 @@ The PostToolUse hook normalizes Bash output (replaces UUIDs, timestamps, IPs wit
397
425
  <details>
398
426
  <summary><b>Can I disable the built-in tool blocking?</b></summary>
399
427
 
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.
428
+ 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
429
 
402
430
  </details>
403
431
 
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, "/");