@levnikolaevich/hex-line-mcp 1.11.0 → 1.12.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 +42 -14
- package/dist/hook.mjs +55 -88
- package/dist/server.mjs +861 -114
- package/output-style.md +38 -31
- package/package.json +1 -1
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
|
|
@@ -70,6 +70,8 @@ Hooks and output style are auto-synced on every MCP server startup. The server c
|
|
|
70
70
|
|
|
71
71
|
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
72
|
|
|
73
|
+
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.
|
|
74
|
+
|
|
73
75
|
## Validation
|
|
74
76
|
|
|
75
77
|
Use the normal package checks:
|
|
@@ -94,6 +96,8 @@ Comparative built-in vs hex-line benchmarks are maintained outside this package.
|
|
|
94
96
|
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
97
|
|
|
96
98
|
- Graph enrichment is optional. If `.hex-skills/codegraph/index.db` is missing, stale, or unreadable, `hex-line` falls back to standard behavior silently.
|
|
99
|
+
- Graph enrichment is project-deterministic. `hex-line` only uses the graph database that belongs to the resolved current project scope.
|
|
100
|
+
- Nested projects do not inherit graph hints from a parent repo index once a nested project boundary is detected.
|
|
97
101
|
- `better-sqlite3` is optional. If it is unavailable, `hex-line` still works without graph hints.
|
|
98
102
|
- `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
103
|
- `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 +132,7 @@ Use `replace_between` inside `edit_file` when you know stable start/end anchors
|
|
|
128
132
|
|
|
129
133
|
### Literal rename / refactor
|
|
130
134
|
|
|
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.
|
|
135
|
+
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
136
|
|
|
133
137
|
### read_file
|
|
134
138
|
|
|
@@ -183,14 +187,29 @@ Edit operations (JSON array):
|
|
|
183
187
|
]
|
|
184
188
|
```
|
|
185
189
|
|
|
190
|
+
Discipline:
|
|
191
|
+
|
|
192
|
+
- Never invent `range_checksum`. Copy it from `read_file` or `grep_search(output:"content")`.
|
|
193
|
+
- First mutation in a file: prefer `grep_search` for narrow targets, or `outline -> read_file(ranges)` for structural edits.
|
|
194
|
+
- Prefer 1-2 hunks on the first pass. Once `edit_file` returns a fresh `revision`, continue from that state.
|
|
195
|
+
|
|
186
196
|
Result footer includes:
|
|
187
197
|
|
|
188
198
|
- `status: OK | AUTO_REBASED | CONFLICT`
|
|
199
|
+
- `reason: ...` as the canonical machine-readable cause for the current status
|
|
189
200
|
- `revision: ...`
|
|
190
201
|
- `file: ...`
|
|
191
202
|
- `changed_ranges: ...` when relevant
|
|
203
|
+
- `recovery_ranges: ...` with the narrowest recommended `read_file` ranges for retry
|
|
204
|
+
- `next_action: ...` as the canonical immediate choice: `apply_retry_edit`, `apply_retry_batch`, or `reread_then_retry`
|
|
192
205
|
- `remapped_refs: ...` when stale anchors were uniquely relocated
|
|
193
|
-
- `retry_checksum: ...` on local conflicts
|
|
206
|
+
- `retry_checksum: ...` on local conflicts, narrowed to the exact target range when possible
|
|
207
|
+
- `retry_edit: ...` when the server can synthesize a ready-to-retry edit skeleton from current local state
|
|
208
|
+
- `retry_edits: ...` on conservative batch conflicts when every conflicted edit can be retried directly
|
|
209
|
+
- `suggested_read_call: ...` when rereading is the safest next step
|
|
210
|
+
- `retry_plan: ...` with a compact machine-readable next-call plan
|
|
211
|
+
- `summary: ...` and `snippet: ...` instead of long prose blocks
|
|
212
|
+
- `edit_conflicts: N` on conservative multi-edit preflight conflicts
|
|
194
213
|
|
|
195
214
|
### write_file
|
|
196
215
|
|
|
@@ -239,7 +258,7 @@ Not for `.json`, `.yaml`, `.txt` -- use `read_file` directly for those.
|
|
|
239
258
|
|
|
240
259
|
### verify
|
|
241
260
|
|
|
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
|
|
261
|
+
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
262
|
|
|
244
263
|
| Parameter | Type | Required | Description |
|
|
245
264
|
|-----------|------|----------|-------------|
|
|
@@ -251,13 +270,16 @@ Example output:
|
|
|
251
270
|
|
|
252
271
|
```text
|
|
253
272
|
status: STALE
|
|
273
|
+
reason: checksums_stale
|
|
254
274
|
revision: rev-17-deadbeef
|
|
255
275
|
file: 1-120:abc123ef
|
|
256
276
|
summary: valid=0 stale=1 invalid=0
|
|
277
|
+
next_action: reread_ranges
|
|
257
278
|
base_revision: rev-16-feedcafe
|
|
258
279
|
changed_ranges: 10-12(replace)
|
|
280
|
+
suggested_read_call: {"tool":"mcp__hex-line__read_file","arguments":{"path":"/repo/file.ts","ranges":["10-12"]}}
|
|
259
281
|
|
|
260
|
-
STALE 10-12 checksum: 10-12:oldc0de0
|
|
282
|
+
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
283
|
```
|
|
262
284
|
|
|
263
285
|
### inspect_path
|
|
@@ -279,15 +301,15 @@ Inspect a file or directory path without guessing which low-level tool to call f
|
|
|
279
301
|
|
|
280
302
|
## Hook
|
|
281
303
|
|
|
282
|
-
The unified hook (`hook.mjs`) handles
|
|
304
|
+
The unified hook (`hook.mjs`) handles three Claude hook events:
|
|
283
305
|
|
|
284
306
|
### PreToolUse: Tool Redirect
|
|
285
307
|
|
|
286
|
-
|
|
308
|
+
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
309
|
|
|
288
310
|
### PreToolUse: Bash Redirect + Dangerous Blocker
|
|
289
311
|
|
|
290
|
-
Intercepts simple Bash commands (`cat`, `head`, `tail`, `
|
|
312
|
+
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
313
|
|
|
292
314
|
### PostToolUse: RTK Output Filter
|
|
293
315
|
|
|
@@ -298,7 +320,11 @@ Triggers on `Bash` tool output exceeding 50 lines. Pipeline:
|
|
|
298
320
|
3. **Deduplicate** -- collapses identical normalized lines with `(xN)` counts
|
|
299
321
|
4. **Truncate** -- keeps first 15 + last 15 lines, omits the middle
|
|
300
322
|
|
|
301
|
-
|
|
323
|
+
### SessionStart: Bootstrap Hint
|
|
324
|
+
|
|
325
|
+
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`.
|
|
326
|
+
|
|
327
|
+
Hook policy constants in `lib/hook-policy.mjs`:
|
|
302
328
|
|
|
303
329
|
| Constant | Default | Purpose |
|
|
304
330
|
|----------|---------|--------|
|
|
@@ -308,7 +334,7 @@ Configuration constants in `hook.mjs`:
|
|
|
308
334
|
|
|
309
335
|
### SessionStart: Tool Preferences
|
|
310
336
|
|
|
311
|
-
Injects a short operational workflow into agent context at session start
|
|
337
|
+
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
338
|
|
|
313
339
|
## Architecture
|
|
314
340
|
|
|
@@ -318,6 +344,8 @@ hex-line-mcp/
|
|
|
318
344
|
hook.mjs Unified hook (PreToolUse + PostToolUse + SessionStart)
|
|
319
345
|
package.json
|
|
320
346
|
lib/
|
|
347
|
+
hook-policy.mjs Shared hook policy: redirects, thresholds, danger patterns
|
|
348
|
+
setup.mjs Startup autosync for hook + output style
|
|
321
349
|
read.mjs File reading with hash annotation
|
|
322
350
|
edit.mjs Anchor-based edits, diff output
|
|
323
351
|
search.mjs ripgrep wrapper with hash-annotated results
|
|
@@ -397,7 +425,7 @@ The PostToolUse hook normalizes Bash output (replaces UUIDs, timestamps, IPs wit
|
|
|
397
425
|
<details>
|
|
398
426
|
<summary><b>Can I disable the built-in tool blocking?</b></summary>
|
|
399
427
|
|
|
400
|
-
Yes.
|
|
428
|
+
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
429
|
|
|
402
430
|
</details>
|
|
403
431
|
|
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, "/");
|