@levnikolaevich/hex-line-mcp 1.3.2 → 1.3.4

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
 
package/dist/hook.mjs ADDED
@@ -0,0 +1,428 @@
1
+ #!/usr/bin/env node
2
+
3
+ // ../hex-common/src/output/normalize.mjs
4
+ var NORM_RULES = [
5
+ [/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi, "<UUID>"],
6
+ [/\d{4}-\d{2}-\d{2}\s\d{2}:\d{2}:\d{2}/g, "<TS>"],
7
+ [/\d{2}-\d{2}-\d{4}\s\d{2}:\d{2}:\d{2}/g, "<TS>"],
8
+ [/\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}(:\d+)?/g, "<IP>"],
9
+ [/\/[0-9a-f]{8,}/gi, "/<ID>"],
10
+ [/\b\d{3,}(?=\b|[a-zA-Z])/g, "<N>"],
11
+ [/trace_id=[0-9a-fA-F]{1,8}/g, "trace_id=<TRACE>"]
12
+ ];
13
+ function normalizeLine(line) {
14
+ let result = line;
15
+ for (const [rx, repl] of NORM_RULES) {
16
+ result = result.replace(rx, repl);
17
+ }
18
+ return result;
19
+ }
20
+ function deduplicateLines(lines) {
21
+ const groups = /* @__PURE__ */ new Map();
22
+ const order = [];
23
+ for (const line of lines) {
24
+ const norm = normalizeLine(line);
25
+ if (groups.has(norm)) groups.get(norm).count++;
26
+ else {
27
+ groups.set(norm, { representative: line, count: 1 });
28
+ order.push(norm);
29
+ }
30
+ }
31
+ order.sort((a, b) => groups.get(b).count - groups.get(a).count);
32
+ return order.map((norm) => {
33
+ const { representative, count } = groups.get(norm);
34
+ return count > 1 ? `${representative} (x${count})` : representative;
35
+ });
36
+ }
37
+ function smartTruncate(text, headLines = 40, tailLines = 20) {
38
+ const lines = text.split("\n");
39
+ const total = lines.length;
40
+ const maxLines = headLines + tailLines;
41
+ if (total <= maxLines) return text;
42
+ const head = lines.slice(0, headLines);
43
+ const tail = lines.slice(total - tailLines);
44
+ const skipped = total - maxLines;
45
+ return [...head, `
46
+ --- ${skipped} lines omitted ---
47
+ `, ...tail].join("\n");
48
+ }
49
+ function normalizeOutput(text, opts = {}) {
50
+ const { deduplicate = true, headLines = 40, tailLines = 20 } = opts;
51
+ const lines = text.split("\n");
52
+ const processed = deduplicate ? deduplicateLines(lines) : lines;
53
+ return smartTruncate(processed.join("\n"), headLines, tailLines);
54
+ }
55
+
56
+ // hook.mjs
57
+ import { readFileSync } from "node:fs";
58
+ import { resolve } from "node:path";
59
+ import { homedir } from "node:os";
60
+ import { fileURLToPath } from "node:url";
61
+ var BINARY_EXT = /* @__PURE__ */ new Set([
62
+ ".png",
63
+ ".jpg",
64
+ ".jpeg",
65
+ ".gif",
66
+ ".bmp",
67
+ ".webp",
68
+ ".svg",
69
+ ".ico",
70
+ ".pdf",
71
+ ".ipynb",
72
+ ".zip",
73
+ ".tar",
74
+ ".gz",
75
+ ".7z",
76
+ ".rar",
77
+ ".exe",
78
+ ".dll",
79
+ ".so",
80
+ ".dylib",
81
+ ".wasm",
82
+ ".mp3",
83
+ ".mp4",
84
+ ".wav",
85
+ ".avi",
86
+ ".mkv",
87
+ ".ttf",
88
+ ".otf",
89
+ ".woff",
90
+ ".woff2"
91
+ ]);
92
+ var REVERSE_TOOL_HINTS = {
93
+ "mcp__hex-line__read_file": "Read (file_path, offset, limit)",
94
+ "mcp__hex-line__edit_file": "Edit (revision-aware hash edits, block rewrite, auto-rebase)",
95
+ "mcp__hex-line__write_file": "Write (file_path, content)",
96
+ "mcp__hex-line__grep_search": "Grep (pattern, path)",
97
+ "mcp__hex-line__directory_tree": "Glob (pattern) or Bash(ls)",
98
+ "mcp__hex-line__get_file_info": "Bash(stat/wc)",
99
+ "mcp__hex-line__outline": "Read with offset/limit",
100
+ "mcp__hex-line__verify": "Verify held checksums / revision without reread",
101
+ "mcp__hex-line__changes": "Bash(git diff)",
102
+ "mcp__hex-line__bulk_replace": "Edit (text rename/refactor across files)",
103
+ "mcp__hex-line__setup_hooks": "Not available (hex-line disabled)"
104
+ };
105
+ var TOOL_HINTS = {
106
+ Read: "mcp__hex-line__read_file (not Read). For writing: write_file (no prior Read needed)",
107
+ Edit: "mcp__hex-line__edit_file for revision-aware hash edits. Batch same-file hunks, carry base_revision, use replace_between for block rewrites",
108
+ Write: "mcp__hex-line__write_file (not Write). No prior Read needed",
109
+ Grep: "mcp__hex-line__grep_search (not Grep). Params: output, literal, context_before, context_after, multiline",
110
+ cat: "mcp__hex-line__read_file (not cat/head/tail/less/more)",
111
+ head: "mcp__hex-line__read_file with limit param (not head)",
112
+ tail: "mcp__hex-line__read_file with offset param (not tail)",
113
+ ls: "mcp__hex-line__directory_tree with pattern param (not ls/find/tree). E.g. pattern='*-mcp' type='dir'",
114
+ stat: "mcp__hex-line__get_file_info (not stat/wc/file)",
115
+ grep: "mcp__hex-line__grep_search (not grep/rg). Params: output, literal, context_before, context_after, multiline",
116
+ sed: "mcp__hex-line__edit_file for hash edits, or mcp__hex-line__bulk_replace for text rename (not sed -i)",
117
+ diff: "mcp__hex-line__changes (not diff). Git-based semantic diff",
118
+ outline: "mcp__hex-line__outline (before reading large code files)",
119
+ verify: "mcp__hex-line__verify (staleness / revision check without re-read)",
120
+ changes: "mcp__hex-line__changes (semantic AST diff)",
121
+ bulk: "mcp__hex-line__bulk_replace (multi-file search-replace)",
122
+ setup: "mcp__hex-line__setup_hooks (configure hooks for agents)"
123
+ };
124
+ var BASH_REDIRECTS = [
125
+ { regex: /^cat\s+\S+/, key: "cat" },
126
+ { regex: /^head\s+/, key: "head" },
127
+ { regex: /^tail\s+(?!-[fF])/, key: "tail" },
128
+ { regex: /^(less|more)\s+/, key: "cat" },
129
+ { regex: /^ls\s+-\S*R(\s|$)/, key: "ls" },
130
+ // ls -R, ls -laR (recursive only)
131
+ { regex: /^dir\s+\/[sS](\s|$)/, key: "ls" },
132
+ // dir /s, dir /S (recursive only)
133
+ { regex: /^tree\s+/, key: "ls" },
134
+ { regex: /^find\s+/, key: "ls" },
135
+ { regex: /^(stat|wc)\s+/, key: "stat" },
136
+ { regex: /^(grep|rg)\s+/, key: "grep" },
137
+ { regex: /^sed\s+-i/, key: "sed" }
138
+ ];
139
+ var TOOL_REDIRECT_MAP = {
140
+ Read: "Read",
141
+ Edit: "Edit",
142
+ Write: "Write",
143
+ Grep: "Grep"
144
+ };
145
+ var DANGEROUS_PATTERNS = [
146
+ {
147
+ regex: /rm\s+(-[rf]+\s+)*[/~]/,
148
+ reason: "rm -rf on root/home directory"
149
+ },
150
+ {
151
+ regex: /git\s+push\s+(-f|--force)/,
152
+ reason: "force push can overwrite remote history"
153
+ },
154
+ {
155
+ regex: /git\s+reset\s+--hard/,
156
+ reason: "hard reset discards uncommitted changes"
157
+ },
158
+ {
159
+ regex: /DROP\s+(TABLE|DATABASE)/i,
160
+ reason: "DROP destroys data permanently"
161
+ },
162
+ {
163
+ regex: /chmod\s+777/,
164
+ reason: "chmod 777 removes all access restrictions"
165
+ },
166
+ {
167
+ regex: /mkfs/,
168
+ reason: "filesystem format destroys all data"
169
+ },
170
+ {
171
+ regex: /dd\s+if=\/dev\/zero/,
172
+ reason: "direct disk write destroys data"
173
+ }
174
+ ];
175
+ var COMPOUND_OPERATORS = /[|]|>>?|&&|\|\||;/;
176
+ var CMD_PATTERNS = [
177
+ [/npm (install|ci|update|add)/i, "npm-install"],
178
+ [/npm test|jest|vitest|mocha|pytest|cargo test/i, "test"],
179
+ [/npm run build|tsc|webpack|vite build|cargo build/i, "build"],
180
+ [/pip install/i, "pip-install"],
181
+ [/git (log|diff|status)/i, "git"]
182
+ ];
183
+ var LINE_THRESHOLD = 50;
184
+ var HEAD_LINES = 15;
185
+ var TAIL_LINES = 15;
186
+ function extOf(filePath) {
187
+ const dot = filePath.lastIndexOf(".");
188
+ return dot !== -1 ? filePath.slice(dot).toLowerCase() : "";
189
+ }
190
+ function detectCommandType(cmd) {
191
+ for (const [re, type] of CMD_PATTERNS) {
192
+ if (re.test(cmd)) return type;
193
+ }
194
+ return "generic";
195
+ }
196
+ function extractBashText(response) {
197
+ if (typeof response === "string") return response;
198
+ if (response && typeof response === "object") {
199
+ const parts = [];
200
+ if (response.stdout) parts.push(response.stdout);
201
+ if (response.stderr) parts.push(response.stderr);
202
+ return parts.join("\n") || "";
203
+ }
204
+ return "";
205
+ }
206
+ var _hexLineDisabled = null;
207
+ function isHexLineDisabled(configPath) {
208
+ if (_hexLineDisabled !== null) return _hexLineDisabled;
209
+ _hexLineDisabled = false;
210
+ try {
211
+ const p = configPath || resolve(homedir(), ".claude.json");
212
+ const claudeJson = JSON.parse(readFileSync(p, "utf-8"));
213
+ const projects = claudeJson.projects;
214
+ if (!projects || typeof projects !== "object") return _hexLineDisabled;
215
+ const cwd = process.cwd().replace(/\\/g, "/").replace(/\/$/, "").toLowerCase();
216
+ for (const [path, config] of Object.entries(projects)) {
217
+ if (path.replace(/\\/g, "/").replace(/\/$/, "").toLowerCase() === cwd) {
218
+ const disabled = config.disabledMcpServers;
219
+ if (Array.isArray(disabled) && disabled.includes("hex-line")) {
220
+ _hexLineDisabled = true;
221
+ }
222
+ break;
223
+ }
224
+ }
225
+ } catch {
226
+ }
227
+ return _hexLineDisabled;
228
+ }
229
+ function _resetHexLineDisabledCache() {
230
+ _hexLineDisabled = null;
231
+ }
232
+ function block(reason, context) {
233
+ const output = {
234
+ hookSpecificOutput: {
235
+ hookEventName: "PreToolUse",
236
+ permissionDecision: "deny",
237
+ permissionDecisionReason: reason
238
+ }
239
+ };
240
+ if (context) output.hookSpecificOutput.additionalContext = context;
241
+ process.stdout.write(JSON.stringify(output));
242
+ process.exit(2);
243
+ }
244
+ function handlePreToolUse(data) {
245
+ const toolName = data.tool_name || "";
246
+ const toolInput = data.tool_input || {};
247
+ if (toolName.startsWith("mcp__hex-line__")) {
248
+ process.exit(0);
249
+ }
250
+ const hintKey = TOOL_REDIRECT_MAP[toolName];
251
+ if (hintKey) {
252
+ const filePath = toolInput.file_path || toolInput.path || "";
253
+ if (BINARY_EXT.has(extOf(filePath))) {
254
+ process.exit(0);
255
+ }
256
+ const normalPath = filePath.replace(/\\/g, "/");
257
+ if (normalPath.includes(".claude/plans/") || normalPath.includes("AppData")) {
258
+ process.exit(0);
259
+ }
260
+ const ALLOWED_CONFIGS = /* @__PURE__ */ new Set(["settings.json", "settings.local.json"]);
261
+ const fileName = normalPath.split("/").pop();
262
+ if (ALLOWED_CONFIGS.has(fileName)) {
263
+ let candidate = filePath;
264
+ if (candidate.startsWith("~/")) {
265
+ candidate = homedir().replace(/\\/g, "/") + candidate.slice(1);
266
+ }
267
+ const absPath = resolve(process.cwd(), candidate).replace(/\\/g, "/");
268
+ const projectClaude = resolve(process.cwd(), ".claude").replace(/\\/g, "/") + "/";
269
+ const globalClaude = resolve(homedir(), ".claude").replace(/\\/g, "/") + "/";
270
+ const cmp = process.platform === "win32" ? (a, b) => a.toLowerCase().startsWith(b.toLowerCase()) : (a, b) => a.startsWith(b);
271
+ if (cmp(absPath, projectClaude) || cmp(absPath, globalClaude)) {
272
+ process.exit(0);
273
+ }
274
+ }
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);
279
+ }
280
+ if (toolName === "Bash") {
281
+ const command = (toolInput.command || "").trim();
282
+ if (command.includes("# hex-confirmed")) {
283
+ process.exit(0);
284
+ }
285
+ const cmdCheck = command.replace(/<<['"]?(\w+)['"]?\s*\n[\s\S]*?\n\1/g, "");
286
+ for (const { regex, reason } of DANGEROUS_PATTERNS) {
287
+ if (regex.test(cmdCheck)) {
288
+ block(
289
+ `DANGEROUS: ${reason}. Ask user to confirm, then retry with: # hex-confirmed`,
290
+ `Original command: ${command.slice(0, 100)}`
291
+ );
292
+ }
293
+ }
294
+ if (COMPOUND_OPERATORS.test(command)) {
295
+ const firstCmd = command.split(/\s*[|;&>]\s*/)[0].trim();
296
+ for (const { regex, key } of BASH_REDIRECTS) {
297
+ if (regex.test(firstCmd)) {
298
+ const hint = TOOL_HINTS[key];
299
+ const toolName2 = hint.split(" (")[0];
300
+ block(`Use ${toolName2} instead of piped command`, hint);
301
+ }
302
+ }
303
+ process.exit(0);
304
+ }
305
+ for (const { regex, key } of BASH_REDIRECTS) {
306
+ if (regex.test(command)) {
307
+ const hint = TOOL_HINTS[key];
308
+ const toolName2 = hint.split(" (")[0];
309
+ const args = command.split(/\s+/).slice(1).join(" ");
310
+ const argsNote = args ? ` \u2014 args: "${args}"` : "";
311
+ block(`Use ${toolName2}${argsNote}`, hint);
312
+ }
313
+ }
314
+ }
315
+ process.exit(0);
316
+ }
317
+ function handlePreToolUseReverse(data) {
318
+ const toolName = data.tool_name || "";
319
+ if (toolName.startsWith("mcp__hex-line__")) {
320
+ const builtIn = REVERSE_TOOL_HINTS[toolName];
321
+ if (builtIn) {
322
+ const target = builtIn.split(" ")[0];
323
+ block(
324
+ `hex-line is disabled in this project. Use ${target}`,
325
+ `hex-line disabled. Use built-in: ${builtIn}`
326
+ );
327
+ }
328
+ block("hex-line is disabled in this project", "Disabled via project settings");
329
+ }
330
+ process.exit(0);
331
+ }
332
+ function handlePostToolUse(data) {
333
+ const toolName = data.tool_name || "";
334
+ if (toolName !== "Bash") {
335
+ process.exit(0);
336
+ }
337
+ const toolInput = data.tool_input || {};
338
+ const rawText = extractBashText(data.tool_response);
339
+ const command = toolInput.command || "";
340
+ if (!rawText) {
341
+ process.exit(0);
342
+ }
343
+ const lines = rawText.split("\n");
344
+ const originalCount = lines.length;
345
+ if (originalCount < LINE_THRESHOLD) {
346
+ process.exit(0);
347
+ }
348
+ const type = detectCommandType(command);
349
+ const filtered = normalizeOutput(lines.join("\n"), { headLines: HEAD_LINES, tailLines: TAIL_LINES });
350
+ const filteredCount = filtered.split("\n").length;
351
+ const header = `RTK FILTERED: ${type} (${originalCount} lines -> ${filteredCount} lines)`;
352
+ const output = [
353
+ "=".repeat(50),
354
+ header,
355
+ "=".repeat(50),
356
+ "",
357
+ filtered,
358
+ "",
359
+ "-".repeat(50),
360
+ `Original: ${originalCount} lines | Filtered: ${filteredCount} lines`,
361
+ "=".repeat(50)
362
+ ].join("\n");
363
+ process.stderr.write(output);
364
+ process.exit(2);
365
+ }
366
+ function handleSessionStart() {
367
+ const settingsFiles = [
368
+ resolve(process.cwd(), ".claude/settings.local.json"),
369
+ resolve(process.cwd(), ".claude/settings.json"),
370
+ resolve(homedir(), ".claude/settings.json")
371
+ ];
372
+ let styleActive = false;
373
+ for (const f of settingsFiles) {
374
+ try {
375
+ const config = JSON.parse(readFileSync(f, "utf-8"));
376
+ if (config.outputStyle === "hex-line") {
377
+ styleActive = true;
378
+ break;
379
+ }
380
+ if (config.outputStyle) break;
381
+ } catch {
382
+ }
383
+ }
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");
400
+ process.stdout.write(JSON.stringify({ systemMessage: msg }));
401
+ process.exit(0);
402
+ }
403
+ if (process.argv[1] === fileURLToPath(import.meta.url)) {
404
+ let input = "";
405
+ process.stdin.on("data", (chunk) => {
406
+ input += chunk;
407
+ });
408
+ process.stdin.on("end", () => {
409
+ try {
410
+ const data = JSON.parse(input);
411
+ const event = data.hook_event_name || "";
412
+ if (isHexLineDisabled()) {
413
+ if (event === "PreToolUse") handlePreToolUseReverse(data);
414
+ process.exit(0);
415
+ }
416
+ if (event === "SessionStart") handleSessionStart();
417
+ else if (event === "PreToolUse") handlePreToolUse(data);
418
+ else if (event === "PostToolUse") handlePostToolUse(data);
419
+ else process.exit(0);
420
+ } catch {
421
+ process.exit(0);
422
+ }
423
+ });
424
+ }
425
+ export {
426
+ _resetHexLineDisabledCache,
427
+ isHexLineDisabled
428
+ };