@levnikolaevich/hex-line-mcp 1.2.0 → 1.3.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 +63 -37
- package/hook.mjs +106 -24
- package/lib/bulk-replace.mjs +8 -6
- package/lib/changes.mjs +7 -6
- package/lib/coerce.mjs +0 -6
- package/lib/edit.mjs +140 -41
- package/lib/format.mjs +138 -0
- package/lib/hash.mjs +1 -1
- package/lib/info.mjs +13 -31
- package/lib/read.mjs +5 -24
- package/lib/search.mjs +213 -77
- package/lib/security.mjs +6 -8
- package/lib/setup.mjs +37 -5
- package/lib/tree.mjs +82 -83
- package/lib/verify.mjs +2 -2
- package/output-style.md +1 -1
- package/package.json +12 -4
- package/server.mjs +39 -24
package/README.md
CHANGED
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
Hash-verified file editing MCP + token efficiency hook for AI coding agents.
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/@levnikolaevich/hex-line-mcp)
|
|
6
|
-
](https://www.npmjs.com/package/@levnikolaevich/hex-line-mcp)
|
|
7
|
+
[](./LICENSE)
|
|
8
|
+

|
|
7
9
|
|
|
8
10
|
Every line carries an FNV-1a content hash. Every edit must present those hashes back -- proving the agent is editing what it thinks it's editing. No stale context, no silent corruption.
|
|
9
11
|
|
|
@@ -16,10 +18,10 @@ Every line carries an FNV-1a content hash. Every edit must present those hashes
|
|
|
16
18
|
| `read_file` | Read file with hash-annotated lines and range checksums | Partial reads via `offset`/`limit` |
|
|
17
19
|
| `edit_file` | Hash-verified edits with anchor or text replacement | Returns compact diff via `diff` package |
|
|
18
20
|
| `write_file` | Create new file or overwrite, auto-creates parent dirs | Path validation, no hash overhead |
|
|
19
|
-
| `grep_search` | Search with ripgrep,
|
|
21
|
+
| `grep_search` | Search with ripgrep, 3 output modes, per-group checksums | Edit-ready: grep -> edit directly with checksums |
|
|
20
22
|
| `outline` | AST-based structural overview via tree-sitter WASM | 95% token reduction (10 lines instead of 500) |
|
|
21
23
|
| `verify` | Check if held range checksums are still valid | Single-line response avoids full re-read |
|
|
22
|
-
| `directory_tree` | Compact directory tree with .gitignore support | Skips node_modules/.git, shows file sizes |
|
|
24
|
+
| `directory_tree` | Compact directory tree with root .gitignore support | Skips node_modules/.git, shows file sizes |
|
|
23
25
|
| `get_file_info` | File metadata without reading content | Size, lines, mtime, type, binary detection |
|
|
24
26
|
| `setup_hooks` | Configure PreToolUse + PostToolUse hooks for Claude/Gemini/Codex | One call sets up everything, idempotent |
|
|
25
27
|
| `changes` | Compare file against git ref, shows added/removed/modified symbols | AST-level semantic diff |
|
|
@@ -106,6 +108,7 @@ Read a file with FNV-1a hash-annotated lines and range checksums. Supports direc
|
|
|
106
108
|
| Parameter | Type | Required | Description |
|
|
107
109
|
|-----------|------|----------|-------------|
|
|
108
110
|
| `path` | string | yes | File or directory path |
|
|
111
|
+
| `paths` | string[] | no | Array of file paths to read (batch mode) |
|
|
109
112
|
| `offset` | number | no | Start line, 1-indexed (default: 1) |
|
|
110
113
|
| `limit` | number | no | Max lines to return (default: 2000, 0 = all) |
|
|
111
114
|
| `plain` | boolean | no | Omit hashes, output `lineNum\|content` instead |
|
|
@@ -121,7 +124,7 @@ checksum: 1-50:f7e2a1b0
|
|
|
121
124
|
|
|
122
125
|
### edit_file
|
|
123
126
|
|
|
124
|
-
Edit using hash-verified anchors or text replacement. Returns
|
|
127
|
+
Edit using hash-verified anchors or text replacement. Returns diff + post-edit checksums for chaining edits.
|
|
125
128
|
|
|
126
129
|
| Parameter | Type | Required | Description |
|
|
127
130
|
|-----------|------|----------|-------------|
|
|
@@ -137,7 +140,8 @@ Edit operations (JSON array):
|
|
|
137
140
|
{"set_line": {"anchor": "ab.12", "new_text": "replacement line"}},
|
|
138
141
|
{"replace_lines": {"start_anchor": "ab.10", "end_anchor": "cd.15", "new_text": "..."}},
|
|
139
142
|
{"insert_after": {"anchor": "ab.20", "text": "inserted line"}},
|
|
140
|
-
{"replace": {"old_text": "
|
|
143
|
+
{"replace": {"old_text": "unique text", "new_text": "replacement"}},
|
|
144
|
+
{"replace": {"old_text": "find all", "new_text": "replace all", "all": true}}
|
|
141
145
|
]
|
|
142
146
|
```
|
|
143
147
|
|
|
@@ -152,17 +156,27 @@ Create a new file or overwrite an existing one. Creates parent directories autom
|
|
|
152
156
|
|
|
153
157
|
### grep_search
|
|
154
158
|
|
|
155
|
-
Search file contents using ripgrep
|
|
159
|
+
Search file contents using ripgrep. Three output modes: `content` (hash-annotated with checksums), `files` (paths only), `count` (match counts).
|
|
156
160
|
|
|
157
161
|
| Parameter | Type | Required | Description |
|
|
158
162
|
|-----------|------|----------|-------------|
|
|
159
|
-
| `pattern` | string | yes |
|
|
163
|
+
| `pattern` | string | yes | Search pattern (regex by default, literal if `literal:true`) |
|
|
160
164
|
| `path` | string | no | Directory or file to search (default: cwd) |
|
|
161
165
|
| `glob` | string | no | Glob filter, e.g. `"*.ts"` |
|
|
162
166
|
| `type` | string | no | File type filter, e.g. `"js"`, `"py"` |
|
|
167
|
+
| `output` | enum | no | Output format: `"content"` (default), `"files"`, `"count"` |
|
|
163
168
|
| `case_insensitive` | boolean | no | Ignore case |
|
|
164
|
-
| `
|
|
169
|
+
| `smart_case` | boolean | no | CI when lowercase, CS when uppercase (`-S`) |
|
|
170
|
+
| `literal` | boolean | no | Literal string search, no regex (`-F`) |
|
|
171
|
+
| `multiline` | boolean | no | Pattern can span multiple lines (`-U`) |
|
|
172
|
+
| `context` | number | no | Symmetric context lines around matches (`-C`) |
|
|
173
|
+
| `context_before` | number | no | Context lines BEFORE match (`-B`) |
|
|
174
|
+
| `context_after` | number | no | Context lines AFTER match (`-A`) |
|
|
165
175
|
| `limit` | number | no | Max matches per file (default: 100) |
|
|
176
|
+
| `total_limit` | number | no | Total match events across all files; multiline matches count as 1 (0 = unlimited) |
|
|
177
|
+
| `plain` | boolean | no | Omit hash tags, return `file:line:content` |
|
|
178
|
+
|
|
179
|
+
**Content mode** returns per-group checksums enabling direct `replace_lines` from grep results without intermediate `read_file`.
|
|
166
180
|
|
|
167
181
|
### outline
|
|
168
182
|
|
|
@@ -189,13 +203,16 @@ Returns a single-line confirmation or lists changed ranges.
|
|
|
189
203
|
|
|
190
204
|
### directory_tree
|
|
191
205
|
|
|
192
|
-
Compact directory tree with .gitignore support
|
|
206
|
+
Compact directory tree with root .gitignore support (path-based rules, negation, dir-only). Nested .gitignore files are not loaded.
|
|
193
207
|
|
|
194
208
|
| Parameter | Type | Required | Description |
|
|
195
209
|
|-----------|------|----------|-------------|
|
|
196
210
|
| `path` | string | yes | Directory path |
|
|
197
|
-
| `
|
|
198
|
-
| `
|
|
211
|
+
| `pattern` | string | no | Glob filter on names (e.g. `"*-mcp"`, `"*.mjs"`). Returns flat match list instead of tree |
|
|
212
|
+
| `type` | string | no | `"file"`, `"dir"`, or `"all"` (default). Like `find -type f/d` |
|
|
213
|
+
| `max_depth` | number | no | Max recursion depth (default: 3, or 20 in pattern mode) |
|
|
214
|
+
| `gitignore` | boolean | no | Respect root .gitignore patterns (default: true). Nested .gitignore not supported |
|
|
215
|
+
| `format` | string | no | `"compact"` = names only, no sizes, depth 1. `"full"` = default with sizes |
|
|
199
216
|
|
|
200
217
|
Skips `node_modules`, `.git`, `dist`, `build`, `__pycache__`, `.next`, `coverage` by default.
|
|
201
218
|
|
|
@@ -211,39 +228,43 @@ Returns: size, line count, modification time (absolute + relative), file type, b
|
|
|
211
228
|
|
|
212
229
|
## Hook
|
|
213
230
|
|
|
214
|
-
The unified
|
|
231
|
+
The unified hook (`hook.mjs`) handles four events:
|
|
232
|
+
|
|
233
|
+
### PreToolUse: Tool Redirect
|
|
215
234
|
|
|
216
|
-
|
|
235
|
+
Blocks built-in `Read`, `Edit`, `Write`, `Grep` on text files and redirects to hex-line equivalents. Binary files (images, PDFs, notebooks, archives, executables, fonts, media) are excluded.
|
|
217
236
|
|
|
218
|
-
|
|
237
|
+
### PreToolUse: Bash Redirect + Dangerous Blocker
|
|
219
238
|
|
|
220
|
-
|
|
239
|
+
Intercepts simple Bash commands (`cat`, `head`, `tail`, `ls`, `find`, `grep`, `sed -i`, etc.) and redirects to hex-line tools. Blocks dangerous commands (`rm -rf /`, `git push --force`, `git reset --hard`, `DROP TABLE`, `chmod 777`, `mkfs`, `dd`).
|
|
221
240
|
|
|
222
|
-
### RTK Output Filter
|
|
241
|
+
### PostToolUse: RTK Output Filter
|
|
223
242
|
|
|
224
243
|
Triggers on `Bash` tool output exceeding 50 lines. Pipeline:
|
|
225
244
|
|
|
226
245
|
1. **Detect command type** -- npm install, test, build, pip install, git verbose, or generic
|
|
227
|
-
2. **
|
|
228
|
-
3. **
|
|
229
|
-
4. **
|
|
230
|
-
5. **Truncate** -- keeps first 12 + last 12 lines, omits the middle
|
|
246
|
+
2. **Normalize** -- replaces UUIDs, timestamps, IPs, hex values, large numbers with placeholders
|
|
247
|
+
3. **Deduplicate** -- collapses identical normalized lines with `(xN)` counts
|
|
248
|
+
4. **Truncate** -- keeps first 15 + last 15 lines, omits the middle
|
|
231
249
|
|
|
232
250
|
Configuration constants in `hook.mjs`:
|
|
233
251
|
|
|
234
252
|
| Constant | Default | Purpose |
|
|
235
|
-
|
|
253
|
+
|----------|---------|--------|
|
|
236
254
|
| `LINE_THRESHOLD` | 50 | Minimum lines to trigger filtering |
|
|
237
|
-
| `
|
|
238
|
-
| `
|
|
239
|
-
|
|
255
|
+
| `HEAD_LINES` | 15 | Lines to keep from start |
|
|
256
|
+
| `TAIL_LINES` | 15 | Lines to keep from end |
|
|
257
|
+
|
|
258
|
+
### SessionStart: Tool Preferences
|
|
259
|
+
|
|
260
|
+
Injects hex-line tool preference list into agent context at session start.
|
|
240
261
|
|
|
241
262
|
## Architecture
|
|
242
263
|
|
|
243
264
|
```
|
|
244
265
|
hex-line-mcp/
|
|
245
|
-
server.mjs MCP server (stdio transport,
|
|
246
|
-
hook.mjs
|
|
266
|
+
server.mjs MCP server (stdio transport, 11 tools)
|
|
267
|
+
hook.mjs Unified hook (PreToolUse + PostToolUse + SessionStart)
|
|
247
268
|
package.json
|
|
248
269
|
lib/
|
|
249
270
|
hash.mjs FNV-1a hashing, 2-char tags, range checksums
|
|
@@ -252,6 +273,13 @@ hex-line-mcp/
|
|
|
252
273
|
search.mjs ripgrep wrapper with hash-annotated results
|
|
253
274
|
outline.mjs tree-sitter WASM AST outline
|
|
254
275
|
verify.mjs Range checksum verification
|
|
276
|
+
info.mjs File metadata (size, lines, mtime, type)
|
|
277
|
+
tree.mjs Directory tree with .gitignore support
|
|
278
|
+
changes.mjs Semantic git diff via AST
|
|
279
|
+
bulk-replace.mjs Multi-file search-and-replace
|
|
280
|
+
setup.mjs Hook installation for Claude/Gemini/Codex
|
|
281
|
+
format.mjs Output formatting utilities
|
|
282
|
+
coerce.mjs Parameter alias mapping
|
|
255
283
|
security.mjs Path validation, binary detection, size limits
|
|
256
284
|
normalize.mjs Output normalization, deduplication, truncation
|
|
257
285
|
```
|
|
@@ -283,16 +311,6 @@ FNV-1a accumulator over all line hashes in the range (little-endian byte feed).
|
|
|
283
311
|
- Write path validation (ancestor directory must exist)
|
|
284
312
|
- Directory restrictions delegated to Claude Code sandbox
|
|
285
313
|
|
|
286
|
-
## Differences from trueline-mcp
|
|
287
|
-
|
|
288
|
-
| Aspect | hex-line-mcp | trueline-mcp |
|
|
289
|
-
|--------|---------------|--------------|
|
|
290
|
-
| Hash algorithm | FNV-1a (pure JS, zero dependencies) | xxHash (native addon) |
|
|
291
|
-
| Diff output | Compact unified diff via `diff` npm package | Custom diff implementation |
|
|
292
|
-
| Hook | Unified `hook.mjs` (reminder + RTK filter) | Separate hook scripts |
|
|
293
|
-
| Path security | Canonicalization + binary detection, no ALLOWED_DIRS | Explicit ALLOWED_DIRS allowlist |
|
|
294
|
-
| Transport | stdio only | stdio |
|
|
295
|
-
| Outline | tree-sitter WASM (15+ languages) | tree-sitter WASM |
|
|
296
314
|
|
|
297
315
|
## FAQ
|
|
298
316
|
|
|
@@ -320,7 +338,7 @@ Outline works on code files only (15+ languages via tree-sitter WASM). For markd
|
|
|
320
338
|
<details>
|
|
321
339
|
<summary><b>How does the RTK filter reduce tokens?</b></summary>
|
|
322
340
|
|
|
323
|
-
The PostToolUse hook normalizes Bash output (replaces UUIDs, timestamps, IPs with placeholders), deduplicates identical lines, and truncates to first
|
|
341
|
+
The PostToolUse hook normalizes Bash output (replaces UUIDs, timestamps, IPs with placeholders), deduplicates identical lines, and truncates to first 15 + last 15 lines. Average savings: 45% (flat) / 52% (weighted) across 18 benchmark scenarios.
|
|
324
342
|
|
|
325
343
|
</details>
|
|
326
344
|
|
|
@@ -331,6 +349,14 @@ Yes. Remove the PreToolUse hook from `.claude/settings.local.json`. The MCP tool
|
|
|
331
349
|
|
|
332
350
|
</details>
|
|
333
351
|
|
|
352
|
+
## Hex Family
|
|
353
|
+
|
|
354
|
+
| Package | Purpose | npm |
|
|
355
|
+
|---------|---------|-----|
|
|
356
|
+
| [hex-line-mcp](https://www.npmjs.com/package/@levnikolaevich/hex-line-mcp) | Local file editing with hash verification + hooks | [](https://www.npmjs.com/package/@levnikolaevich/hex-line-mcp) |
|
|
357
|
+
| [hex-ssh-mcp](https://www.npmjs.com/package/@levnikolaevich/hex-ssh-mcp) | Remote file editing over SSH | [](https://www.npmjs.com/package/@levnikolaevich/hex-ssh-mcp) |
|
|
358
|
+
| [hex-graph-mcp](https://www.npmjs.com/package/@levnikolaevich/hex-graph-mcp) | Code knowledge graph with AST indexing | [](https://www.npmjs.com/package/@levnikolaevich/hex-graph-mcp) |
|
|
359
|
+
|
|
334
360
|
## License
|
|
335
361
|
|
|
336
362
|
MIT
|
package/hook.mjs
CHANGED
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
* Exit 2 = block (PreToolUse) or feedback via stderr (PostToolUse)
|
|
24
24
|
*/
|
|
25
25
|
|
|
26
|
-
import {
|
|
26
|
+
import { normalizeOutput } from "./lib/normalize.mjs";
|
|
27
27
|
import { readFileSync } from "node:fs";
|
|
28
28
|
import { resolve } from "node:path";
|
|
29
29
|
import { homedir } from "node:os";
|
|
@@ -39,17 +39,31 @@ const BINARY_EXT = new Set([
|
|
|
39
39
|
".ttf", ".otf", ".woff", ".woff2",
|
|
40
40
|
]);
|
|
41
41
|
|
|
42
|
+
const REVERSE_TOOL_HINTS = {
|
|
43
|
+
"mcp__hex-line__read_file": "Read (file_path, offset, limit)",
|
|
44
|
+
"mcp__hex-line__edit_file": "Edit (file_path, old_string, new_string)",
|
|
45
|
+
"mcp__hex-line__write_file": "Write (file_path, content)",
|
|
46
|
+
"mcp__hex-line__grep_search": "Grep (pattern, path)",
|
|
47
|
+
"mcp__hex-line__directory_tree": "Glob (pattern) or Bash(ls)",
|
|
48
|
+
"mcp__hex-line__get_file_info": "Bash(stat/wc)",
|
|
49
|
+
"mcp__hex-line__outline": "Read with offset/limit",
|
|
50
|
+
"mcp__hex-line__verify": "Read the file again with Read",
|
|
51
|
+
"mcp__hex-line__changes": "Bash(git diff)",
|
|
52
|
+
"mcp__hex-line__bulk_replace": "Edit for each file",
|
|
53
|
+
"mcp__hex-line__setup_hooks": "Not available (hex-line disabled)",
|
|
54
|
+
};
|
|
55
|
+
|
|
42
56
|
const TOOL_HINTS = {
|
|
43
57
|
Read: "mcp__hex-line__read_file (not Read). For writing: write_file (no prior Read needed)",
|
|
44
58
|
Edit: "mcp__hex-line__edit_file (not Edit, not sed -i). read_file first for hashes",
|
|
45
59
|
Write: "mcp__hex-line__write_file (not Write). No prior Read needed",
|
|
46
|
-
Grep: "mcp__hex-line__grep_search (not Grep). Params:
|
|
60
|
+
Grep: "mcp__hex-line__grep_search (not Grep). Params: output, literal, context_before, context_after, multiline",
|
|
47
61
|
cat: "mcp__hex-line__read_file (not cat/head/tail/less/more)",
|
|
48
62
|
head: "mcp__hex-line__read_file with limit param (not head)",
|
|
49
63
|
tail: "mcp__hex-line__read_file with offset param (not tail)",
|
|
50
64
|
ls: "mcp__hex-line__directory_tree with pattern param (not ls/find/tree). E.g. pattern='*-mcp' type='dir'",
|
|
51
65
|
stat: "mcp__hex-line__get_file_info (not stat/wc/file)",
|
|
52
|
-
grep: "mcp__hex-line__grep_search (not grep/rg). Params:
|
|
66
|
+
grep: "mcp__hex-line__grep_search (not grep/rg). Params: output, literal, context_before, context_after, multiline",
|
|
53
67
|
sed: "mcp__hex-line__edit_file (not sed -i). read_file first for hashes",
|
|
54
68
|
diff: "mcp__hex-line__changes (not diff). Git-based semantic diff",
|
|
55
69
|
outline: "mcp__hex-line__outline (before reading large code files)",
|
|
@@ -138,6 +152,39 @@ function detectCommandType(cmd) {
|
|
|
138
152
|
return "generic";
|
|
139
153
|
}
|
|
140
154
|
|
|
155
|
+
/** Cache: null = not computed yet */
|
|
156
|
+
let _hexLineDisabled = null;
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Check if hex-line MCP is disabled for the current project.
|
|
160
|
+
* Reads ~/.claude.json → projects.{cwd}.disabledMcpServers.
|
|
161
|
+
* Fail-open: returns false on any error.
|
|
162
|
+
*/
|
|
163
|
+
function isHexLineDisabled(configPath) {
|
|
164
|
+
if (_hexLineDisabled !== null) return _hexLineDisabled;
|
|
165
|
+
_hexLineDisabled = false;
|
|
166
|
+
try {
|
|
167
|
+
const p = configPath || resolve(homedir(), ".claude.json");
|
|
168
|
+
const claudeJson = JSON.parse(readFileSync(p, "utf-8"));
|
|
169
|
+
const projects = claudeJson.projects;
|
|
170
|
+
if (!projects || typeof projects !== "object") return _hexLineDisabled;
|
|
171
|
+
const cwd = process.cwd().replace(/\\/g, "/").replace(/\/$/, "").toLowerCase();
|
|
172
|
+
for (const [path, config] of Object.entries(projects)) {
|
|
173
|
+
if (path.replace(/\\/g, "/").replace(/\/$/, "").toLowerCase() === cwd) {
|
|
174
|
+
const disabled = config.disabledMcpServers;
|
|
175
|
+
if (Array.isArray(disabled) && disabled.includes("hex-line")) {
|
|
176
|
+
_hexLineDisabled = true;
|
|
177
|
+
}
|
|
178
|
+
break;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
} catch { /* fail open */ }
|
|
182
|
+
return _hexLineDisabled;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** Reset cache (for testing). */
|
|
186
|
+
function _resetHexLineDisabledCache() { _hexLineDisabled = null; }
|
|
187
|
+
|
|
141
188
|
function block(reason, context) {
|
|
142
189
|
const output = {
|
|
143
190
|
hookSpecificOutput: {
|
|
@@ -237,6 +284,28 @@ function handlePreToolUse(data) {
|
|
|
237
284
|
process.exit(0);
|
|
238
285
|
}
|
|
239
286
|
|
|
287
|
+
// ---- PreToolUse REVERSE handler (hex-line disabled) ----
|
|
288
|
+
|
|
289
|
+
function handlePreToolUseReverse(data) {
|
|
290
|
+
const toolName = data.tool_name || "";
|
|
291
|
+
|
|
292
|
+
// Agent tries hex-line tool that's disabled → redirect to built-in
|
|
293
|
+
if (toolName.startsWith("mcp__hex-line__")) {
|
|
294
|
+
const builtIn = REVERSE_TOOL_HINTS[toolName];
|
|
295
|
+
if (builtIn) {
|
|
296
|
+
const target = builtIn.split(" ")[0];
|
|
297
|
+
block(
|
|
298
|
+
`hex-line is disabled in this project. Use ${target}`,
|
|
299
|
+
`hex-line disabled. Use built-in: ${builtIn}`
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
block("hex-line is disabled in this project", "Disabled via project settings");
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// All built-in tools — approve silently
|
|
306
|
+
process.exit(0);
|
|
307
|
+
}
|
|
308
|
+
|
|
240
309
|
// ---- PostToolUse handler ----
|
|
241
310
|
|
|
242
311
|
function handlePostToolUse(data) {
|
|
@@ -266,10 +335,8 @@ function handlePostToolUse(data) {
|
|
|
266
335
|
|
|
267
336
|
const type = detectCommandType(command);
|
|
268
337
|
|
|
269
|
-
// Pipeline: deduplicate -> smart truncate
|
|
270
|
-
const
|
|
271
|
-
const dedupedText = deduped.join("\n");
|
|
272
|
-
const filtered = smartTruncate(dedupedText, HEAD_LINES, TAIL_LINES);
|
|
338
|
+
// Pipeline: normalize -> deduplicate -> smart truncate
|
|
339
|
+
const filtered = normalizeOutput(lines.join("\n"), { headLines: HEAD_LINES, tailLines: TAIL_LINES });
|
|
273
340
|
const filteredCount = filtered.split("\n").length;
|
|
274
341
|
|
|
275
342
|
const header = `RTK FILTERED: ${type} (${originalCount} lines -> ${filteredCount} lines)`;
|
|
@@ -331,21 +398,36 @@ function handleSessionStart() {
|
|
|
331
398
|
}
|
|
332
399
|
|
|
333
400
|
// ---- Main: read stdin, route by hook_event_name ----
|
|
401
|
+
// Guard: only run when executed directly, not when imported for testing
|
|
334
402
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
process.stdin.on("
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
}
|
|
403
|
+
import { fileURLToPath } from "node:url";
|
|
404
|
+
|
|
405
|
+
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
406
|
+
let input = "";
|
|
407
|
+
process.stdin.on("data", (chunk) => {
|
|
408
|
+
input += chunk;
|
|
409
|
+
});
|
|
410
|
+
process.stdin.on("end", () => {
|
|
411
|
+
try {
|
|
412
|
+
const data = JSON.parse(input);
|
|
413
|
+
const event = data.hook_event_name || "";
|
|
414
|
+
|
|
415
|
+
if (isHexLineDisabled()) {
|
|
416
|
+
// REVERSE MODE: block hex-line calls, approve everything else
|
|
417
|
+
if (event === "PreToolUse") handlePreToolUseReverse(data);
|
|
418
|
+
process.exit(0); // SessionStart, PostToolUse — silent exit
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// NORMAL MODE
|
|
422
|
+
if (event === "SessionStart") handleSessionStart();
|
|
423
|
+
else if (event === "PreToolUse") handlePreToolUse(data);
|
|
424
|
+
else if (event === "PostToolUse") handlePostToolUse(data);
|
|
425
|
+
else process.exit(0);
|
|
426
|
+
} catch {
|
|
427
|
+
process.exit(0);
|
|
428
|
+
}
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// ---- Exports for testing ----
|
|
433
|
+
export { isHexLineDisabled, _resetHexLineDisabledCache };
|
package/lib/bulk-replace.mjs
CHANGED
|
@@ -1,16 +1,18 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { writeFileSync } from "node:fs";
|
|
2
|
+
import { execFileSync } from "node:child_process";
|
|
3
3
|
import { resolve } from "node:path";
|
|
4
4
|
import { simpleDiff } from "./edit.mjs";
|
|
5
|
+
import { normalizePath } from "./security.mjs";
|
|
6
|
+
import { readText, MAX_OUTPUT_CHARS } from "./format.mjs";
|
|
5
7
|
|
|
6
8
|
export function bulkReplace(rootDir, globPattern, replacements, opts = {}) {
|
|
7
9
|
const { dryRun = false, maxFiles = 100 } = opts;
|
|
8
|
-
const abs = resolve(rootDir);
|
|
10
|
+
const abs = resolve(normalizePath(rootDir));
|
|
9
11
|
|
|
10
12
|
// Find files via ripgrep (respects .gitignore)
|
|
11
13
|
let files;
|
|
12
14
|
try {
|
|
13
|
-
const rgOut =
|
|
15
|
+
const rgOut = execFileSync("rg", ["--files", "-g", globPattern, abs], { encoding: "utf-8", timeout: 10000 });
|
|
14
16
|
files = rgOut.trim().split("\n").filter(Boolean);
|
|
15
17
|
} catch (e) {
|
|
16
18
|
if (e.status === 1) return "No files matched the glob pattern.";
|
|
@@ -23,12 +25,12 @@ export function bulkReplace(rootDir, globPattern, replacements, opts = {}) {
|
|
|
23
25
|
|
|
24
26
|
const results = [];
|
|
25
27
|
let changed = 0, skipped = 0, errors = 0;
|
|
26
|
-
const MAX_OUTPUT =
|
|
28
|
+
const MAX_OUTPUT = MAX_OUTPUT_CHARS;
|
|
27
29
|
let totalChars = 0;
|
|
28
30
|
|
|
29
31
|
for (const file of files) {
|
|
30
32
|
try {
|
|
31
|
-
const original =
|
|
33
|
+
const original = readText(file);
|
|
32
34
|
let content = original;
|
|
33
35
|
|
|
34
36
|
for (const { old: oldText, new: newText } of replacements) {
|
package/lib/changes.mjs
CHANGED
|
@@ -6,10 +6,11 @@
|
|
|
6
6
|
* without temp files.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
9
|
+
import { execFileSync } from "node:child_process";
|
|
10
|
+
import { statSync } from "node:fs";
|
|
11
11
|
import { extname } from "node:path";
|
|
12
12
|
import { validatePath } from "./security.mjs";
|
|
13
|
+
import { readText } from "./format.mjs";
|
|
13
14
|
import { outlineFromContent } from "./outline.mjs";
|
|
14
15
|
|
|
15
16
|
/**
|
|
@@ -50,7 +51,7 @@ function toSymbolMap(entries) {
|
|
|
50
51
|
* Get relative path from git root for `git show`.
|
|
51
52
|
*/
|
|
52
53
|
function gitRelativePath(absPath) {
|
|
53
|
-
const root =
|
|
54
|
+
const root = execFileSync("git", ["rev-parse", "--show-toplevel"], {
|
|
54
55
|
cwd: absPath.replace(/[/\\][^/\\]+$/, ""),
|
|
55
56
|
encoding: "utf-8",
|
|
56
57
|
timeout: 5000,
|
|
@@ -79,7 +80,7 @@ export async function fileChanges(filePath, compareAgainst = "HEAD") {
|
|
|
79
80
|
// Directory: return git diff --stat (compact file list, no content reads)
|
|
80
81
|
if (statSync(real).isDirectory()) {
|
|
81
82
|
try {
|
|
82
|
-
const stat =
|
|
83
|
+
const stat = execFileSync("git", ["diff", "--stat", compareAgainst, "--", "."], {
|
|
83
84
|
cwd: real,
|
|
84
85
|
encoding: "utf-8",
|
|
85
86
|
timeout: 10000,
|
|
@@ -94,7 +95,7 @@ export async function fileChanges(filePath, compareAgainst = "HEAD") {
|
|
|
94
95
|
const ext = extname(real).toLowerCase();
|
|
95
96
|
|
|
96
97
|
// Check if outline supports this extension
|
|
97
|
-
const currentContent =
|
|
98
|
+
const currentContent = readText(real);
|
|
98
99
|
const currentResult = await outlineFromContent(currentContent, ext);
|
|
99
100
|
if (!currentResult) {
|
|
100
101
|
return `Cannot outline ${ext} files. Supported: .js .mjs .ts .py .go .rs .java .c .cpp .cs .rb .php .kt .swift .sh .bash`;
|
|
@@ -104,7 +105,7 @@ export async function fileChanges(filePath, compareAgainst = "HEAD") {
|
|
|
104
105
|
const relPath = gitRelativePath(real);
|
|
105
106
|
let gitContent;
|
|
106
107
|
try {
|
|
107
|
-
gitContent =
|
|
108
|
+
gitContent = execFileSync("git", ["show", `${compareAgainst}:${relPath}`], {
|
|
108
109
|
cwd: real.replace(/[/\\][^/\\]+$/, ""),
|
|
109
110
|
encoding: "utf-8",
|
|
110
111
|
timeout: 5000,
|
package/lib/coerce.mjs
CHANGED
|
@@ -12,12 +12,6 @@ const ALIASES = {
|
|
|
12
12
|
// grep_search
|
|
13
13
|
query: "pattern",
|
|
14
14
|
search: "pattern",
|
|
15
|
-
max_results: "limit",
|
|
16
|
-
maxResults: "limit",
|
|
17
|
-
maxMatches: "limit",
|
|
18
|
-
max_matches: "limit",
|
|
19
|
-
contextLines: "context",
|
|
20
|
-
context_lines: "context",
|
|
21
15
|
ignoreCase: "case_insensitive",
|
|
22
16
|
ignore_case: "case_insensitive",
|
|
23
17
|
|