@levnikolaevich/hex-line-mcp 1.11.1 → 1.12.1
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 +44 -14
- package/dist/hook.mjs +55 -88
- package/dist/server.mjs +864 -115
- package/output-style.md +38 -31
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -38,16 +38,16 @@ Advanced / occasional:
|
|
|
38
38
|
| `verify` | Check if held checksums / revision are still current | Staleness check without full re-read |
|
|
39
39
|
| `inspect_path` | Unified file-or-directory inspection | File metadata for files, tree or pattern search for directories |
|
|
40
40
|
| `changes` | Compare file against git ref, shows added/removed/modified symbols | AST-level semantic diff |
|
|
41
|
-
| `bulk_replace` | Search-and-replace across multiple files
|
|
41
|
+
| `bulk_replace` | Search-and-replace across multiple files inside an explicit root path | Compact summary (default) or capped diffs via `format`, dry_run, max_files |
|
|
42
42
|
|
|
43
|
-
### Hooks (PreToolUse + PostToolUse)
|
|
43
|
+
### Hooks (SessionStart + PreToolUse + PostToolUse)
|
|
44
44
|
|
|
45
45
|
| Event | Trigger | Action |
|
|
46
46
|
|-------|---------|--------|
|
|
47
|
-
| **PreToolUse** | Read/Edit/Write/Grep on text files |
|
|
47
|
+
| **PreToolUse** | Read/Edit/Write/Grep on text files | Redirect-first policy for text files; built-in tools stay reserved for binary/media and `.claude/settings*.json` exceptions |
|
|
48
48
|
| **PreToolUse** | Bash with dangerous commands | Blocks `rm -rf /`, `git push --force`, etc. Agent must confirm with user |
|
|
49
49
|
| **PostToolUse** | Bash with 50+ lines output | RTK: deduplicates, truncates, shows filtered summary to Claude as feedback |
|
|
50
|
-
| **SessionStart** | Session begins | Injects a short
|
|
50
|
+
| **SessionStart** | Session begins | Injects a short bootstrap hint; defers to the active output style when `hex-line` style is enabled |
|
|
51
51
|
|
|
52
52
|
|
|
53
53
|
### Bash Redirects
|
|
@@ -64,12 +64,16 @@ claude mcp add -s user hex-line -- hex-line-mcp
|
|
|
64
64
|
|
|
65
65
|
ripgrep is bundled via `@vscode/ripgrep` — no manual install needed for `grep_search`.
|
|
66
66
|
|
|
67
|
+
Requires Node.js >= 20.19.0.
|
|
68
|
+
|
|
67
69
|
### Hooks
|
|
68
70
|
|
|
69
71
|
Hooks and output style are auto-synced on every MCP server startup. The server compares installed files with bundled versions and updates only when content differs. First run after `npm i -g` triggers full install automatically.
|
|
70
72
|
|
|
71
73
|
Hooks are written to global `~/.claude/settings.json` with absolute path to `hook.mjs`. Output style is installed to `~/.claude/output-styles/hex-line.md` and activated if no other style is set. To activate manually: `/config` > Output style > hex-line.
|
|
72
74
|
|
|
75
|
+
No extra manual setup is required after install. The startup sync uses the current Node runtime and a stable hook path under `~/.claude/hex-line`, so the hook command survives spaces in the home directory on Windows, macOS, and Linux.
|
|
76
|
+
|
|
73
77
|
## Validation
|
|
74
78
|
|
|
75
79
|
Use the normal package checks:
|
|
@@ -94,6 +98,8 @@ Comparative built-in vs hex-line benchmarks are maintained outside this package.
|
|
|
94
98
|
If a project already has `.hex-skills/codegraph/index.db`, `hex-line` automatically adds lightweight graph hints to `read_file`, `outline`, `grep_search`, `edit_file`, and `changes`.
|
|
95
99
|
|
|
96
100
|
- Graph enrichment is optional. If `.hex-skills/codegraph/index.db` is missing, stale, or unreadable, `hex-line` falls back to standard behavior silently.
|
|
101
|
+
- Graph enrichment is project-deterministic. `hex-line` only uses the graph database that belongs to the resolved current project scope.
|
|
102
|
+
- Nested projects do not inherit graph hints from a parent repo index once a nested project boundary is detected.
|
|
97
103
|
- `better-sqlite3` is optional. If it is unavailable, `hex-line` still works without graph hints.
|
|
98
104
|
- `read_file`, `outline`, and `grep_search` stay compact: they only surface high-signal local facts such as `api`, framework entrypoints, callers, flow, and clone hints.
|
|
99
105
|
- `edit_file` and `changes` surface the deeper review layer: external callers, downstream return/property flow, clone peers, public API risk, framework entrypoint risk, and same-name sibling warnings when present.
|
|
@@ -128,7 +134,7 @@ Use `replace_between` inside `edit_file` when you know stable start/end anchors
|
|
|
128
134
|
|
|
129
135
|
### Literal rename / refactor
|
|
130
136
|
|
|
131
|
-
Use `bulk_replace` for text rename patterns across one or more files. Returns compact summary by default; pass `format: "full"` for capped diffs. Do not use it as a substitute for structured block rewrites.
|
|
137
|
+
Use `bulk_replace` for text rename patterns across one or more files inside a known project root or directory scope. Pass `path` explicitly. In normal agent workflows that scope should be auto-filled from the current project root, not typed manually. Returns compact summary by default; pass `format: "full"` for capped diffs. Do not use it as a substitute for structured block rewrites.
|
|
132
138
|
|
|
133
139
|
### read_file
|
|
134
140
|
|
|
@@ -183,14 +189,29 @@ Edit operations (JSON array):
|
|
|
183
189
|
]
|
|
184
190
|
```
|
|
185
191
|
|
|
192
|
+
Discipline:
|
|
193
|
+
|
|
194
|
+
- Never invent `range_checksum`. Copy it from `read_file` or `grep_search(output:"content")`.
|
|
195
|
+
- First mutation in a file: prefer `grep_search` for narrow targets, or `outline -> read_file(ranges)` for structural edits.
|
|
196
|
+
- Prefer 1-2 hunks on the first pass. Once `edit_file` returns a fresh `revision`, continue from that state.
|
|
197
|
+
|
|
186
198
|
Result footer includes:
|
|
187
199
|
|
|
188
200
|
- `status: OK | AUTO_REBASED | CONFLICT`
|
|
201
|
+
- `reason: ...` as the canonical machine-readable cause for the current status
|
|
189
202
|
- `revision: ...`
|
|
190
203
|
- `file: ...`
|
|
191
204
|
- `changed_ranges: ...` when relevant
|
|
205
|
+
- `recovery_ranges: ...` with the narrowest recommended `read_file` ranges for retry
|
|
206
|
+
- `next_action: ...` as the canonical immediate choice: `apply_retry_edit`, `apply_retry_batch`, or `reread_then_retry`
|
|
192
207
|
- `remapped_refs: ...` when stale anchors were uniquely relocated
|
|
193
|
-
- `retry_checksum: ...` on local conflicts
|
|
208
|
+
- `retry_checksum: ...` on local conflicts, narrowed to the exact target range when possible
|
|
209
|
+
- `retry_edit: ...` when the server can synthesize a ready-to-retry edit skeleton from current local state
|
|
210
|
+
- `retry_edits: ...` on conservative batch conflicts when every conflicted edit can be retried directly
|
|
211
|
+
- `suggested_read_call: ...` when rereading is the safest next step
|
|
212
|
+
- `retry_plan: ...` with a compact machine-readable next-call plan
|
|
213
|
+
- `summary: ...` and `snippet: ...` instead of long prose blocks
|
|
214
|
+
- `edit_conflicts: N` on conservative multi-edit preflight conflicts
|
|
194
215
|
|
|
195
216
|
### write_file
|
|
196
217
|
|
|
@@ -239,7 +260,7 @@ Not for `.json`, `.yaml`, `.txt` -- use `read_file` directly for those.
|
|
|
239
260
|
|
|
240
261
|
### verify
|
|
241
262
|
|
|
242
|
-
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
|
|
263
|
+
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 canonical `status`, `summary`, `next_action`, and compact entry lines.
|
|
243
264
|
|
|
244
265
|
| Parameter | Type | Required | Description |
|
|
245
266
|
|-----------|------|----------|-------------|
|
|
@@ -251,13 +272,16 @@ Example output:
|
|
|
251
272
|
|
|
252
273
|
```text
|
|
253
274
|
status: STALE
|
|
275
|
+
reason: checksums_stale
|
|
254
276
|
revision: rev-17-deadbeef
|
|
255
277
|
file: 1-120:abc123ef
|
|
256
278
|
summary: valid=0 stale=1 invalid=0
|
|
279
|
+
next_action: reread_ranges
|
|
257
280
|
base_revision: rev-16-feedcafe
|
|
258
281
|
changed_ranges: 10-12(replace)
|
|
282
|
+
suggested_read_call: {"tool":"mcp__hex-line__read_file","arguments":{"path":"/repo/file.ts","ranges":["10-12"]}}
|
|
259
283
|
|
|
260
|
-
STALE 10-12 checksum: 10-12:oldc0de0
|
|
284
|
+
entry: 1/1 | status: STALE | span: 10-12 | checksum: 10-12:oldc0de0 | current_checksum: 10-12:newc0de0 | next_action: reread_range | summary: content changed since checksum capture
|
|
261
285
|
```
|
|
262
286
|
|
|
263
287
|
### inspect_path
|
|
@@ -279,15 +303,15 @@ Inspect a file or directory path without guessing which low-level tool to call f
|
|
|
279
303
|
|
|
280
304
|
## Hook
|
|
281
305
|
|
|
282
|
-
The unified hook (`hook.mjs`) handles
|
|
306
|
+
The unified hook (`hook.mjs`) handles three Claude hook events:
|
|
283
307
|
|
|
284
308
|
### PreToolUse: Tool Redirect
|
|
285
309
|
|
|
286
|
-
|
|
310
|
+
Applies redirect-first steering to built-in `Read`, `Edit`, `Write`, and `Grep` on text files. Binary/media files (images, PDFs, notebooks, archives, executables, fonts, media) stay on built-in tools. `.claude/settings.json` and `.claude/settings.local.json` at project root or home are also allowed on built-in tools.
|
|
287
311
|
|
|
288
312
|
### PreToolUse: Bash Redirect + Dangerous Blocker
|
|
289
313
|
|
|
290
|
-
Intercepts simple Bash commands (`cat`, `head`, `tail`, `
|
|
314
|
+
Intercepts simple Bash commands (`cat`, `head`, `tail`, `tree`, `find`, `stat`, `wc -l`, `grep`, `rg`, `sed -i`, etc.) and redirects covered cases to hex-line tools. `ls`/`dir` are redirected only for recursive listing. Dangerous commands (`rm -rf /`, `git push --force`, `git reset --hard`, `DROP TABLE`, `chmod 777`, `mkfs`, `dd`) are blocked.
|
|
291
315
|
|
|
292
316
|
### PostToolUse: RTK Output Filter
|
|
293
317
|
|
|
@@ -298,7 +322,11 @@ Triggers on `Bash` tool output exceeding 50 lines. Pipeline:
|
|
|
298
322
|
3. **Deduplicate** -- collapses identical normalized lines with `(xN)` counts
|
|
299
323
|
4. **Truncate** -- keeps first 15 + last 15 lines, omits the middle
|
|
300
324
|
|
|
301
|
-
|
|
325
|
+
### SessionStart: Bootstrap Hint
|
|
326
|
+
|
|
327
|
+
Injects a compact startup reminder. If the `hex-line` output style is active, the hook emits only a minimal bootstrap hint plus `ToolSearch('+hex-line read edit')` fallback. Otherwise it injects the short preferred read/edit workflow directly, including the scope rule: use file paths for file tools and the current project root for repo-wide tools such as `bulk_replace`.
|
|
328
|
+
|
|
329
|
+
Hook policy constants in `lib/hook-policy.mjs`:
|
|
302
330
|
|
|
303
331
|
| Constant | Default | Purpose |
|
|
304
332
|
|----------|---------|--------|
|
|
@@ -308,7 +336,7 @@ Configuration constants in `hook.mjs`:
|
|
|
308
336
|
|
|
309
337
|
### SessionStart: Tool Preferences
|
|
310
338
|
|
|
311
|
-
Injects a short operational workflow into agent context at session start
|
|
339
|
+
Injects a short operational workflow into agent context at session start. If schemas are not loaded yet, it includes the `ToolSearch('+hex-line read edit')` fallback. Primary flow stays `outline -> read_file -> edit_file -> verify`, with targeted reads over full-file reads.
|
|
312
340
|
|
|
313
341
|
## Architecture
|
|
314
342
|
|
|
@@ -318,6 +346,8 @@ hex-line-mcp/
|
|
|
318
346
|
hook.mjs Unified hook (PreToolUse + PostToolUse + SessionStart)
|
|
319
347
|
package.json
|
|
320
348
|
lib/
|
|
349
|
+
hook-policy.mjs Shared hook policy: redirects, thresholds, danger patterns
|
|
350
|
+
setup.mjs Startup autosync for hook + output style
|
|
321
351
|
read.mjs File reading with hash annotation
|
|
322
352
|
edit.mjs Anchor-based edits, diff output
|
|
323
353
|
search.mjs ripgrep wrapper with hash-annotated results
|
|
@@ -397,7 +427,7 @@ The PostToolUse hook normalizes Bash output (replaces UUIDs, timestamps, IPs wit
|
|
|
397
427
|
<details>
|
|
398
428
|
<summary><b>Can I disable the built-in tool blocking?</b></summary>
|
|
399
429
|
|
|
400
|
-
Yes.
|
|
430
|
+
Yes. To downgrade redirects to advice, set `.hex-skills/environment_state.json` to `{ "hooks": { "mode": "advisory" } }`. To remove the hook entirely, delete the `hex-line` hook entries from `~/.claude/settings.json`. To disable the MCP server for one project, add `hex-line` to `~/.claude.json -> projects.{cwd}.disabledMcpServers`.
|
|
401
431
|
|
|
402
432
|
</details>
|
|
403
433
|
|
package/dist/hook.mjs
CHANGED
|
@@ -54,10 +54,12 @@ function normalizeOutput(text, opts = {}) {
|
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
// hook.mjs
|
|
57
|
-
import { readFileSync,
|
|
58
|
-
import { resolve } from "node:path";
|
|
57
|
+
import { readFileSync, writeSync } from "node:fs";
|
|
58
|
+
import { resolve as resolve2 } from "node:path";
|
|
59
59
|
import { homedir } from "node:os";
|
|
60
|
-
|
|
60
|
+
|
|
61
|
+
// lib/hook-policy.mjs
|
|
62
|
+
import { resolve } from "node:path";
|
|
61
63
|
var BINARY_EXT = /* @__PURE__ */ new Set([
|
|
62
64
|
".png",
|
|
63
65
|
".jpg",
|
|
@@ -98,7 +100,9 @@ var OUTLINEABLE_EXT = /* @__PURE__ */ new Set([
|
|
|
98
100
|
".tsx",
|
|
99
101
|
".py",
|
|
100
102
|
".cs",
|
|
101
|
-
".php"
|
|
103
|
+
".php",
|
|
104
|
+
".md",
|
|
105
|
+
".mdx"
|
|
102
106
|
]);
|
|
103
107
|
var REVERSE_TOOL_HINTS = {
|
|
104
108
|
"mcp__hex-line__read_file": "Read (file_path, offset, limit)",
|
|
@@ -109,7 +113,7 @@ var REVERSE_TOOL_HINTS = {
|
|
|
109
113
|
"mcp__hex-line__outline": "Read with offset/limit",
|
|
110
114
|
"mcp__hex-line__verify": "Read (re-read file to check freshness)",
|
|
111
115
|
"mcp__hex-line__changes": "Bash(git diff)",
|
|
112
|
-
"mcp__hex-line__bulk_replace": "Edit (text rename/refactor across files)"
|
|
116
|
+
"mcp__hex-line__bulk_replace": "Edit (text rename/refactor across files inside an explicit root path)"
|
|
113
117
|
};
|
|
114
118
|
var TOOL_HINTS = {
|
|
115
119
|
Read: "mcp__hex-line__read_file (not Read). For writing: write_file (no prior Read needed)",
|
|
@@ -122,12 +126,12 @@ var TOOL_HINTS = {
|
|
|
122
126
|
ls: "mcp__hex-line__inspect_path for tree or pattern search (not ls/find/tree). E.g. pattern='*-mcp' type='dir'",
|
|
123
127
|
stat: "mcp__hex-line__inspect_path for compact file metadata (not stat/wc/file)",
|
|
124
128
|
grep: "mcp__hex-line__grep_search (not grep/rg). Params: output, literal, context_before, context_after, multiline",
|
|
125
|
-
sed: "mcp__hex-line__edit_file for hash edits, or mcp__hex-line__bulk_replace for text rename (not sed -i)",
|
|
129
|
+
sed: "mcp__hex-line__edit_file for hash edits, or mcp__hex-line__bulk_replace with path=<project root> for text rename (not sed -i)",
|
|
126
130
|
diff: "mcp__hex-line__changes (not diff). Git diff with change symbols",
|
|
127
131
|
outline: "mcp__hex-line__outline (before reading large code files)",
|
|
128
132
|
verify: "mcp__hex-line__verify (staleness / revision check without re-read)",
|
|
129
133
|
changes: "mcp__hex-line__changes (git diff with change symbols)",
|
|
130
|
-
bulk: "mcp__hex-line__bulk_replace (multi-file search-replace)"
|
|
134
|
+
bulk: "mcp__hex-line__bulk_replace with path=<project root> (multi-file search-replace)"
|
|
131
135
|
};
|
|
132
136
|
var DEFERRED_HINT = "If schemas not loaded: ToolSearch('+hex-line read edit')";
|
|
133
137
|
var BASH_REDIRECTS = [
|
|
@@ -136,9 +140,7 @@ var BASH_REDIRECTS = [
|
|
|
136
140
|
{ regex: /^tail\s+(?!-[fF])/, key: "tail" },
|
|
137
141
|
{ regex: /^(less|more)\s+/, key: "cat" },
|
|
138
142
|
{ regex: /^ls\s+-\S*R(\s|$)/, key: "ls" },
|
|
139
|
-
// ls -R, ls -laR (recursive only)
|
|
140
143
|
{ regex: /^dir\s+\/[sS](\s|$)/, key: "ls" },
|
|
141
|
-
// dir /s, dir /S (recursive only)
|
|
142
144
|
{ regex: /^tree\s+/, key: "ls" },
|
|
143
145
|
{ regex: /^find\s+/, key: "ls" },
|
|
144
146
|
{ regex: /^(stat|wc)\s+/, key: "stat" },
|
|
@@ -152,34 +154,13 @@ var TOOL_REDIRECT_MAP = {
|
|
|
152
154
|
Grep: "Grep"
|
|
153
155
|
};
|
|
154
156
|
var DANGEROUS_PATTERNS = [
|
|
155
|
-
{
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
},
|
|
159
|
-
{
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
},
|
|
163
|
-
{
|
|
164
|
-
regex: /git\s+reset\s+--hard/,
|
|
165
|
-
reason: "hard reset discards uncommitted changes"
|
|
166
|
-
},
|
|
167
|
-
{
|
|
168
|
-
regex: /DROP\s+(TABLE|DATABASE)/i,
|
|
169
|
-
reason: "DROP destroys data permanently"
|
|
170
|
-
},
|
|
171
|
-
{
|
|
172
|
-
regex: /chmod\s+777/,
|
|
173
|
-
reason: "chmod 777 removes all access restrictions"
|
|
174
|
-
},
|
|
175
|
-
{
|
|
176
|
-
regex: /mkfs/,
|
|
177
|
-
reason: "filesystem format destroys all data"
|
|
178
|
-
},
|
|
179
|
-
{
|
|
180
|
-
regex: /dd\s+if=\/dev\/zero/,
|
|
181
|
-
reason: "direct disk write destroys data"
|
|
182
|
-
}
|
|
157
|
+
{ regex: /rm\s+(-[rf]+\s+)*[/~]/, reason: "rm -rf on root/home directory" },
|
|
158
|
+
{ regex: /git\s+push\s+(-f|--force)/, reason: "force push can overwrite remote history" },
|
|
159
|
+
{ regex: /git\s+reset\s+--hard/, reason: "hard reset discards uncommitted changes" },
|
|
160
|
+
{ regex: /DROP\s+(TABLE|DATABASE)/i, reason: "DROP destroys data permanently" },
|
|
161
|
+
{ regex: /chmod\s+777/, reason: "chmod 777 removes all access restrictions" },
|
|
162
|
+
{ regex: /mkfs/, reason: "filesystem format destroys all data" },
|
|
163
|
+
{ regex: /dd\s+if=\/dev\/zero/, reason: "direct disk write destroys data" }
|
|
183
164
|
];
|
|
184
165
|
var COMPOUND_OPERATORS = /[|]|>>?|&&|\|\||;/;
|
|
185
166
|
var CMD_PATTERNS = [
|
|
@@ -189,11 +170,24 @@ var CMD_PATTERNS = [
|
|
|
189
170
|
[/pip install/i, "pip-install"],
|
|
190
171
|
[/git (log|diff|status)/i, "git"]
|
|
191
172
|
];
|
|
192
|
-
var
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
173
|
+
var HOOK_OUTPUT_POLICY = {
|
|
174
|
+
lineThreshold: 50,
|
|
175
|
+
headLines: 15,
|
|
176
|
+
tailLines: 15
|
|
177
|
+
};
|
|
178
|
+
function buildAllowedClaudeSettingsPaths(cwd, home) {
|
|
179
|
+
const cwdNorm = cwd.replace(/\\/g, "/");
|
|
180
|
+
const homeNorm = home.replace(/\\/g, "/");
|
|
181
|
+
return [
|
|
182
|
+
resolve(cwdNorm, ".claude/settings.json"),
|
|
183
|
+
resolve(cwdNorm, ".claude/settings.local.json"),
|
|
184
|
+
resolve(homeNorm, ".claude/settings.json"),
|
|
185
|
+
resolve(homeNorm, ".claude/settings.local.json")
|
|
186
|
+
].map((entry) => entry.replace(/\\/g, "/").toLowerCase());
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// hook.mjs
|
|
190
|
+
import { fileURLToPath } from "node:url";
|
|
197
191
|
function extOf(filePath) {
|
|
198
192
|
const dot = filePath.lastIndexOf(".");
|
|
199
193
|
return dot !== -1 ? filePath.slice(dot).toLowerCase() : "";
|
|
@@ -203,16 +197,8 @@ function getFilePath(toolInput) {
|
|
|
203
197
|
}
|
|
204
198
|
function resolveToolPath(filePath) {
|
|
205
199
|
if (!filePath) return "";
|
|
206
|
-
if (filePath.startsWith("~/")) return
|
|
207
|
-
return
|
|
208
|
-
}
|
|
209
|
-
function getFileSize(filePath) {
|
|
210
|
-
if (!filePath) return null;
|
|
211
|
-
try {
|
|
212
|
-
return statSync(resolveToolPath(filePath)).size;
|
|
213
|
-
} catch {
|
|
214
|
-
return null;
|
|
215
|
-
}
|
|
200
|
+
if (filePath.startsWith("~/")) return resolve2(homedir(), filePath.slice(2));
|
|
201
|
+
return resolve2(process.cwd(), filePath);
|
|
216
202
|
}
|
|
217
203
|
function isPartialRead(toolInput) {
|
|
218
204
|
return [toolInput.offset, toolInput.limit, toolInput.start_line, toolInput.end_line, toolInput.ranges].some((value) => value !== void 0 && value !== null && value !== "");
|
|
@@ -238,7 +224,7 @@ function isHexLineDisabled(configPath) {
|
|
|
238
224
|
if (_hexLineDisabled !== null) return _hexLineDisabled;
|
|
239
225
|
_hexLineDisabled = false;
|
|
240
226
|
try {
|
|
241
|
-
const p = configPath ||
|
|
227
|
+
const p = configPath || resolve2(homedir(), ".claude.json");
|
|
242
228
|
const claudeJson = JSON.parse(readFileSync(p, "utf-8"));
|
|
243
229
|
const projects = claudeJson.projects;
|
|
244
230
|
if (!projects || typeof projects !== "object") return _hexLineDisabled;
|
|
@@ -264,7 +250,7 @@ function getHookMode() {
|
|
|
264
250
|
if (_hookMode !== void 0) return _hookMode;
|
|
265
251
|
_hookMode = "blocking";
|
|
266
252
|
try {
|
|
267
|
-
const stateFile =
|
|
253
|
+
const stateFile = resolve2(process.cwd(), ".hex-skills/environment_state.json");
|
|
268
254
|
const data = JSON.parse(readFileSync(stateFile, "utf-8"));
|
|
269
255
|
if (data.hooks?.mode === "advisory") _hookMode = "advisory";
|
|
270
256
|
} catch {
|
|
@@ -317,47 +303,26 @@ function handlePreToolUse(data) {
|
|
|
317
303
|
const hintKey = TOOL_REDIRECT_MAP[toolName];
|
|
318
304
|
if (hintKey) {
|
|
319
305
|
const filePath = getFilePath(toolInput);
|
|
320
|
-
const fileSize = getFileSize(filePath);
|
|
321
306
|
if (BINARY_EXT.has(extOf(filePath))) {
|
|
322
307
|
process.exit(0);
|
|
323
308
|
}
|
|
324
309
|
const resolvedNorm = resolveToolPath(filePath).replace(/\\/g, "/");
|
|
325
|
-
const
|
|
326
|
-
|
|
327
|
-
const claudeAllow = [
|
|
328
|
-
cwdNorm + "/.claude/settings.json",
|
|
329
|
-
cwdNorm + "/.claude/settings.local.json",
|
|
330
|
-
homeNorm + "/.claude/settings.json",
|
|
331
|
-
homeNorm + "/.claude/settings.local.json"
|
|
332
|
-
];
|
|
333
|
-
if (claudeAllow.some((p) => resolvedNorm.toLowerCase() === p.toLowerCase())) {
|
|
310
|
+
const claudeAllow = buildAllowedClaudeSettingsPaths(process.cwd(), homedir());
|
|
311
|
+
if (claudeAllow.includes(resolvedNorm.toLowerCase())) {
|
|
334
312
|
process.exit(0);
|
|
335
313
|
}
|
|
336
314
|
if (resolvedNorm.includes("/.claude/")) {
|
|
337
315
|
redirect("Protected .claude/ path. Use built-in tools for .claude/ config files.");
|
|
338
316
|
}
|
|
339
317
|
if (toolName === "Read") {
|
|
340
|
-
if (isPartialRead(toolInput)) {
|
|
341
|
-
process.exit(0);
|
|
342
|
-
}
|
|
343
|
-
if (fileSize !== null && fileSize <= LARGE_FILE_BYTES) {
|
|
344
|
-
const ext2 = filePath ? extOf(filePath) : "";
|
|
345
|
-
const hint = filePath && OUTLINEABLE_EXT.has(ext2) ? `Use mcp__hex-line__outline(path="${filePath}") for structure, then mcp__hex-line__read_file(path="${filePath}") with ranges.` : filePath ? `Use mcp__hex-line__read_file(path="${filePath}"). Built-in Read wastes edit context.` : "Use mcp__hex-line__read_file. Built-in Read wastes edit context.";
|
|
346
|
-
advise(hint, DEFERRED_HINT);
|
|
347
|
-
}
|
|
348
318
|
const ext = filePath ? extOf(filePath) : "";
|
|
349
|
-
const
|
|
350
|
-
|
|
319
|
+
const rangeHint = isPartialRead(toolInput) ? " Preserve the same offset/limit or ranges in read_file." : "";
|
|
320
|
+
const outlineHint = filePath && OUTLINEABLE_EXT.has(ext) ? `Use mcp__hex-line__outline(path="${filePath}") for structure, then mcp__hex-line__read_file(path="${filePath}") with ranges to read only what you need.${rangeHint}` : filePath ? `Use mcp__hex-line__read_file(path="${filePath}") with ranges or offset/limit.${rangeHint}` : "Use mcp__hex-line__inspect_path or mcp__hex-line__read_file";
|
|
321
|
+
redirect(outlineHint, "Use hex-line for text-file reads to keep hashes, revision metadata, and graph hints in one flow.\n" + DEFERRED_HINT);
|
|
351
322
|
}
|
|
352
323
|
if (toolName === "Edit") {
|
|
353
|
-
const oldText = String(toolInput.old_string || "");
|
|
354
|
-
const isLargeEdit = Boolean(toolInput.replace_all) || oldText.length > LARGE_EDIT_CHARS || fileSize !== null && fileSize > LARGE_FILE_BYTES;
|
|
355
|
-
if (!isLargeEdit) {
|
|
356
|
-
const editHint = filePath ? `Prefer mcp__hex-line__edit_file(path="${filePath}") for hash-verified edits.` : "Prefer mcp__hex-line__edit_file for hash-verified edits.";
|
|
357
|
-
advise(editHint);
|
|
358
|
-
}
|
|
359
324
|
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";
|
|
360
|
-
redirect(target, "
|
|
325
|
+
redirect(target, "Use hash-verified edits for text files. Locate anchors/checksums first, then call edit_file once with batched edits.\n" + DEFERRED_HINT);
|
|
361
326
|
}
|
|
362
327
|
if (toolName === "Write") {
|
|
363
328
|
const pathNote = filePath ? ` with path="${filePath}"` : "";
|
|
@@ -433,11 +398,14 @@ function handlePostToolUse(data) {
|
|
|
433
398
|
}
|
|
434
399
|
const lines = rawText.split("\n");
|
|
435
400
|
const originalCount = lines.length;
|
|
436
|
-
if (originalCount <
|
|
401
|
+
if (originalCount < HOOK_OUTPUT_POLICY.lineThreshold) {
|
|
437
402
|
process.exit(0);
|
|
438
403
|
}
|
|
439
404
|
const type = detectCommandType(command);
|
|
440
|
-
const filtered = normalizeOutput(lines.join("\n"), {
|
|
405
|
+
const filtered = normalizeOutput(lines.join("\n"), {
|
|
406
|
+
headLines: HOOK_OUTPUT_POLICY.headLines,
|
|
407
|
+
tailLines: HOOK_OUTPUT_POLICY.tailLines
|
|
408
|
+
});
|
|
441
409
|
const filteredCount = filtered.split("\n").length;
|
|
442
410
|
const header = `RTK FILTERED: ${type} (${originalCount} lines -> ${filteredCount} lines)`;
|
|
443
411
|
const output = [
|
|
@@ -455,9 +423,9 @@ function handlePostToolUse(data) {
|
|
|
455
423
|
}
|
|
456
424
|
function handleSessionStart() {
|
|
457
425
|
const settingsFiles = [
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
426
|
+
resolve2(process.cwd(), ".claude/settings.local.json"),
|
|
427
|
+
resolve2(process.cwd(), ".claude/settings.json"),
|
|
428
|
+
resolve2(homedir(), ".claude/settings.json")
|
|
461
429
|
];
|
|
462
430
|
let styleActive = false;
|
|
463
431
|
for (const f of settingsFiles) {
|
|
@@ -471,8 +439,7 @@ function handleSessionStart() {
|
|
|
471
439
|
} catch {
|
|
472
440
|
}
|
|
473
441
|
}
|
|
474
|
-
const
|
|
475
|
-
const msg = prefix + "<hex-line_instructions>\n <deferred_loading>If hex-line schemas not loaded, run: ToolSearch('+hex-line read edit')</deferred_loading>\n <exploration>\n <rule>Use outline for structure (code + markdown), not Read. ~10-20 lines vs hundreds.</rule>\n <rule>Use read_file with offset/limit or ranges for targeted reads.</rule>\n <rule>Use grep_search before editing to get hash anchors.</rule>\n </exploration>\n <editing>\n <path name='surgical'>grep_search \u2192 edit_file (fastest: hash-verified, no full read needed)</path>\n <path name='exploratory'>outline \u2192 read_file (ranges) \u2192 edit_file with base_revision</path>\n <path name='multi-file'>bulk_replace for text rename/refactor across files</path>\n </editing>\n <tips>\n <tip>Carry revision from read_file into base_revision on edit_file.</tip>\n <tip>If edit returns CONFLICT, call verify \u2014 only reread when STALE.</tip>\n <tip>Batch multiple edits to same file in one edit_file call.</tip>\n <tip>Use write_file for new files (no prior Read needed).</tip>\n </tips>\n <exceptions>Built-in Read OK for: images, PDFs, notebooks, Glob (always), .claude/settings.json</exceptions>\n</hex-line_instructions>";
|
|
442
|
+
const msg = styleActive ? "Hex-line MCP available. Output style active.\n<hex-line_instructions>\n <deferred_loading>If hex-line schemas not loaded, run: ToolSearch('+hex-line read edit')</deferred_loading>\n <note>Follow the active hex-line output style for primary tool choices.</note>\n <exceptions>Built-in tools stay OK for images, PDFs, notebooks, Glob, .claude/settings.json, and .claude/settings.local.json.</exceptions>\n</hex-line_instructions>" : "Hex-line MCP available.\n<hex-line_instructions>\n <deferred_loading>If hex-line schemas not loaded, run: ToolSearch('+hex-line read edit')</deferred_loading>\n <exploration>\n <rule>Use outline for structure (code + markdown), not Read. ~10-20 lines vs hundreds.</rule>\n <rule>Use read_file with offset/limit or ranges for targeted reads.</rule>\n <rule>Use grep_search before editing to get hash anchors.</rule>\n </exploration>\n <editing>\n <path name='surgical'>grep_search \u2192 edit_file (fastest: hash-verified, no full read needed)</path>\n <path name='exploratory'>outline \u2192 read_file (ranges) \u2192 edit_file with base_revision</path>\n <path name='multi-file'>bulk_replace(path="<project root>") for text rename/refactor across files</path>\n </editing>\n <tips>\n <tip>Auto-fill path from the active file or project root. Do not leave repo scope implicit.</tip>\n <tip>Never invent range_checksum. Copy it from fresh read_file or grep_search blocks.</tip>\n <tip>Prefer set_line or insert_after for small local changes and replace_between for larger bounded rewrites.</tip>\n <tip>Carry revision from read_file into base_revision on edit_file.</tip>\n <tip>If edit returns CONFLICT, call verify \u2014 only reread when STALE.</tip>\n <tip>Avoid large first-pass edit batches. Start with 1-2 hunks, then continue from the returned revision.</tip>\n <tip>Use write_file for new files (no prior Read needed).</tip>\n </tips>\n <exceptions>Built-in tools stay OK for images, PDFs, notebooks, Glob, .claude/settings.json, and .claude/settings.local.json.</exceptions>\n</hex-line_instructions>";
|
|
476
443
|
safeExit(1, JSON.stringify({ systemMessage: msg }), 0);
|
|
477
444
|
}
|
|
478
445
|
var _norm = (p) => p.replace(/\\/g, "/");
|