@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 +51 -37
- package/dist/hook.mjs +63 -22
- package/dist/server.mjs +895 -442
- package/output-style.md +23 -21
- package/package.json +4 -4
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 |
|
|
39
|
-
| `outline` | AST-based structural overview via tree-sitter WASM | 95% token reduction
|
|
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 |
|
|
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
|
|
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
|
-
##
|
|
91
|
+
## Validation
|
|
92
92
|
|
|
93
|
-
|
|
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
|
|
100
|
-
npm run
|
|
96
|
+
npm test
|
|
97
|
+
npm run lint
|
|
98
|
+
npm run check
|
|
101
99
|
```
|
|
102
100
|
|
|
103
|
-
|
|
101
|
+
Maintainers can also run the internal scenario harness when they want reproducible repo-local workflow regressions:
|
|
104
102
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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-
|
|
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` (
|
|
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 `
|
|
240
|
+
| `plain` | boolean | no | Omit hash tags inside block entries, return `lineNum\|content` |
|
|
238
241
|
|
|
239
|
-
|
|
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
|
|
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 `.
|
|
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
|
|
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 |
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
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
|
-
|
|
385
|
-
|
|
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
|
}
|