@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 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 range checksums | Partial reads via `offset`/`limit` |
19
- | `edit_file` | Hash-verified anchor edits (set_line, replace_lines, insert_after) | Returns compact diff via `diff` package |
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 range checksums are still valid | Single-line response avoids full re-read |
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 and range checksums. Supports directory listing.
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
 
@@ -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 "./lib/normalize.mjs";
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 (anchor-based hash edits only)",
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": "Read the file again with Read",
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 hash-verified edits (not Edit). For text rename use bulk_replace",
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 "./hash.mjs";
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
- /** No aliases — canonical parameter names only. */
2
- export function coerceParams(params) { return params; }
1
+ export * from "@levnikolaevich/hex-common/runtime/coerce";