@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 +62 -7
- package/dist/hook.mjs +428 -0
- package/dist/server.mjs +6615 -0
- package/output-style.md +16 -2
- package/package.json +16 -12
- package/benchmark/atomic.mjs +0 -502
- package/benchmark/graph.mjs +0 -80
- package/benchmark/index.mjs +0 -144
- package/benchmark/workflows.mjs +0 -259
- package/hook.mjs +0 -466
- package/lib/benchmark-helpers.mjs +0 -541
- package/lib/bulk-replace.mjs +0 -65
- package/lib/changes.mjs +0 -176
- package/lib/coerce.mjs +0 -2
- package/lib/edit.mjs +0 -400
- package/lib/format.mjs +0 -138
- package/lib/graph-enrich.mjs +0 -226
- package/lib/hash.mjs +0 -109
- package/lib/info.mjs +0 -91
- package/lib/normalize.mjs +0 -106
- package/lib/outline.mjs +0 -201
- package/lib/read.mjs +0 -136
- package/lib/search.mjs +0 -269
- package/lib/security.mjs +0 -112
- package/lib/setup.mjs +0 -275
- package/lib/tree.mjs +0 -236
- package/lib/update-check.mjs +0 -56
- package/lib/verify.mjs +0 -55
- package/server.mjs +0 -381
package/hook.mjs
DELETED
|
@@ -1,466 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* Unified hook for hex-line-mcp.
|
|
4
|
-
*
|
|
5
|
-
* Handles three events:
|
|
6
|
-
*
|
|
7
|
-
* PreToolUse:
|
|
8
|
-
* - Tool redirect: blocks Read/Edit/Write/Grep for text files,
|
|
9
|
-
* redirecting to hex-line MCP equivalents.
|
|
10
|
-
* - Bash redirect: blocks simple cat/head/tail/ls/grep/sed/diff
|
|
11
|
-
* commands, redirecting to hex-line MCP equivalents.
|
|
12
|
-
* - Dangerous command blocker: blocks rm -rf /, force push,
|
|
13
|
-
* hard reset, DROP, chmod 777, mkfs, dd, etc.
|
|
14
|
-
*
|
|
15
|
-
* PostToolUse:
|
|
16
|
-
* - RTK output filter: compresses verbose Bash output
|
|
17
|
-
* (npm install, test, build, pip, git). Stderr shown to
|
|
18
|
-
* Claude as feedback.
|
|
19
|
-
*
|
|
20
|
-
* SessionStart:
|
|
21
|
-
* - Injects tool preference list into agent context.
|
|
22
|
-
*
|
|
23
|
-
* Exit 0 = approve / no feedback / systemMessage
|
|
24
|
-
* Exit 2 = block (PreToolUse) or stderr feedback (PostToolUse)
|
|
25
|
-
*/
|
|
26
|
-
|
|
27
|
-
import { normalizeOutput } from "./lib/normalize.mjs";
|
|
28
|
-
import { readFileSync } from "node:fs";
|
|
29
|
-
import { resolve } from "node:path";
|
|
30
|
-
import { homedir } from "node:os";
|
|
31
|
-
|
|
32
|
-
// ---- Constants ----
|
|
33
|
-
|
|
34
|
-
const BINARY_EXT = new Set([
|
|
35
|
-
".png", ".jpg", ".jpeg", ".gif", ".bmp", ".webp", ".svg", ".ico",
|
|
36
|
-
".pdf", ".ipynb",
|
|
37
|
-
".zip", ".tar", ".gz", ".7z", ".rar",
|
|
38
|
-
".exe", ".dll", ".so", ".dylib", ".wasm",
|
|
39
|
-
".mp3", ".mp4", ".wav", ".avi", ".mkv",
|
|
40
|
-
".ttf", ".otf", ".woff", ".woff2",
|
|
41
|
-
]);
|
|
42
|
-
|
|
43
|
-
const REVERSE_TOOL_HINTS = {
|
|
44
|
-
"mcp__hex-line__read_file": "Read (file_path, offset, limit)",
|
|
45
|
-
"mcp__hex-line__edit_file": "Edit (anchor-based hash edits only)",
|
|
46
|
-
"mcp__hex-line__write_file": "Write (file_path, content)",
|
|
47
|
-
"mcp__hex-line__grep_search": "Grep (pattern, path)",
|
|
48
|
-
"mcp__hex-line__directory_tree": "Glob (pattern) or Bash(ls)",
|
|
49
|
-
"mcp__hex-line__get_file_info": "Bash(stat/wc)",
|
|
50
|
-
"mcp__hex-line__outline": "Read with offset/limit",
|
|
51
|
-
"mcp__hex-line__verify": "Read the file again with Read",
|
|
52
|
-
"mcp__hex-line__changes": "Bash(git diff)",
|
|
53
|
-
"mcp__hex-line__bulk_replace": "Edit (text rename/refactor across files)",
|
|
54
|
-
"mcp__hex-line__setup_hooks": "Not available (hex-line disabled)",
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
const TOOL_HINTS = {
|
|
58
|
-
Read: "mcp__hex-line__read_file (not Read). For writing: write_file (no prior Read needed)",
|
|
59
|
-
Edit: "mcp__hex-line__edit_file for hash-verified edits (not Edit). For text rename use bulk_replace",
|
|
60
|
-
Write: "mcp__hex-line__write_file (not Write). No prior Read needed",
|
|
61
|
-
Grep: "mcp__hex-line__grep_search (not Grep). Params: output, literal, context_before, context_after, multiline",
|
|
62
|
-
cat: "mcp__hex-line__read_file (not cat/head/tail/less/more)",
|
|
63
|
-
head: "mcp__hex-line__read_file with limit param (not head)",
|
|
64
|
-
tail: "mcp__hex-line__read_file with offset param (not tail)",
|
|
65
|
-
ls: "mcp__hex-line__directory_tree with pattern param (not ls/find/tree). E.g. pattern='*-mcp' type='dir'",
|
|
66
|
-
stat: "mcp__hex-line__get_file_info (not stat/wc/file)",
|
|
67
|
-
grep: "mcp__hex-line__grep_search (not grep/rg). Params: output, literal, context_before, context_after, multiline",
|
|
68
|
-
sed: "mcp__hex-line__edit_file for hash edits, or mcp__hex-line__bulk_replace for text rename (not sed -i)",
|
|
69
|
-
diff: "mcp__hex-line__changes (not diff). Git-based semantic diff",
|
|
70
|
-
outline: "mcp__hex-line__outline (before reading large code files)",
|
|
71
|
-
verify: "mcp__hex-line__verify (staleness check without re-read)",
|
|
72
|
-
changes: "mcp__hex-line__changes (semantic AST diff)",
|
|
73
|
-
bulk: "mcp__hex-line__bulk_replace (multi-file search-replace)",
|
|
74
|
-
setup: "mcp__hex-line__setup_hooks (configure hooks for agents)",
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
const BASH_REDIRECTS = [
|
|
78
|
-
{ regex: /^cat\s+\S+/, key: "cat" },
|
|
79
|
-
{ regex: /^head\s+/, key: "head" },
|
|
80
|
-
{ regex: /^tail\s+(?!-[fF])/, key: "tail" },
|
|
81
|
-
{ regex: /^(less|more)\s+/, key: "cat" },
|
|
82
|
-
{ regex: /^ls\s+-\S*R(\s|$)/, key: "ls" }, // ls -R, ls -laR (recursive only)
|
|
83
|
-
{ regex: /^dir\s+\/[sS](\s|$)/, key: "ls" }, // dir /s, dir /S (recursive only)
|
|
84
|
-
{ regex: /^tree\s+/, key: "ls" },
|
|
85
|
-
{ regex: /^find\s+/, key: "ls" },
|
|
86
|
-
{ regex: /^(stat|wc)\s+/, key: "stat" },
|
|
87
|
-
{ regex: /^(grep|rg)\s+/, key: "grep" },
|
|
88
|
-
{ regex: /^sed\s+-i/, key: "sed" },
|
|
89
|
-
];
|
|
90
|
-
|
|
91
|
-
const TOOL_REDIRECT_MAP = {
|
|
92
|
-
Read: "Read",
|
|
93
|
-
Edit: "Edit",
|
|
94
|
-
Write: "Write",
|
|
95
|
-
Grep: "Grep",
|
|
96
|
-
};
|
|
97
|
-
|
|
98
|
-
const DANGEROUS_PATTERNS = [
|
|
99
|
-
{
|
|
100
|
-
regex: /rm\s+(-[rf]+\s+)*[/~]/,
|
|
101
|
-
reason: "rm -rf on root/home directory",
|
|
102
|
-
},
|
|
103
|
-
{
|
|
104
|
-
regex: /git\s+push\s+(-f|--force)/,
|
|
105
|
-
reason: "force push can overwrite remote history",
|
|
106
|
-
},
|
|
107
|
-
{
|
|
108
|
-
regex: /git\s+reset\s+--hard/,
|
|
109
|
-
reason: "hard reset discards uncommitted changes",
|
|
110
|
-
},
|
|
111
|
-
{
|
|
112
|
-
regex: /DROP\s+(TABLE|DATABASE)/i,
|
|
113
|
-
reason: "DROP destroys data permanently",
|
|
114
|
-
},
|
|
115
|
-
{
|
|
116
|
-
regex: /chmod\s+777/,
|
|
117
|
-
reason: "chmod 777 removes all access restrictions",
|
|
118
|
-
},
|
|
119
|
-
{
|
|
120
|
-
regex: /mkfs/,
|
|
121
|
-
reason: "filesystem format destroys all data",
|
|
122
|
-
},
|
|
123
|
-
{
|
|
124
|
-
regex: /dd\s+if=\/dev\/zero/,
|
|
125
|
-
reason: "direct disk write destroys data",
|
|
126
|
-
},
|
|
127
|
-
];
|
|
128
|
-
|
|
129
|
-
const COMPOUND_OPERATORS = /[|]|>>?|&&|\|\||;/;
|
|
130
|
-
|
|
131
|
-
const CMD_PATTERNS = [
|
|
132
|
-
[/npm (install|ci|update|add)/i, "npm-install"],
|
|
133
|
-
[/npm test|jest|vitest|mocha|pytest|cargo test/i, "test"],
|
|
134
|
-
[/npm run build|tsc|webpack|vite build|cargo build/i, "build"],
|
|
135
|
-
[/pip install/i, "pip-install"],
|
|
136
|
-
[/git (log|diff|status)/i, "git"],
|
|
137
|
-
];
|
|
138
|
-
|
|
139
|
-
const LINE_THRESHOLD = 50;
|
|
140
|
-
const HEAD_LINES = 15;
|
|
141
|
-
const TAIL_LINES = 15;
|
|
142
|
-
|
|
143
|
-
// ---- Helpers ----
|
|
144
|
-
|
|
145
|
-
function extOf(filePath) {
|
|
146
|
-
const dot = filePath.lastIndexOf(".");
|
|
147
|
-
return dot !== -1 ? filePath.slice(dot).toLowerCase() : "";
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
function detectCommandType(cmd) {
|
|
151
|
-
for (const [re, type] of CMD_PATTERNS) {
|
|
152
|
-
if (re.test(cmd)) return type;
|
|
153
|
-
}
|
|
154
|
-
return "generic";
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
function extractBashText(response) {
|
|
158
|
-
if (typeof response === "string") return response;
|
|
159
|
-
if (response && typeof response === "object") {
|
|
160
|
-
// Combine stdout + stderr in stable order
|
|
161
|
-
const parts = [];
|
|
162
|
-
if (response.stdout) parts.push(response.stdout);
|
|
163
|
-
if (response.stderr) parts.push(response.stderr);
|
|
164
|
-
return parts.join("\n") || "";
|
|
165
|
-
}
|
|
166
|
-
return ""; // unknown shape \u2192 fail open
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
/** Cache: null = not computed yet */
|
|
170
|
-
let _hexLineDisabled = null;
|
|
171
|
-
|
|
172
|
-
/**
|
|
173
|
-
* Check if hex-line MCP is disabled for the current project.
|
|
174
|
-
* Reads ~/.claude.json → projects.{cwd}.disabledMcpServers.
|
|
175
|
-
* Fail-open: returns false on any error.
|
|
176
|
-
*/
|
|
177
|
-
function isHexLineDisabled(configPath) {
|
|
178
|
-
if (_hexLineDisabled !== null) return _hexLineDisabled;
|
|
179
|
-
_hexLineDisabled = false;
|
|
180
|
-
try {
|
|
181
|
-
const p = configPath || resolve(homedir(), ".claude.json");
|
|
182
|
-
const claudeJson = JSON.parse(readFileSync(p, "utf-8"));
|
|
183
|
-
const projects = claudeJson.projects;
|
|
184
|
-
if (!projects || typeof projects !== "object") return _hexLineDisabled;
|
|
185
|
-
const cwd = process.cwd().replace(/\\/g, "/").replace(/\/$/, "").toLowerCase();
|
|
186
|
-
for (const [path, config] of Object.entries(projects)) {
|
|
187
|
-
if (path.replace(/\\/g, "/").replace(/\/$/, "").toLowerCase() === cwd) {
|
|
188
|
-
const disabled = config.disabledMcpServers;
|
|
189
|
-
if (Array.isArray(disabled) && disabled.includes("hex-line")) {
|
|
190
|
-
_hexLineDisabled = true;
|
|
191
|
-
}
|
|
192
|
-
break;
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
} catch { /* fail open */ }
|
|
196
|
-
return _hexLineDisabled;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
/** Reset cache (for testing). */
|
|
200
|
-
function _resetHexLineDisabledCache() { _hexLineDisabled = null; }
|
|
201
|
-
|
|
202
|
-
function block(reason, context) {
|
|
203
|
-
const output = {
|
|
204
|
-
hookSpecificOutput: {
|
|
205
|
-
hookEventName: "PreToolUse",
|
|
206
|
-
permissionDecision: "deny",
|
|
207
|
-
permissionDecisionReason: reason,
|
|
208
|
-
}
|
|
209
|
-
};
|
|
210
|
-
if (context) output.hookSpecificOutput.additionalContext = context;
|
|
211
|
-
process.stdout.write(JSON.stringify(output));
|
|
212
|
-
process.exit(2);
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
// ---- PreToolUse handler ----
|
|
216
|
-
|
|
217
|
-
function handlePreToolUse(data) {
|
|
218
|
-
const toolName = data.tool_name || "";
|
|
219
|
-
const toolInput = data.tool_input || {};
|
|
220
|
-
|
|
221
|
-
// Already using hex-line - approve silently
|
|
222
|
-
if (toolName.startsWith("mcp__hex-line__")) {
|
|
223
|
-
process.exit(0);
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
// Tool redirect: Read / Edit / Write / Grep
|
|
227
|
-
const hintKey = TOOL_REDIRECT_MAP[toolName];
|
|
228
|
-
if (hintKey) {
|
|
229
|
-
const filePath = toolInput.file_path || toolInput.path || "";
|
|
230
|
-
|
|
231
|
-
// Skip binary extensions
|
|
232
|
-
if (BINARY_EXT.has(extOf(filePath))) {
|
|
233
|
-
process.exit(0);
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
// Skip plan-mode and system paths (normalize backslashes for Windows)
|
|
237
|
-
const normalPath = filePath.replace(/\\/g, "/");
|
|
238
|
-
if (normalPath.includes(".claude/plans/") || normalPath.includes("AppData")) {
|
|
239
|
-
process.exit(0);
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
// Skip Claude config files (settings.json, settings.local.json)
|
|
243
|
-
const ALLOWED_CONFIGS = new Set(["settings.json", "settings.local.json"]);
|
|
244
|
-
const fileName = normalPath.split("/").pop();
|
|
245
|
-
if (ALLOWED_CONFIGS.has(fileName)) {
|
|
246
|
-
let candidate = filePath;
|
|
247
|
-
if (candidate.startsWith("~/")) {
|
|
248
|
-
candidate = homedir().replace(/\\/g, "/") + candidate.slice(1);
|
|
249
|
-
}
|
|
250
|
-
const absPath = resolve(process.cwd(), candidate).replace(/\\/g, "/");
|
|
251
|
-
const projectClaude = resolve(process.cwd(), ".claude").replace(/\\/g, "/") + "/";
|
|
252
|
-
const globalClaude = resolve(homedir(), ".claude").replace(/\\/g, "/") + "/";
|
|
253
|
-
const cmp = process.platform === "win32"
|
|
254
|
-
? (a, b) => a.toLowerCase().startsWith(b.toLowerCase())
|
|
255
|
-
: (a, b) => a.startsWith(b);
|
|
256
|
-
if (cmp(absPath, projectClaude) || cmp(absPath, globalClaude)) {
|
|
257
|
-
process.exit(0);
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
// Block with redirect — include extracted path for instant retry
|
|
262
|
-
const hint = TOOL_HINTS[hintKey];
|
|
263
|
-
const toolName2 = hint.split(" (")[0];
|
|
264
|
-
const pathNote = filePath ? ` with path="${filePath}"` : "";
|
|
265
|
-
block(`Use ${toolName2}${pathNote}`, hint);
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
// Bash tool checks
|
|
269
|
-
if (toolName === "Bash") {
|
|
270
|
-
const command = (toolInput.command || "").trim();
|
|
271
|
-
|
|
272
|
-
// User-confirmed bypass
|
|
273
|
-
if (command.includes("# hex-confirmed")) {
|
|
274
|
-
process.exit(0);
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
// Strip heredoc bodies to avoid false positives on data content
|
|
278
|
-
// e.g. gh api -f body="$(cat <<'EOF'\n...rm -rf...\nEOF)"
|
|
279
|
-
const cmdCheck = command.replace(/<<['"]?(\w+)['"]?\s*\n[\s\S]*?\n\1/g, "");
|
|
280
|
-
|
|
281
|
-
// Dangerous command blocker
|
|
282
|
-
for (const { regex, reason } of DANGEROUS_PATTERNS) {
|
|
283
|
-
if (regex.test(cmdCheck)) {
|
|
284
|
-
block(
|
|
285
|
-
`DANGEROUS: ${reason}. Ask user to confirm, then retry with: # hex-confirmed`,
|
|
286
|
-
`Original command: ${command.slice(0, 100)}`
|
|
287
|
-
);
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
// Compound commands: check first command in pipe before skipping
|
|
292
|
-
if (COMPOUND_OPERATORS.test(command)) {
|
|
293
|
-
const firstCmd = command.split(/\s*[|;&>]\s*/)[0].trim();
|
|
294
|
-
for (const { regex, key } of BASH_REDIRECTS) {
|
|
295
|
-
if (regex.test(firstCmd)) {
|
|
296
|
-
const hint = TOOL_HINTS[key];
|
|
297
|
-
const toolName2 = hint.split(" (")[0];
|
|
298
|
-
block(`Use ${toolName2} instead of piped command`, hint);
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
process.exit(0);
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
// Simple command redirect — extract args for instant retry
|
|
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 ? ` — args: "${args}"` : "";
|
|
311
|
-
block(`Use ${toolName2}${argsNote}`, hint);
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
// Everything else - approve
|
|
317
|
-
process.exit(0);
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
// ---- PreToolUse REVERSE handler (hex-line disabled) ----
|
|
321
|
-
|
|
322
|
-
function handlePreToolUseReverse(data) {
|
|
323
|
-
const toolName = data.tool_name || "";
|
|
324
|
-
|
|
325
|
-
// Agent tries hex-line tool that's disabled → redirect to built-in
|
|
326
|
-
if (toolName.startsWith("mcp__hex-line__")) {
|
|
327
|
-
const builtIn = REVERSE_TOOL_HINTS[toolName];
|
|
328
|
-
if (builtIn) {
|
|
329
|
-
const target = builtIn.split(" ")[0];
|
|
330
|
-
block(
|
|
331
|
-
`hex-line is disabled in this project. Use ${target}`,
|
|
332
|
-
`hex-line disabled. Use built-in: ${builtIn}`
|
|
333
|
-
);
|
|
334
|
-
}
|
|
335
|
-
block("hex-line is disabled in this project", "Disabled via project settings");
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
// All built-in tools — approve silently
|
|
339
|
-
process.exit(0);
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
// ---- PostToolUse handler ----
|
|
343
|
-
|
|
344
|
-
function handlePostToolUse(data) {
|
|
345
|
-
const toolName = data.tool_name || "";
|
|
346
|
-
|
|
347
|
-
// Only filter Bash output
|
|
348
|
-
if (toolName !== "Bash") {
|
|
349
|
-
process.exit(0);
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
const toolInput = data.tool_input || {};
|
|
353
|
-
const rawText = extractBashText(data.tool_response);
|
|
354
|
-
const command = toolInput.command || "";
|
|
355
|
-
|
|
356
|
-
// Nothing to filter
|
|
357
|
-
if (!rawText) {
|
|
358
|
-
process.exit(0);
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
const lines = rawText.split("\n");
|
|
362
|
-
const originalCount = lines.length;
|
|
363
|
-
|
|
364
|
-
// Short output - no filtering
|
|
365
|
-
if (originalCount < LINE_THRESHOLD) {
|
|
366
|
-
process.exit(0);
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
const type = detectCommandType(command);
|
|
370
|
-
|
|
371
|
-
// Pipeline: normalize -> deduplicate -> smart truncate
|
|
372
|
-
const filtered = normalizeOutput(lines.join("\n"), { headLines: HEAD_LINES, tailLines: TAIL_LINES });
|
|
373
|
-
const filteredCount = filtered.split("\n").length;
|
|
374
|
-
|
|
375
|
-
const header = `RTK FILTERED: ${type} (${originalCount} lines -> ${filteredCount} lines)`;
|
|
376
|
-
|
|
377
|
-
const output = [
|
|
378
|
-
"=".repeat(50),
|
|
379
|
-
header,
|
|
380
|
-
"=".repeat(50),
|
|
381
|
-
"",
|
|
382
|
-
filtered,
|
|
383
|
-
"",
|
|
384
|
-
"-".repeat(50),
|
|
385
|
-
`Original: ${originalCount} lines | Filtered: ${filteredCount} lines`,
|
|
386
|
-
"=".repeat(50),
|
|
387
|
-
].join("\n");
|
|
388
|
-
|
|
389
|
-
process.stderr.write(output);
|
|
390
|
-
process.exit(2);
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
// ---- SessionStart: inject tool preferences ----
|
|
394
|
-
|
|
395
|
-
function handleSessionStart() {
|
|
396
|
-
// Check if hex-line output style is active (skip full hints if so)
|
|
397
|
-
const settingsFiles = [
|
|
398
|
-
resolve(process.cwd(), ".claude/settings.local.json"),
|
|
399
|
-
resolve(process.cwd(), ".claude/settings.json"),
|
|
400
|
-
resolve(homedir(), ".claude/settings.json"),
|
|
401
|
-
];
|
|
402
|
-
let styleActive = false;
|
|
403
|
-
for (const f of settingsFiles) {
|
|
404
|
-
try {
|
|
405
|
-
const config = JSON.parse(readFileSync(f, "utf-8"));
|
|
406
|
-
if (config.outputStyle === "hex-line") { styleActive = true; break; }
|
|
407
|
-
if (config.outputStyle) break; // another style overrides
|
|
408
|
-
} catch { /* file missing or invalid */ }
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
if (styleActive) {
|
|
412
|
-
process.stdout.write(JSON.stringify({ systemMessage: "hex-line Output Style active." }));
|
|
413
|
-
process.exit(0);
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
// Full hints when output style is not active
|
|
417
|
-
const seen = new Set();
|
|
418
|
-
const lines = [];
|
|
419
|
-
for (const hint of Object.values(TOOL_HINTS)) {
|
|
420
|
-
const tool = hint.split(" ")[0];
|
|
421
|
-
if (!seen.has(tool)) {
|
|
422
|
-
seen.add(tool);
|
|
423
|
-
lines.push(`- ${hint}`);
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
|
-
lines.push("Exceptions: images, PDFs, notebooks, .claude/settings.json, .claude/settings.local.json \u2192 built-in Read; Glob always OK");
|
|
427
|
-
lines.push("Bash OK for: npm/node/git/docker/curl, pipes, scripts");
|
|
428
|
-
const msg = "Hex-line MCP available. Workflow:\n- Discovery: read_file, grep_search, outline, directory_tree\n- Hash edits: edit_file (set_line, replace_lines, insert_after)\n- Text rename: bulk_replace (multi-file search-replace)\n- Write new: write_file\n" + lines.join("\n");
|
|
429
|
-
process.stdout.write(JSON.stringify({ systemMessage: msg }));
|
|
430
|
-
process.exit(0);
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
// ---- Main: read stdin, route by hook_event_name ----
|
|
434
|
-
// Guard: only run when executed directly, not when imported for testing
|
|
435
|
-
|
|
436
|
-
import { fileURLToPath } from "node:url";
|
|
437
|
-
|
|
438
|
-
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
439
|
-
let input = "";
|
|
440
|
-
process.stdin.on("data", (chunk) => {
|
|
441
|
-
input += chunk;
|
|
442
|
-
});
|
|
443
|
-
process.stdin.on("end", () => {
|
|
444
|
-
try {
|
|
445
|
-
const data = JSON.parse(input);
|
|
446
|
-
const event = data.hook_event_name || "";
|
|
447
|
-
|
|
448
|
-
if (isHexLineDisabled()) {
|
|
449
|
-
// REVERSE MODE: block hex-line calls, approve everything else
|
|
450
|
-
if (event === "PreToolUse") handlePreToolUseReverse(data);
|
|
451
|
-
process.exit(0); // SessionStart, PostToolUse — silent exit
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
// NORMAL MODE
|
|
455
|
-
if (event === "SessionStart") handleSessionStart();
|
|
456
|
-
else if (event === "PreToolUse") handlePreToolUse(data);
|
|
457
|
-
else if (event === "PostToolUse") handlePostToolUse(data);
|
|
458
|
-
else process.exit(0);
|
|
459
|
-
} catch {
|
|
460
|
-
process.exit(0);
|
|
461
|
-
}
|
|
462
|
-
});
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
// ---- Exports for testing ----
|
|
466
|
-
export { isHexLineDisabled, _resetHexLineDisabledCache };
|