@levnikolaevich/hex-line-mcp 1.3.2 → 1.3.3
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 +62 -7
- package/benchmark/workflows.mjs +92 -1
- package/hook.mjs +6 -6
- package/lib/benchmark-helpers.mjs +1 -1
- package/lib/coerce.mjs +1 -2
- package/lib/edit.mjs +255 -121
- package/lib/hash.mjs +1 -109
- package/lib/normalize.mjs +1 -106
- package/lib/outline.mjs +30 -86
- package/lib/read.mjs +6 -4
- package/lib/revisions.mjs +238 -0
- package/lib/search.mjs +6 -7
- package/lib/update-check.mjs +1 -56
- package/lib/verify.mjs +30 -15
- package/output-style.md +16 -2
- package/package.json +12 -6
- package/server.mjs +33 -39
package/README.md
CHANGED
|
@@ -13,14 +13,31 @@ Every line carries an FNV-1a content hash. Every edit must present those hashes
|
|
|
13
13
|
|
|
14
14
|
### 11 MCP Tools
|
|
15
15
|
|
|
16
|
+
Core day-to-day tools:
|
|
17
|
+
|
|
18
|
+
- `read_file`
|
|
19
|
+
- `edit_file`
|
|
20
|
+
- `grep_search`
|
|
21
|
+
- `outline`
|
|
22
|
+
- `verify`
|
|
23
|
+
- `bulk_replace`
|
|
24
|
+
|
|
25
|
+
Advanced / occasional:
|
|
26
|
+
|
|
27
|
+
- `write_file`
|
|
28
|
+
- `directory_tree`
|
|
29
|
+
- `get_file_info`
|
|
30
|
+
- `changes`
|
|
31
|
+
- `setup_hooks`
|
|
32
|
+
|
|
16
33
|
| Tool | Description | Key Feature |
|
|
17
34
|
|------|-------------|-------------|
|
|
18
|
-
| `read_file` | Read file with hash-annotated lines and
|
|
19
|
-
| `edit_file` |
|
|
35
|
+
| `read_file` | Read file with hash-annotated lines, checksums, and revision | Partial reads via `offset`/`limit` |
|
|
36
|
+
| `edit_file` | Revision-aware anchor edits (`set_line`, `replace_lines`, `insert_after`, `replace_between`) | Batched same-file edits + conservative auto-rebase |
|
|
20
37
|
| `write_file` | Create new file or overwrite, auto-creates parent dirs | Path validation, no hash overhead |
|
|
21
38
|
| `grep_search` | Search with ripgrep, 3 output modes, per-group checksums | Edit-ready: grep -> edit directly with checksums |
|
|
22
39
|
| `outline` | AST-based structural overview via tree-sitter WASM | 95% token reduction (10 lines instead of 500) |
|
|
23
|
-
| `verify` | Check if held
|
|
40
|
+
| `verify` | Check if held checksums / revision are still current | Staleness check without full re-read |
|
|
24
41
|
| `directory_tree` | Compact directory tree with root .gitignore support | Skips node_modules/.git, shows file sizes |
|
|
25
42
|
| `get_file_info` | File metadata without reading content | Size, lines, mtime, type, binary detection |
|
|
26
43
|
| `setup_hooks` | Configure Claude hooks + install output style | Gemini/Codex get guidance only; no hooks |
|
|
@@ -49,6 +66,8 @@ npm i -g @levnikolaevich/hex-line-mcp
|
|
|
49
66
|
claude mcp add -s user hex-line -- hex-line-mcp
|
|
50
67
|
```
|
|
51
68
|
|
|
69
|
+
ripgrep is bundled via `@vscode/ripgrep` — no manual install needed for `grep_search`.
|
|
70
|
+
|
|
52
71
|
### Hooks
|
|
53
72
|
|
|
54
73
|
Automatic setup (run once after MCP install):
|
|
@@ -121,9 +140,31 @@ If a project already has `.codegraph/index.db`, `hex-line` can add lightweight g
|
|
|
121
140
|
|
|
122
141
|
## Tools Reference
|
|
123
142
|
|
|
143
|
+
## Common Workflows
|
|
144
|
+
|
|
145
|
+
### Local code edit in one file
|
|
146
|
+
|
|
147
|
+
1. `outline` for large code files
|
|
148
|
+
2. `read_file` for the exact range you need
|
|
149
|
+
3. `edit_file` with all known hunks in one call
|
|
150
|
+
|
|
151
|
+
### Follow-up edit on the same file
|
|
152
|
+
|
|
153
|
+
1. Carry `revision` from the earlier `read_file` or `edit_file`
|
|
154
|
+
2. Pass it back as `base_revision`
|
|
155
|
+
3. Use `verify` before rereading the file
|
|
156
|
+
|
|
157
|
+
### Rewrite a long block
|
|
158
|
+
|
|
159
|
+
Use `replace_between` inside `edit_file` when you know stable start/end anchors and want to replace a large function, class, or config block without reciting the old body.
|
|
160
|
+
|
|
161
|
+
### Literal rename / refactor
|
|
162
|
+
|
|
163
|
+
Use `bulk_replace` for text rename patterns across one or more files. Do not use it as a substitute for structured block rewrites.
|
|
164
|
+
|
|
124
165
|
### read_file
|
|
125
166
|
|
|
126
|
-
Read a file with FNV-1a hash-annotated lines
|
|
167
|
+
Read a file with FNV-1a hash-annotated lines, range checksums, file checksum, and revision. Supports directory listing.
|
|
127
168
|
|
|
128
169
|
| Parameter | Type | Required | Description |
|
|
129
170
|
|-----------|------|----------|-------------|
|
|
@@ -140,11 +181,13 @@ ab.1 import { resolve } from "node:path";
|
|
|
140
181
|
cd.2 import { readFileSync } from "node:fs";
|
|
141
182
|
...
|
|
142
183
|
checksum: 1-50:f7e2a1b0
|
|
184
|
+
revision: rev-12-a1b2c3d4
|
|
185
|
+
file: 1-120:beefcafe
|
|
143
186
|
```
|
|
144
187
|
|
|
145
188
|
### edit_file
|
|
146
189
|
|
|
147
|
-
Edit using hash-verified anchors. For text rename use bulk_replace.
|
|
190
|
+
Edit using revision-aware hash-verified anchors. Prefer one batched call per file. For text rename use bulk_replace.
|
|
148
191
|
|
|
149
192
|
| Parameter | Type | Required | Description |
|
|
150
193
|
|-----------|------|----------|-------------|
|
|
@@ -152,17 +195,28 @@ Edit using hash-verified anchors. For text rename use bulk_replace.
|
|
|
152
195
|
| `edits` | string | yes | JSON array of edit operations (see below) |
|
|
153
196
|
| `dry_run` | boolean | no | Preview changes without writing |
|
|
154
197
|
| `restore_indent` | boolean | no | Auto-fix indentation to match anchor context (default: false) |
|
|
198
|
+
| `base_revision` | string | no | Prior revision from `read_file` / `edit_file` for same-file follow-up edits |
|
|
199
|
+
| `conflict_policy` | enum | no | `conservative` or `strict` (default: `conservative`) |
|
|
155
200
|
|
|
156
201
|
Edit operations (JSON array):
|
|
157
202
|
|
|
158
203
|
```json
|
|
159
204
|
[
|
|
160
205
|
{"set_line": {"anchor": "ab.12", "new_text": "replacement line"}},
|
|
161
|
-
{"replace_lines": {"start_anchor": "ab.10", "end_anchor": "cd.15", "new_text": "..."}},
|
|
206
|
+
{"replace_lines": {"start_anchor": "ab.10", "end_anchor": "cd.15", "new_text": "...", "range_checksum": "10-15:a1b2c3d4"}},
|
|
162
207
|
{"insert_after": {"anchor": "ab.20", "text": "inserted line"}},
|
|
208
|
+
{"replace_between": {"start_anchor": "ab.30", "end_anchor": "cd.80", "new_text": "...", "boundary_mode": "inclusive"}}
|
|
163
209
|
]
|
|
164
210
|
```
|
|
165
211
|
|
|
212
|
+
Result footer includes:
|
|
213
|
+
|
|
214
|
+
- `status: OK | AUTO_REBASED | CONFLICT`
|
|
215
|
+
- `revision: ...`
|
|
216
|
+
- `file: ...`
|
|
217
|
+
- `changed_ranges: ...` when relevant
|
|
218
|
+
- `retry_checksum: ...` on local conflicts
|
|
219
|
+
|
|
166
220
|
### write_file
|
|
167
221
|
|
|
168
222
|
Create a new file or overwrite an existing one. Creates parent directories automatically.
|
|
@@ -210,12 +264,13 @@ Not for `.md`, `.json`, `.yaml`, `.txt` -- use `read_file` directly for those.
|
|
|
210
264
|
|
|
211
265
|
### verify
|
|
212
266
|
|
|
213
|
-
Check if range checksums from a prior read are still valid
|
|
267
|
+
Check if range checksums from a prior read are still valid, optionally relative to a prior `base_revision`.
|
|
214
268
|
|
|
215
269
|
| Parameter | Type | Required | Description |
|
|
216
270
|
|-----------|------|----------|-------------|
|
|
217
271
|
| `path` | string | yes | File path |
|
|
218
272
|
| `checksums` | string | yes | JSON array of checksum strings, e.g. `["1-50:f7e2a1b0"]` |
|
|
273
|
+
| `base_revision` | string | no | Prior revision to compare against latest state |
|
|
219
274
|
|
|
220
275
|
Returns a single-line confirmation or lists changed ranges.
|
|
221
276
|
|
package/benchmark/workflows.mjs
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* - targeted edit inside a large smoke test
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import { copyFileSync, mkdirSync, rmSync, unlinkSync } from "node:fs";
|
|
12
|
+
import { copyFileSync, mkdirSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
|
|
13
13
|
import { resolve, dirname } from "node:path";
|
|
14
14
|
import { tmpdir } from "node:os";
|
|
15
15
|
import { fnv1a, lineTag, rangeChecksum } from "../lib/hash.mjs";
|
|
@@ -255,5 +255,96 @@ export async function runWorkflows(config) {
|
|
|
255
255
|
}
|
|
256
256
|
}
|
|
257
257
|
|
|
258
|
+
// W5: revision-aware follow-up edit after unrelated line shift
|
|
259
|
+
{
|
|
260
|
+
const tempPath = resolve(tmpdir(), `hex-line-wf5-${Date.now()}.mjs`);
|
|
261
|
+
const prefix = Array.from({ length: 80 }, (_, i) => `pre-${i}`);
|
|
262
|
+
const suffix = Array.from({ length: 80 }, (_, i) => `post-${i}`);
|
|
263
|
+
const sourceLines = [
|
|
264
|
+
...prefix,
|
|
265
|
+
"head1",
|
|
266
|
+
"head2",
|
|
267
|
+
"targetA",
|
|
268
|
+
"targetB",
|
|
269
|
+
"tail",
|
|
270
|
+
...suffix,
|
|
271
|
+
"",
|
|
272
|
+
];
|
|
273
|
+
const sourceText = sourceLines.join("\n");
|
|
274
|
+
mkdirSync(dirname(tempPath), { recursive: true });
|
|
275
|
+
writeFileSync(tempPath, sourceText, "utf-8");
|
|
276
|
+
|
|
277
|
+
const head1Idx = prefix.length;
|
|
278
|
+
const targetAIdx = prefix.length + 2;
|
|
279
|
+
const targetBIdx = prefix.length + 3;
|
|
280
|
+
const withInsert = [
|
|
281
|
+
...prefix,
|
|
282
|
+
"head1",
|
|
283
|
+
"inserted",
|
|
284
|
+
"head2",
|
|
285
|
+
"targetA",
|
|
286
|
+
"targetB",
|
|
287
|
+
"tail",
|
|
288
|
+
...suffix,
|
|
289
|
+
"",
|
|
290
|
+
];
|
|
291
|
+
const updatedLines = [
|
|
292
|
+
...prefix,
|
|
293
|
+
"head1",
|
|
294
|
+
"inserted",
|
|
295
|
+
"head2",
|
|
296
|
+
"targetA",
|
|
297
|
+
"updatedB",
|
|
298
|
+
"tail",
|
|
299
|
+
...suffix,
|
|
300
|
+
"",
|
|
301
|
+
];
|
|
302
|
+
|
|
303
|
+
const { value: without } = runN(() => {
|
|
304
|
+
let total = 0;
|
|
305
|
+
total += simBuiltInReadFull(tempPath, sourceLines).length;
|
|
306
|
+
total += simBuiltInEdit(tempPath, sourceLines, withInsert).length;
|
|
307
|
+
total += simBuiltInReadFull(tempPath, withInsert).length;
|
|
308
|
+
total += simBuiltInEdit(tempPath, withInsert, updatedLines).length;
|
|
309
|
+
return total;
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
const { value: withSL } = runN(() => {
|
|
313
|
+
let total = 0;
|
|
314
|
+
writeFileSync(tempPath, sourceText, "utf-8");
|
|
315
|
+
const baseRead = readFile(tempPath, { offset: head1Idx + 1, limit: 8 });
|
|
316
|
+
total += baseRead.length;
|
|
317
|
+
const baseRevision = baseRead.match(/revision: (\S+)/)?.[1];
|
|
318
|
+
const headTag = lineTag(fnv1a(sourceLines[head1Idx]));
|
|
319
|
+
total += editFile(tempPath, [{ insert_after: { anchor: `${headTag}.${head1Idx + 1}`, text: "inserted" } }]).length;
|
|
320
|
+
const startTag = lineTag(fnv1a(sourceLines[targetAIdx]));
|
|
321
|
+
const endTag = lineTag(fnv1a(sourceLines[targetBIdx]));
|
|
322
|
+
const rc = rangeChecksum(
|
|
323
|
+
[fnv1a(sourceLines[targetAIdx]), fnv1a(sourceLines[targetBIdx])],
|
|
324
|
+
targetAIdx + 1,
|
|
325
|
+
targetBIdx + 1,
|
|
326
|
+
);
|
|
327
|
+
total += editFile(tempPath, [{
|
|
328
|
+
replace_lines: {
|
|
329
|
+
start_anchor: `${startTag}.${targetAIdx + 1}`,
|
|
330
|
+
end_anchor: `${endTag}.${targetBIdx + 1}`,
|
|
331
|
+
new_text: "targetA\nupdatedB",
|
|
332
|
+
range_checksum: rc,
|
|
333
|
+
}
|
|
334
|
+
}], { baseRevision, conflictPolicy: "conservative" }).length;
|
|
335
|
+
return total;
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
workflowResults.push({
|
|
339
|
+
id: "W5",
|
|
340
|
+
scenario: "Follow-up edit after unrelated line shift",
|
|
341
|
+
without,
|
|
342
|
+
withSL,
|
|
343
|
+
opsWithout: 4,
|
|
344
|
+
opsWith: 3,
|
|
345
|
+
});
|
|
346
|
+
try { unlinkSync(tempPath); } catch {}
|
|
347
|
+
}
|
|
348
|
+
|
|
258
349
|
return workflowResults;
|
|
259
350
|
}
|
package/hook.mjs
CHANGED
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
* Exit 2 = block (PreToolUse) or stderr feedback (PostToolUse)
|
|
25
25
|
*/
|
|
26
26
|
|
|
27
|
-
import { normalizeOutput } from "
|
|
27
|
+
import { normalizeOutput } from "@levnikolaevich/hex-common/output/normalize";
|
|
28
28
|
import { readFileSync } from "node:fs";
|
|
29
29
|
import { resolve } from "node:path";
|
|
30
30
|
import { homedir } from "node:os";
|
|
@@ -42,13 +42,13 @@ const BINARY_EXT = new Set([
|
|
|
42
42
|
|
|
43
43
|
const REVERSE_TOOL_HINTS = {
|
|
44
44
|
"mcp__hex-line__read_file": "Read (file_path, offset, limit)",
|
|
45
|
-
"mcp__hex-line__edit_file": "Edit (
|
|
45
|
+
"mcp__hex-line__edit_file": "Edit (revision-aware hash edits, block rewrite, auto-rebase)",
|
|
46
46
|
"mcp__hex-line__write_file": "Write (file_path, content)",
|
|
47
47
|
"mcp__hex-line__grep_search": "Grep (pattern, path)",
|
|
48
48
|
"mcp__hex-line__directory_tree": "Glob (pattern) or Bash(ls)",
|
|
49
49
|
"mcp__hex-line__get_file_info": "Bash(stat/wc)",
|
|
50
50
|
"mcp__hex-line__outline": "Read with offset/limit",
|
|
51
|
-
"mcp__hex-line__verify": "
|
|
51
|
+
"mcp__hex-line__verify": "Verify held checksums / revision without reread",
|
|
52
52
|
"mcp__hex-line__changes": "Bash(git diff)",
|
|
53
53
|
"mcp__hex-line__bulk_replace": "Edit (text rename/refactor across files)",
|
|
54
54
|
"mcp__hex-line__setup_hooks": "Not available (hex-line disabled)",
|
|
@@ -56,7 +56,7 @@ const REVERSE_TOOL_HINTS = {
|
|
|
56
56
|
|
|
57
57
|
const TOOL_HINTS = {
|
|
58
58
|
Read: "mcp__hex-line__read_file (not Read). For writing: write_file (no prior Read needed)",
|
|
59
|
-
Edit: "mcp__hex-line__edit_file for
|
|
59
|
+
Edit: "mcp__hex-line__edit_file for revision-aware hash edits. Batch same-file hunks, carry base_revision, use replace_between for block rewrites",
|
|
60
60
|
Write: "mcp__hex-line__write_file (not Write). No prior Read needed",
|
|
61
61
|
Grep: "mcp__hex-line__grep_search (not Grep). Params: output, literal, context_before, context_after, multiline",
|
|
62
62
|
cat: "mcp__hex-line__read_file (not cat/head/tail/less/more)",
|
|
@@ -68,7 +68,7 @@ const TOOL_HINTS = {
|
|
|
68
68
|
sed: "mcp__hex-line__edit_file for hash edits, or mcp__hex-line__bulk_replace for text rename (not sed -i)",
|
|
69
69
|
diff: "mcp__hex-line__changes (not diff). Git-based semantic diff",
|
|
70
70
|
outline: "mcp__hex-line__outline (before reading large code files)",
|
|
71
|
-
verify: "mcp__hex-line__verify (staleness check without re-read)",
|
|
71
|
+
verify: "mcp__hex-line__verify (staleness / revision check without re-read)",
|
|
72
72
|
changes: "mcp__hex-line__changes (semantic AST diff)",
|
|
73
73
|
bulk: "mcp__hex-line__bulk_replace (multi-file search-replace)",
|
|
74
74
|
setup: "mcp__hex-line__setup_hooks (configure hooks for agents)",
|
|
@@ -425,7 +425,7 @@ function handleSessionStart() {
|
|
|
425
425
|
}
|
|
426
426
|
lines.push("Exceptions: images, PDFs, notebooks, .claude/settings.json, .claude/settings.local.json \u2192 built-in Read; Glob always OK");
|
|
427
427
|
lines.push("Bash OK for: npm/node/git/docker/curl, pipes, scripts");
|
|
428
|
-
const msg = "Hex-line MCP available. Workflow:\n- Discovery: read_file, grep_search, outline, directory_tree\n- Hash edits: edit_file (set_line, replace_lines, insert_after)\n- Text rename: bulk_replace (multi-file search-replace)\n- Write new: write_file\n" + lines.join("\n");
|
|
428
|
+
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");
|
|
429
429
|
process.stdout.write(JSON.stringify({ systemMessage: msg }));
|
|
430
430
|
process.exit(0);
|
|
431
431
|
}
|
|
@@ -2,7 +2,7 @@ import { readFileSync, statSync, readdirSync } from "node:fs";
|
|
|
2
2
|
import { execSync } from "node:child_process";
|
|
3
3
|
import { performance } from "node:perf_hooks";
|
|
4
4
|
import { resolve, extname, join } from "node:path";
|
|
5
|
-
import { fnv1a, lineTag } from "
|
|
5
|
+
import { fnv1a, lineTag } from "@levnikolaevich/hex-common/text-protocol/hash";
|
|
6
6
|
import { readFile } from "./read.mjs";
|
|
7
7
|
|
|
8
8
|
// ---------------------------------------------------------------------------
|
package/lib/coerce.mjs
CHANGED
|
@@ -1,2 +1 @@
|
|
|
1
|
-
|
|
2
|
-
export function coerceParams(params) { return params; }
|
|
1
|
+
export * from "@levnikolaevich/hex-common/runtime/coerce";
|