@levnikolaevich/hex-line-mcp 1.4.0 → 1.6.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
@@ -32,11 +32,11 @@ Advanced / occasional:
32
32
 
33
33
  | Tool | Description | Key Feature |
34
34
  |------|-------------|-------------|
35
- | `read_file` | Read file with hash-annotated lines, checksums, and revision | Partial reads via `offset`/`limit` |
35
+ | `read_file` | Read file with hash-annotated lines, checksums, and revision | Partial reads via `offset`/`limit` or `ranges`, compact output by default |
36
36
  | `edit_file` | Revision-aware anchor edits (`set_line`, `replace_lines`, `insert_after`, `replace_between`) | Batched same-file edits + conservative auto-rebase |
37
37
  | `write_file` | Create new file or overwrite, auto-creates parent dirs | Path validation, no hash overhead |
38
- | `grep_search` | Search with ripgrep, 3 output modes, per-group checksums | Edit-ready: grep -> edit directly with checksums |
39
- | `outline` | AST-based structural overview via tree-sitter WASM | 95% token reduction (10 lines instead of 500) |
38
+ | `grep_search` | Search with ripgrep, 3 output modes, per-group checksums | Plain `files`/`count`, compact edit-ready `content` |
39
+ | `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 |
40
40
  | `verify` | Check if held checksums / revision are still current | Staleness check without full re-read |
41
41
  | `directory_tree` | Compact directory tree with root .gitignore support | Skips node_modules/.git, shows file sizes |
42
42
  | `get_file_info` | File metadata without reading content | Size, lines, mtime, type, binary detection |
@@ -48,10 +48,10 @@ Advanced / occasional:
48
48
 
49
49
  | Event | Trigger | Action |
50
50
  |-------|---------|--------|
51
- | **PreToolUse** | Read/Edit/Write/Grep on text files | Blocks built-in, forces hex-line tools |
51
+ | **PreToolUse** | Read/Edit/Write/Grep on text files | Size-aware redirect: cheap small operations may pass, expensive ones are redirected |
52
52
  | **PreToolUse** | Bash with dangerous commands | Blocks `rm -rf /`, `git push --force`, etc. Agent must confirm with user |
53
53
  | **PostToolUse** | Bash with 50+ lines output | RTK: deduplicates, truncates, shows filtered summary to Claude as feedback |
54
- | **SessionStart** | Session begins | Injects full tool preference list into agent context |
54
+ | **SessionStart** | Session begins | Injects a short no-discovery workflow for hex-line tools |
55
55
 
56
56
 
57
57
  ### Bash Redirects
@@ -88,29 +88,24 @@ mcp__hex-line__setup_hooks(agent="claude")
88
88
 
89
89
  The `setup_hooks` tool automatically installs the output style to `~/.claude/output-styles/hex-line.md` and activates it if no other style is set. To activate manually: `/config` > Output style > hex-line.
90
90
 
91
- ## Benchmarking
91
+ ## Validation
92
92
 
93
- Two-tier benchmark architecture:
94
-
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)
93
+ Use the normal package checks:
97
94
 
98
95
  ```bash
99
- npm run benchmark -- --repo /path/to/repo
100
- npm run benchmark:diagnostic -- --repo /path/to/repo
96
+ npm test
97
+ npm run lint
98
+ npm run check
101
99
  ```
102
100
 
103
- Current hex-line workflow metrics on the `hex-line-mcp` repo (all real library calls):
101
+ Maintainers can also run the internal scenario harness when they want reproducible repo-local workflow regressions:
104
102
 
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 |
103
+ ```bash
104
+ npm run scenarios -- --repo /path/to/repo
105
+ npm run scenarios:diagnostic -- --repo /path/to/repo
106
+ ```
112
107
 
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.
108
+ Comparative built-in vs hex-line benchmarks are maintained outside this package.
114
109
 
115
110
  ### Optional Graph Enrichment
116
111
 
@@ -152,7 +147,7 @@ Use `bulk_replace` for text rename patterns across one or more files. Returns co
152
147
 
153
148
  ### read_file
154
149
 
155
- Read a file with FNV-1a hash-annotated lines, range checksums, file checksum, and revision. Supports directory listing.
150
+ 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.
156
151
 
157
152
  | Parameter | Type | Required | Description |
158
153
  |-----------|------|----------|-------------|
@@ -160,17 +155,24 @@ Read a file with FNV-1a hash-annotated lines, range checksums, file checksum, an
160
155
  | `paths` | string[] | no | Array of file paths to read (batch mode) |
161
156
  | `offset` | number | no | Start line, 1-indexed (default: 1) |
162
157
  | `limit` | number | no | Max lines to return (default: 2000, 0 = all) |
158
+ | `ranges` | array | no | Explicit line ranges, e.g. `[{ "start": 10, "end": 30 }]` |
159
+ | `include_graph` | boolean | no | Opt in to graph annotations when the graph index exists |
163
160
  | `plain` | boolean | no | Omit hashes, output `lineNum\|content` instead |
164
161
 
165
- Output format:
162
+ Default output is compact but block-structured:
166
163
 
167
164
  ```
165
+ File: lib/search.mjs
166
+ meta: 282 lines, 10.2KB, 2 hours ago
167
+ revision: rev-12-a1b2c3d4
168
+ file: 1-282:beefcafe
169
+
170
+ block: read_range
171
+ span: 1-3
168
172
  ab.1 import { resolve } from "node:path";
169
173
  cd.2 import { readFileSync } from "node:fs";
170
- ...
171
- checksum: 1-50:f7e2a1b0
172
- revision: rev-12-a1b2c3d4
173
- file: 1-120:beefcafe
174
+ ef.3 ...
175
+ checksum: 1-3:f7e2a1b0
174
176
  ```
175
177
 
176
178
  ### edit_file
@@ -203,6 +205,7 @@ Result footer includes:
203
205
  - `revision: ...`
204
206
  - `file: ...`
205
207
  - `changed_ranges: ...` when relevant
208
+ - `remapped_refs: ...` when stale anchors were uniquely relocated
206
209
  - `retry_checksum: ...` on local conflicts
207
210
 
208
211
  ### write_file
@@ -216,7 +219,7 @@ Create a new file or overwrite an existing one. Creates parent directories autom
216
219
 
217
220
  ### grep_search
218
221
 
219
- Search file contents using ripgrep. Three output modes: `content` (hash-annotated with checksums), `files` (paths only), `count` (match counts).
222
+ Search file contents using ripgrep. Three output modes: `content` (canonical `search_hunk` blocks), `files` (plain path list), `count` (plain `file:count` list).
220
223
 
221
224
  | Parameter | Type | Required | Description |
222
225
  |-----------|------|----------|-------------|
@@ -234,13 +237,13 @@ Search file contents using ripgrep. Three output modes: `content` (hash-annotate
234
237
  | `context_after` | number | no | Context lines AFTER match (`-A`) |
235
238
  | `limit` | number | no | Max matches per file (default: 100) |
236
239
  | `total_limit` | number | no | Total match events across all files; multiline matches count as 1 (0 = unlimited) |
237
- | `plain` | boolean | no | Omit hash tags, return `file:line:content` |
240
+ | `plain` | boolean | no | Omit hash tags inside block entries, return `lineNum\|content` |
238
241
 
239
- **Content mode** returns per-group checksums enabling direct `replace_lines` from grep results without intermediate `read_file`.
242
+ `content` mode returns canonical `search_hunk` blocks with per-hunk checksums enabling direct `replace_lines` from grep results without intermediate `read_file`.
240
243
 
241
244
  ### outline
242
245
 
243
- AST-based structural outline: functions, classes, interfaces with line ranges.
246
+ 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`.
244
247
 
245
248
  | Parameter | Type | Required | Description |
246
249
  |-----------|------|----------|-------------|
@@ -248,19 +251,30 @@ AST-based structural outline: functions, classes, interfaces with line ranges.
248
251
 
249
252
  Supported languages: JavaScript, TypeScript (JSX/TSX), Python, Go, Rust, Java, C, C++, C#, Ruby, PHP, Kotlin, Swift, Bash -- 15+ via tree-sitter WASM.
250
253
 
251
- Not for `.md`, `.json`, `.yaml`, `.txt` -- use `read_file` directly for those.
254
+ Not for `.json`, `.yaml`, `.txt` -- use `read_file` directly for those.
252
255
 
253
256
  ### verify
254
257
 
255
- Check if range checksums from a prior read are still valid, optionally relative to a prior `base_revision`.
258
+ 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.
256
259
 
257
260
  | Parameter | Type | Required | Description |
258
261
  |-----------|------|----------|-------------|
259
262
  | `path` | string | yes | File path |
260
- | `checksums` | string | yes | JSON array of checksum strings, e.g. `["1-50:f7e2a1b0"]` |
263
+ | `checksums` | string[] | yes | Array of checksum strings, e.g. `["1-50:f7e2a1b0"]` |
261
264
  | `base_revision` | string | no | Prior revision to compare against latest state |
262
265
 
263
- Returns a single-line confirmation or lists changed ranges.
266
+ Example output:
267
+
268
+ ```text
269
+ status: STALE
270
+ revision: rev-17-deadbeef
271
+ file: 1-120:abc123ef
272
+ summary: valid=0 stale=1 invalid=0
273
+ base_revision: rev-16-feedcafe
274
+ changed_ranges: 10-12(replace)
275
+
276
+ STALE 10-12 checksum: 10-12:oldc0de0 current=10-12:newc0de0
277
+ ```
264
278
 
265
279
  ### directory_tree
266
280
 
@@ -318,7 +332,7 @@ Configuration constants in `hook.mjs`:
318
332
 
319
333
  ### SessionStart: Tool Preferences
320
334
 
321
- Injects hex-line tool preference list into agent context at session start.
335
+ 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.
322
336
 
323
337
  ## Architecture
324
338
 
@@ -392,7 +406,7 @@ The edit is rejected with an error showing which lines changed since the last re
392
406
  <details>
393
407
  <summary><b>Is outline available for all file types?</b></summary>
394
408
 
395
- Outline works on code files only (15+ languages via tree-sitter WASM). For markdown, JSON, YAML, and text files use `read_file` directly -- these formats don't benefit from structural outline.
409
+ 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`.
396
410
 
397
411
  </details>
398
412
 
package/dist/hook.mjs CHANGED
@@ -54,7 +54,7 @@ function normalizeOutput(text, opts = {}) {
54
54
  }
55
55
 
56
56
  // hook.mjs
57
- import { readFileSync } from "node:fs";
57
+ import { readFileSync, statSync } from "node:fs";
58
58
  import { resolve } from "node:path";
59
59
  import { homedir } from "node:os";
60
60
  import { fileURLToPath } from "node:url";
@@ -183,10 +183,31 @@ var CMD_PATTERNS = [
183
183
  var LINE_THRESHOLD = 50;
184
184
  var HEAD_LINES = 15;
185
185
  var TAIL_LINES = 15;
186
+ var LARGE_FILE_BYTES = 15 * 1024;
187
+ var LARGE_EDIT_CHARS = 1200;
186
188
  function extOf(filePath) {
187
189
  const dot = filePath.lastIndexOf(".");
188
190
  return dot !== -1 ? filePath.slice(dot).toLowerCase() : "";
189
191
  }
192
+ function getFilePath(toolInput) {
193
+ return toolInput.file_path || toolInput.path || "";
194
+ }
195
+ function resolveToolPath(filePath) {
196
+ if (!filePath) return "";
197
+ if (filePath.startsWith("~/")) return resolve(homedir(), filePath.slice(2));
198
+ return resolve(process.cwd(), filePath);
199
+ }
200
+ function getFileSize(filePath) {
201
+ if (!filePath) return null;
202
+ try {
203
+ return statSync(resolveToolPath(filePath)).size;
204
+ } catch {
205
+ return null;
206
+ }
207
+ }
208
+ function isPartialRead(toolInput) {
209
+ return [toolInput.offset, toolInput.limit, toolInput.start_line, toolInput.end_line, toolInput.ranges].some((value) => value !== void 0 && value !== null && value !== "");
210
+ }
190
211
  function detectCommandType(cmd) {
191
212
  for (const [re, type] of CMD_PATTERNS) {
192
213
  if (re.test(cmd)) return type;
@@ -241,6 +262,16 @@ function block(reason, context) {
241
262
  process.stdout.write(JSON.stringify(output));
242
263
  process.exit(2);
243
264
  }
265
+ function advise(reason) {
266
+ process.stdout.write(JSON.stringify({
267
+ hookSpecificOutput: {
268
+ hookEventName: "PreToolUse",
269
+ permissionDecision: "approve",
270
+ permissionDecisionReason: reason
271
+ }
272
+ }));
273
+ process.exit(0);
274
+ }
244
275
  function handlePreToolUse(data) {
245
276
  const toolName = data.tool_name || "";
246
277
  const toolInput = data.tool_input || {};
@@ -249,7 +280,8 @@ function handlePreToolUse(data) {
249
280
  }
250
281
  const hintKey = TOOL_REDIRECT_MAP[toolName];
251
282
  if (hintKey) {
252
- const filePath = toolInput.file_path || toolInput.path || "";
283
+ const filePath = getFilePath(toolInput);
284
+ const fileSize = getFileSize(filePath);
253
285
  if (BINARY_EXT.has(extOf(filePath))) {
254
286
  process.exit(0);
255
287
  }
@@ -272,10 +304,33 @@ function handlePreToolUse(data) {
272
304
  process.exit(0);
273
305
  }
274
306
  }
275
- const hint = TOOL_HINTS[hintKey];
276
- const toolName2 = hint.split(" (")[0];
277
- const pathNote = filePath ? ` with path="${filePath}"` : "";
278
- block(`Use ${toolName2}${pathNote}`, hint);
307
+ if (toolName === "Read") {
308
+ if (isPartialRead(toolInput)) {
309
+ process.exit(0);
310
+ }
311
+ if (fileSize !== null && fileSize <= LARGE_FILE_BYTES) {
312
+ advise("hex-line read_file returns hash-annotated lines for verified edit workflow. For code files, outline gives a compact structural map first.");
313
+ }
314
+ const target = filePath ? `Use mcp__hex-line__outline or mcp__hex-line__read_file with path="${filePath}"` : "Use mcp__hex-line__directory_tree or mcp__hex-line__read_file";
315
+ block(target, "For large or unknown full reads: call outline first, then read_file with offset/limit or ranges. Do not use built-in Read here.");
316
+ }
317
+ if (toolName === "Edit") {
318
+ const oldText = String(toolInput.old_string || "");
319
+ const isLargeEdit = Boolean(toolInput.replace_all) || oldText.length > LARGE_EDIT_CHARS || fileSize !== null && fileSize > LARGE_FILE_BYTES;
320
+ if (!isLargeEdit) {
321
+ process.exit(0);
322
+ }
323
+ const target = filePath ? `Use mcp__hex-line__grep_search or mcp__hex-line__read_file, then mcp__hex-line__edit_file with path="${filePath}"` : "Use mcp__hex-line__grep_search or mcp__hex-line__read_file, then mcp__hex-line__edit_file";
324
+ block(target, "For large or repeated edits: locate anchors/checksums first, then call edit_file once with batched edits.");
325
+ }
326
+ if (toolName === "Write") {
327
+ const pathNote = filePath ? ` with path="${filePath}"` : "";
328
+ block(`Use mcp__hex-line__write_file${pathNote}`, TOOL_HINTS.Write);
329
+ }
330
+ if (toolName === "Grep") {
331
+ const pathNote = filePath ? ` with path="${filePath}"` : "";
332
+ block(`Use mcp__hex-line__grep_search${pathNote}`, TOOL_HINTS.Grep);
333
+ }
279
334
  }
280
335
  if (toolName === "Bash") {
281
336
  const command = (toolInput.command || "").trim();
@@ -381,22 +436,8 @@ function handleSessionStart() {
381
436
  } catch {
382
437
  }
383
438
  }
384
- if (styleActive) {
385
- process.stdout.write(JSON.stringify({ systemMessage: "hex-line Output Style active." }));
386
- process.exit(0);
387
- }
388
- const seen = /* @__PURE__ */ new Set();
389
- const lines = [];
390
- for (const hint of Object.values(TOOL_HINTS)) {
391
- const tool = hint.split(" ")[0];
392
- if (!seen.has(tool)) {
393
- seen.add(tool);
394
- lines.push(`- ${hint}`);
395
- }
396
- }
397
- lines.push("Exceptions: images, PDFs, notebooks, .claude/settings.json, .claude/settings.local.json \u2192 built-in Read; Glob always OK");
398
- lines.push("Bash OK for: npm/node/git/docker/curl, pipes, scripts");
399
- const msg = "Hex-line MCP available. Workflow:\n- Discovery: read_file, grep_search, outline, directory_tree\n- Same-file edits: prefer ONE edit_file call per file, carry revision/base_revision\n- Hash edits: edit_file (set_line, replace_lines, insert_after, replace_between)\n- Large rewrites: replace_between instead of reciting old blocks\n- Text rename: bulk_replace (multi-file search-replace)\n- Verify staleness: verify before considering reread\n- Write new: write_file\n" + lines.join("\n");
439
+ const prefix = styleActive ? "Hex-line MCP available. Output style active.\n" : "Hex-line MCP available.\n";
440
+ 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.";
400
441
  process.stdout.write(JSON.stringify({ systemMessage: msg }));
401
442
  process.exit(0);
402
443
  }