@levnikolaevich/hex-line-mcp 1.16.0 → 1.17.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 +13 -7
- package/dist/hook.mjs +199 -61
- package/dist/server.mjs +107 -17
- package/output-style.md +9 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -35,10 +35,10 @@ Advanced / occasional:
|
|
|
35
35
|
| `read_file` | Read file with progressive disclosure, optional edit-ready metadata, and automatic graph hints when available | Minimal plain discovery by default, explicit `edit_ready` for verified edits |
|
|
36
36
|
| `edit_file` | Revision-aware anchor edits (`set_line`, `replace_lines`, `insert_after`, `replace_between`) | Batched same-file edits + conservative auto-rebase |
|
|
37
37
|
| `write_file` | Create new file or overwrite, auto-creates parent dirs | Path validation, no hash overhead |
|
|
38
|
-
| `grep_search` | Search with ripgrep, summary-first discovery, and optional edit-ready hunks | `summary` by default,
|
|
38
|
+
| `grep_search` | Search with ripgrep, summary-first discovery, and optional edit-ready hunks | `summary` by default, capped `content` mode with explicit `allow_large_output` escape hatch |
|
|
39
39
|
| `outline` | AST-based structural overview with hash anchors via tree-sitter WASM. Supports JavaScript/TypeScript, Python, C#, PHP, and fence-aware markdown headings | 95% token reduction, direct edit anchors |
|
|
40
40
|
| `verify` | Check if held checksums / revision are still current | Staleness check without full re-read |
|
|
41
|
-
| `inspect_path` | Unified file-or-directory inspection | Minimal tree discovery by default,
|
|
41
|
+
| `inspect_path` | Unified file-or-directory inspection | Minimal tree discovery by default, capped pattern-mode lists with refine hints |
|
|
42
42
|
| `changes` | Compare file against git ref, shows added/removed/modified symbols | AST-level semantic diff with risk/provenance preview |
|
|
43
43
|
| `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 |
|
|
44
44
|
|
|
@@ -46,7 +46,7 @@ Advanced / occasional:
|
|
|
46
46
|
|
|
47
47
|
| Event | Trigger | Action |
|
|
48
48
|
|-------|---------|--------|
|
|
49
|
-
| **PreToolUse** | Read/Edit/Write/Grep on text
|
|
49
|
+
| **PreToolUse** | Read/Edit/Write/Grep/Glob on project text scope | Hard redirect to hex-line for project-scoped text files and file discovery; built-in tools stay available for binary/media and text paths outside the current project root |
|
|
50
50
|
| **PreToolUse** | Bash with dangerous commands | Blocks `rm -rf /`, `git push --force`, etc. Agent must confirm with user |
|
|
51
51
|
| **PostToolUse** | Bash with 50+ lines output | RTK: deduplicates, truncates, shows filtered summary to Claude as feedback |
|
|
52
52
|
| **SessionStart** | Session begins | Injects a short bootstrap hint; defers to the active output style when `hex-line` style is enabled |
|
|
@@ -54,7 +54,7 @@ Advanced / occasional:
|
|
|
54
54
|
|
|
55
55
|
### Bash Redirects
|
|
56
56
|
|
|
57
|
-
PreToolUse also intercepts
|
|
57
|
+
PreToolUse also intercepts project-scoped file inspection Bash commands: `cat`, `type`, `Get-Content`, `head`, `tail`, `ls`, `dir`, `tree`, `find`, `Get-ChildItem`, `stat`, `wc`, `Get-Item`, `grep`, `rg`, `findstr`, `Select-String`, `sed -i`. Targeted inspection pipelines are redirected too; Git/build/test/docker/network and stdin-filter pipelines remain allowed.
|
|
58
58
|
## Install
|
|
59
59
|
|
|
60
60
|
### MCP Server
|
|
@@ -292,9 +292,13 @@ Search file contents using ripgrep. Default mode is `summary` for discovery. Use
|
|
|
292
292
|
| `total_limit` | number | no | Total match events across all files; multiline matches count as 1 (default: 50 for `summary`, 200 for `content`, 1000 for `files`/`count`, 0 = unlimited) |
|
|
293
293
|
| `plain` | boolean | no | Omit hash tags inside block entries, return `lineNum\|content` |
|
|
294
294
|
| `edit_ready` | boolean | no | Preserve hash/checksum search hunks in `content` mode |
|
|
295
|
+
| `allow_large_output` | boolean | no | Bypass the default `content`-mode block/char caps when you intentionally need a larger payload |
|
|
295
296
|
|
|
296
297
|
`summary` mode returns counts, top files, and a few plain snippets. `content` mode returns canonical `search_hunk` blocks with per-hunk checksums enabling direct `replace_lines` from grep results without intermediate `read_file`.
|
|
297
298
|
|
|
299
|
+
- Default `content` mode is intentionally capped to keep discovery cheap. When truncation happens, the diagnostic block includes `shown_matches`, `file_count`, `truncated`, `next_action`, and `suggested_refine_call`.
|
|
300
|
+
- Treat `allow_large_output: true` as an explicit override for review/debug workflows, not as the normal discovery path.
|
|
301
|
+
|
|
298
302
|
### outline
|
|
299
303
|
|
|
300
304
|
AST-based structural outline with hash anchors for direct `edit_file` usage. Supports JavaScript/TypeScript, Python, C#, PHP, and fence-aware markdown heading navigation (`.md`/`.mdx`). Each entry includes a hash tag for immediate anchor use without intermediate `read_file`.
|
|
@@ -343,6 +347,7 @@ Inspect a file or directory path without guessing which low-level tool to call f
|
|
|
343
347
|
| `pattern` | string | no | Glob filter on names (e.g. `"*-mcp"`, `"*.mjs"`). Returns flat match list instead of tree |
|
|
344
348
|
| `type` | string | no | `"file"`, `"dir"`, or `"all"` (default). Like `find -type f/d` |
|
|
345
349
|
| `max_depth` | number | no | Max recursion depth (default: 2 for discovery, or 20 in pattern mode) |
|
|
350
|
+
| `max_entries` | number | no | Max entries to show in pattern mode before truncation metadata is returned (default: 60, `0` = unlimited) |
|
|
346
351
|
| `gitignore` | boolean | no | Respect root .gitignore patterns (default: true). Nested .gitignore not supported |
|
|
347
352
|
| `format` | string | no | `"compact"` = shorter path view. `"full"` = include sizes / metadata where available |
|
|
348
353
|
| `verbosity` | enum | no | `minimal`, `compact`, or `full` |
|
|
@@ -350,6 +355,7 @@ Inspect a file or directory path without guessing which low-level tool to call f
|
|
|
350
355
|
- For regular files it returns compact metadata: size, line count when cheap, modification time, type, and binary flag.
|
|
351
356
|
- For directories it returns a gitignore-aware tree.
|
|
352
357
|
- With `pattern`, it switches to flat match mode and works as the preferred replacement for `find` / recursive `ls`.
|
|
358
|
+
- Broad pattern mode is capped by default and returns `match_count`, `shown_count`, `truncated`, `next_action`, and `suggested_refine_call` so the caller can narrow `path` before asking for more.
|
|
353
359
|
|
|
354
360
|
## Hook
|
|
355
361
|
|
|
@@ -357,11 +363,11 @@ The unified hook (`hook.mjs`) handles three Claude hook events:
|
|
|
357
363
|
|
|
358
364
|
### PreToolUse: Tool Redirect
|
|
359
365
|
|
|
360
|
-
|
|
366
|
+
Hard-routes built-in `Read`, `Edit`, `Write`, `Grep`, and `Glob` on project-scoped text workflows to hex-line. Binary/media files (images, PDFs, notebooks, archives, executables, fonts, media) and text paths outside the current project root stay on built-in tools.
|
|
361
367
|
|
|
362
368
|
### PreToolUse: Bash Redirect + Dangerous Blocker
|
|
363
369
|
|
|
364
|
-
Intercepts
|
|
370
|
+
Intercepts project-scoped Bash file inspection commands (`cat`, `type`, `Get-Content`, `head`, `tail`, `ls`, `dir`, `tree`, `find`, `Get-ChildItem`, `stat`, `wc`, `Get-Item`, `grep`, `rg`, `findstr`, `Select-String`, `sed -i`, etc.) and redirects covered cases to hex-line tools. Targeted inspection pipelines are redirected. Dangerous commands (`rm -rf /`, `git push --force`, `git reset --hard`, `DROP TABLE`, `chmod 777`, `mkfs`, `dd`) are blocked.
|
|
365
371
|
|
|
366
372
|
### PostToolUse: RTK Output Filter
|
|
367
373
|
|
|
@@ -477,7 +483,7 @@ The PostToolUse hook normalizes Bash output (replaces UUIDs, timestamps, IPs wit
|
|
|
477
483
|
<details>
|
|
478
484
|
<summary><b>Can I disable the built-in tool blocking?</b></summary>
|
|
479
485
|
|
|
480
|
-
Yes.
|
|
486
|
+
Yes. The supported runtime bypass is `.hex-skills/environment_state.json` -> `{ "hooks": { "mode": "advisory" } }`, which downgrades project text redirects to advice. 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`.
|
|
481
487
|
|
|
482
488
|
</details>
|
|
483
489
|
|
package/dist/hook.mjs
CHANGED
|
@@ -55,11 +55,10 @@ function normalizeOutput(text, opts = {}) {
|
|
|
55
55
|
|
|
56
56
|
// hook.mjs
|
|
57
57
|
import { readFileSync, writeSync } from "node:fs";
|
|
58
|
-
import { resolve
|
|
58
|
+
import { resolve } from "node:path";
|
|
59
59
|
import { homedir } from "node:os";
|
|
60
60
|
|
|
61
61
|
// lib/hook-policy.mjs
|
|
62
|
-
import { resolve } from "node:path";
|
|
63
62
|
var BINARY_EXT = /* @__PURE__ */ new Set([
|
|
64
63
|
".png",
|
|
65
64
|
".jpg",
|
|
@@ -109,7 +108,7 @@ var REVERSE_TOOL_HINTS = {
|
|
|
109
108
|
"mcp__hex-line__edit_file": "Edit (old_string, new_string, replace_all)",
|
|
110
109
|
"mcp__hex-line__write_file": "Write (file_path, content)",
|
|
111
110
|
"mcp__hex-line__grep_search": "Grep (pattern, path)",
|
|
112
|
-
"mcp__hex-line__inspect_path": "Path info / tree / Bash(ls,stat)",
|
|
111
|
+
"mcp__hex-line__inspect_path": "Glob (pattern, path) / Path info / tree / Bash(ls,stat)",
|
|
113
112
|
"mcp__hex-line__outline": "Read with offset/limit",
|
|
114
113
|
"mcp__hex-line__verify": "Read (check checksum/revision freshness before follow-up edits)",
|
|
115
114
|
"mcp__hex-line__changes": "Bash(git diff)",
|
|
@@ -120,12 +119,13 @@ var TOOL_HINTS = {
|
|
|
120
119
|
Edit: "mcp__hex-line__edit_file for revision-aware hash edits. Batch same-file hunks, carry base_revision, use replace_between for block rewrites",
|
|
121
120
|
Write: "mcp__hex-line__write_file (not Write). No prior Read needed",
|
|
122
121
|
Grep: "mcp__hex-line__grep_search (not Grep). Params: output, literal, context_before, context_after, multiline",
|
|
123
|
-
|
|
122
|
+
Glob: "mcp__hex-line__inspect_path (not Glob). Use pattern=... with an explicit path for project file discovery and name/path globbing",
|
|
123
|
+
cat: "mcp__hex-line__read_file (not cat/head/tail/less/more/type/Get-Content)",
|
|
124
124
|
head: "mcp__hex-line__read_file with limit param (not head)",
|
|
125
125
|
tail: "mcp__hex-line__read_file with offset param (not tail)",
|
|
126
|
-
ls: "mcp__hex-line__inspect_path for tree or pattern search (not ls/find/tree). E.g. pattern='*-mcp' type='dir'",
|
|
127
|
-
stat: "mcp__hex-line__inspect_path for compact file metadata (not stat/wc/file)",
|
|
128
|
-
grep: "mcp__hex-line__grep_search (not grep/rg). Params: output, literal, context_before, context_after, multiline",
|
|
126
|
+
ls: "mcp__hex-line__inspect_path for tree or pattern search (not ls/dir/find/tree/Get-ChildItem). E.g. pattern='*-mcp' type='dir'",
|
|
127
|
+
stat: "mcp__hex-line__inspect_path for compact file metadata (not stat/wc/Get-Item/file)",
|
|
128
|
+
grep: "mcp__hex-line__grep_search (not grep/rg/findstr/Select-String). Params: output, literal, context_before, context_after, multiline",
|
|
129
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)",
|
|
130
130
|
diff: "mcp__hex-line__changes (not diff). Git diff with change symbols",
|
|
131
131
|
outline: "mcp__hex-line__outline (before reading large code files)",
|
|
@@ -135,23 +135,25 @@ var TOOL_HINTS = {
|
|
|
135
135
|
};
|
|
136
136
|
var DEFERRED_HINT = "If schemas not loaded: ToolSearch('+hex-line read edit')";
|
|
137
137
|
var BASH_REDIRECTS = [
|
|
138
|
-
{ regex: /^cat\
|
|
139
|
-
{ regex: /^head\
|
|
140
|
-
{ regex: /^tail\s
|
|
141
|
-
{ regex: /^(less|more)\
|
|
142
|
-
{ regex: /^
|
|
143
|
-
{ regex: /^dir\
|
|
144
|
-
{ regex: /^
|
|
145
|
-
{ regex: /^
|
|
146
|
-
{ regex: /^(
|
|
147
|
-
{ regex: /^(grep|rg)\
|
|
148
|
-
{ regex: /^
|
|
138
|
+
{ regex: /^(cat|type)\b/i, key: "cat", kind: "reader" },
|
|
139
|
+
{ regex: /^head\b/i, key: "head", kind: "reader" },
|
|
140
|
+
{ regex: /^tail\b(?!.*\s-[fF](\s|$))(?!.*\s--follow(\s|$))/i, key: "tail", kind: "reader" },
|
|
141
|
+
{ regex: /^(less|more)\b/i, key: "cat", kind: "reader" },
|
|
142
|
+
{ regex: /^(Get-Content|gc)\b/i, key: "cat", kind: "reader" },
|
|
143
|
+
{ regex: /^(ls|dir|tree|find)\b/i, key: "ls", kind: "list" },
|
|
144
|
+
{ regex: /^(Get-ChildItem|gci)\b/i, key: "ls", kind: "list" },
|
|
145
|
+
{ regex: /^(stat|wc)\b/i, key: "stat", kind: "meta" },
|
|
146
|
+
{ regex: /^(Get-Item|gi)\b/i, key: "stat", kind: "meta" },
|
|
147
|
+
{ regex: /^(grep|rg|findstr)\b/i, key: "grep", kind: "search" },
|
|
148
|
+
{ regex: /^Select-String\b/i, key: "grep", kind: "search" },
|
|
149
|
+
{ regex: /^sed\b.*\s-i(\s|$)/i, key: "sed", kind: "edit" }
|
|
149
150
|
];
|
|
150
151
|
var TOOL_REDIRECT_MAP = {
|
|
151
152
|
Read: "Read",
|
|
152
153
|
Edit: "Edit",
|
|
153
154
|
Write: "Write",
|
|
154
|
-
Grep: "Grep"
|
|
155
|
+
Grep: "Grep",
|
|
156
|
+
Glob: "Glob"
|
|
155
157
|
};
|
|
156
158
|
var DANGEROUS_PATTERNS = [
|
|
157
159
|
{ regex: /rm\s+(-[rf]+\s+)*[/~]/, reason: "rm -rf on root/home directory" },
|
|
@@ -175,15 +177,16 @@ var HOOK_OUTPUT_POLICY = {
|
|
|
175
177
|
headLines: 15,
|
|
176
178
|
tailLines: 15
|
|
177
179
|
};
|
|
178
|
-
function
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
180
|
+
function normalizePolicyPath(filePath) {
|
|
181
|
+
if (!filePath) return "";
|
|
182
|
+
let normalized = filePath.replace(/\\/g, "/");
|
|
183
|
+
if (normalized.length > 1 && normalized.endsWith("/")) normalized = normalized.slice(0, -1);
|
|
184
|
+
return process.platform === "win32" ? normalized.toLowerCase() : normalized;
|
|
185
|
+
}
|
|
186
|
+
function isWithinDir(candidatePath, dirPath) {
|
|
187
|
+
const candidate = normalizePolicyPath(candidatePath);
|
|
188
|
+
const dir = normalizePolicyPath(dirPath);
|
|
189
|
+
return !!candidate && !!dir && (candidate === dir || candidate.startsWith(`${dir}/`));
|
|
187
190
|
}
|
|
188
191
|
|
|
189
192
|
// hook.mjs
|
|
@@ -195,10 +198,140 @@ function extOf(filePath) {
|
|
|
195
198
|
function getFilePath(toolInput) {
|
|
196
199
|
return toolInput.file_path || toolInput.path || "";
|
|
197
200
|
}
|
|
201
|
+
function getGlobScopePath(toolInput) {
|
|
202
|
+
if (toolInput.path) return toolInput.path;
|
|
203
|
+
const pattern = typeof toolInput.pattern === "string" ? toolInput.pattern : "";
|
|
204
|
+
if (!pattern) return "";
|
|
205
|
+
const prefix = pattern.split(/[*?[{\]]/, 1)[0];
|
|
206
|
+
return prefix.replace(/[\\/]+$/, "");
|
|
207
|
+
}
|
|
198
208
|
function resolveToolPath(filePath) {
|
|
199
209
|
if (!filePath) return "";
|
|
200
|
-
if (filePath.startsWith("~/")) return
|
|
201
|
-
return
|
|
210
|
+
if (filePath.startsWith("~/")) return resolve(homedir(), filePath.slice(2));
|
|
211
|
+
return resolve(process.cwd(), filePath);
|
|
212
|
+
}
|
|
213
|
+
function stripMatchingQuotes(token) {
|
|
214
|
+
if (!token || token.length < 2) return token;
|
|
215
|
+
const first = token[0];
|
|
216
|
+
const last = token[token.length - 1];
|
|
217
|
+
return first === last && (first === '"' || first === "'" || first === "`") ? token.slice(1, -1) : token;
|
|
218
|
+
}
|
|
219
|
+
function tokenizeCommand(segment) {
|
|
220
|
+
return (segment.match(/"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|`(?:[^`\\]|\\.)*`|\S+/g) || []).map(stripMatchingQuotes);
|
|
221
|
+
}
|
|
222
|
+
function splitCompoundCommand(command) {
|
|
223
|
+
return command.split(/\s*(?:\|\||&&|[|;]|>>?)\s*/).map((part) => part.trim()).filter(Boolean);
|
|
224
|
+
}
|
|
225
|
+
function readNamedOptionValues(tokens, optionNames) {
|
|
226
|
+
const values = [];
|
|
227
|
+
const names = new Set(optionNames.map((name) => name.toLowerCase()));
|
|
228
|
+
for (let i = 1; i < tokens.length; i++) {
|
|
229
|
+
const token = tokens[i];
|
|
230
|
+
const lower = token.toLowerCase();
|
|
231
|
+
let matched = false;
|
|
232
|
+
for (const name of names) {
|
|
233
|
+
if (lower === name) {
|
|
234
|
+
if (tokens[i + 1]) {
|
|
235
|
+
values.push(tokens[i + 1]);
|
|
236
|
+
}
|
|
237
|
+
matched = true;
|
|
238
|
+
break;
|
|
239
|
+
}
|
|
240
|
+
if (lower.startsWith(`${name}=`) || lower.startsWith(`${name}:`)) {
|
|
241
|
+
values.push(token.slice(name.length + 1));
|
|
242
|
+
matched = true;
|
|
243
|
+
break;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
if (matched) continue;
|
|
247
|
+
}
|
|
248
|
+
return { values };
|
|
249
|
+
}
|
|
250
|
+
function collectPositionals(tokens, { optionValueFlags = [], slashOptions = false } = {}) {
|
|
251
|
+
const values = [];
|
|
252
|
+
const expectsValue = new Set(optionValueFlags.map((flag) => flag.toLowerCase()));
|
|
253
|
+
for (let i = 1; i < tokens.length; i++) {
|
|
254
|
+
const token = tokens[i];
|
|
255
|
+
const lower = token.toLowerCase();
|
|
256
|
+
if (expectsValue.has(lower)) {
|
|
257
|
+
i += 1;
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
if (lower.startsWith("-")) continue;
|
|
261
|
+
if (slashOptions && lower.startsWith("/")) continue;
|
|
262
|
+
values.push(token);
|
|
263
|
+
}
|
|
264
|
+
return values;
|
|
265
|
+
}
|
|
266
|
+
function resolveCandidatePaths(pathTokens) {
|
|
267
|
+
return pathTokens.map(resolveToolPath).filter(Boolean).map(normalizePolicyPath);
|
|
268
|
+
}
|
|
269
|
+
function isProjectScopedPath(filePath) {
|
|
270
|
+
return !!filePath && isWithinDir(resolveToolPath(filePath), process.cwd());
|
|
271
|
+
}
|
|
272
|
+
function getBashPathCandidates(spec, tokens, isFirstSegment) {
|
|
273
|
+
const command = (tokens[0] || "").toLowerCase();
|
|
274
|
+
const powershellPaths = readNamedOptionValues(tokens, ["-path", "-literalpath"]);
|
|
275
|
+
const positional = (flags = [], slashOptions = false) => collectPositionals(tokens, { optionValueFlags: flags, slashOptions });
|
|
276
|
+
if (spec.kind === "reader") {
|
|
277
|
+
if (powershellPaths.values.length > 0) return powershellPaths.values;
|
|
278
|
+
if (command === "head" || command === "tail") {
|
|
279
|
+
return positional(["-n", "-c", "--lines", "--bytes"]).slice(-1);
|
|
280
|
+
}
|
|
281
|
+
return positional();
|
|
282
|
+
}
|
|
283
|
+
if (spec.kind === "list") {
|
|
284
|
+
if (powershellPaths.values.length > 0) return powershellPaths.values;
|
|
285
|
+
if (command === "find") return positional().slice(0, 1);
|
|
286
|
+
if (command === "dir") return positional([], true);
|
|
287
|
+
return positional();
|
|
288
|
+
}
|
|
289
|
+
if (spec.kind === "meta") {
|
|
290
|
+
if (powershellPaths.values.length > 0) return powershellPaths.values;
|
|
291
|
+
if (command === "wc") return positional(["-l", "-w", "-c", "-m", "-L"]).slice(0);
|
|
292
|
+
return positional();
|
|
293
|
+
}
|
|
294
|
+
if (spec.kind === "edit") {
|
|
295
|
+
const args = positional(["-e", "-f"]);
|
|
296
|
+
const hasScriptOption = tokens.some((token) => {
|
|
297
|
+
const lower = token.toLowerCase();
|
|
298
|
+
return lower === "-e" || lower === "-f";
|
|
299
|
+
});
|
|
300
|
+
return hasScriptOption ? args : args.slice(1);
|
|
301
|
+
}
|
|
302
|
+
if (spec.kind === "search") {
|
|
303
|
+
if (powershellPaths.values.length > 0) return powershellPaths.values;
|
|
304
|
+
const args = command === "findstr" ? positional(["/c"], true) : positional(["-e", "-f", "-g", "--glob", "-t", "--type", "--type-not", "-A", "-B", "-C", "-m"]);
|
|
305
|
+
const hasExplicitPatternOption = command === "findstr" ? tokens.some((token) => {
|
|
306
|
+
const lower = token.toLowerCase();
|
|
307
|
+
return lower === "/c" || lower.startsWith("/c:");
|
|
308
|
+
}) : tokens.some((token) => {
|
|
309
|
+
const lower = token.toLowerCase();
|
|
310
|
+
return lower === "-e" || lower === "-f";
|
|
311
|
+
});
|
|
312
|
+
const pathArgs = hasExplicitPatternOption ? args : args.slice(1);
|
|
313
|
+
if (pathArgs.length > 0) return pathArgs;
|
|
314
|
+
return isFirstSegment ? [process.cwd()] : [];
|
|
315
|
+
}
|
|
316
|
+
return [];
|
|
317
|
+
}
|
|
318
|
+
function getBashRedirect(segment, isFirstSegment) {
|
|
319
|
+
const tokens = tokenizeCommand(segment);
|
|
320
|
+
if (tokens.length === 0) return null;
|
|
321
|
+
const spec = BASH_REDIRECTS.find((entry) => entry.regex.test(segment));
|
|
322
|
+
if (!spec) return null;
|
|
323
|
+
const targets = resolveCandidatePaths(getBashPathCandidates(spec, tokens, isFirstSegment));
|
|
324
|
+
if (targets.length > 0 && !targets.some((target) => isWithinDir(target, process.cwd()))) {
|
|
325
|
+
return null;
|
|
326
|
+
}
|
|
327
|
+
if (targets.length === 0 && spec.kind !== "list" && !(spec.kind === "search" && isFirstSegment)) {
|
|
328
|
+
return null;
|
|
329
|
+
}
|
|
330
|
+
const hint = TOOL_HINTS[spec.key];
|
|
331
|
+
return {
|
|
332
|
+
hint,
|
|
333
|
+
toolName: hint.split(" (")[0]
|
|
334
|
+
};
|
|
202
335
|
}
|
|
203
336
|
function isPartialRead(toolInput) {
|
|
204
337
|
return [toolInput.offset, toolInput.limit, toolInput.start_line, toolInput.end_line, toolInput.ranges].some((value) => value !== void 0 && value !== null && value !== "");
|
|
@@ -224,7 +357,7 @@ function isHexLineDisabled(configPath) {
|
|
|
224
357
|
if (_hexLineDisabled !== null) return _hexLineDisabled;
|
|
225
358
|
_hexLineDisabled = false;
|
|
226
359
|
try {
|
|
227
|
-
const p = configPath ||
|
|
360
|
+
const p = configPath || resolve(homedir(), ".claude.json");
|
|
228
361
|
const claudeJson = JSON.parse(readFileSync(p, "utf-8"));
|
|
229
362
|
const projects = claudeJson.projects;
|
|
230
363
|
if (!projects || typeof projects !== "object") return _hexLineDisabled;
|
|
@@ -250,7 +383,7 @@ function getHookMode() {
|
|
|
250
383
|
if (_hookMode !== void 0) return _hookMode;
|
|
251
384
|
_hookMode = "blocking";
|
|
252
385
|
try {
|
|
253
|
-
const stateFile =
|
|
386
|
+
const stateFile = resolve(process.cwd(), ".hex-skills/environment_state.json");
|
|
254
387
|
const data = JSON.parse(readFileSync(stateFile, "utf-8"));
|
|
255
388
|
if (data.hooks?.mode === "advisory") _hookMode = "advisory";
|
|
256
389
|
} catch {
|
|
@@ -270,7 +403,9 @@ function block(reason, context) {
|
|
|
270
403
|
${context}` : reason;
|
|
271
404
|
const output = {
|
|
272
405
|
hookSpecificOutput: {
|
|
273
|
-
|
|
406
|
+
hookEventName: "PreToolUse",
|
|
407
|
+
permissionDecision: "deny",
|
|
408
|
+
permissionDecisionReason: reason
|
|
274
409
|
},
|
|
275
410
|
systemMessage: msg
|
|
276
411
|
};
|
|
@@ -280,7 +415,9 @@ ${context}` : reason;
|
|
|
280
415
|
function advise(reason, context) {
|
|
281
416
|
const output = {
|
|
282
417
|
hookSpecificOutput: {
|
|
283
|
-
|
|
418
|
+
hookEventName: "PreToolUse",
|
|
419
|
+
permissionDecision: "allow",
|
|
420
|
+
permissionDecisionReason: reason
|
|
284
421
|
},
|
|
285
422
|
systemMessage: context ? `${reason}
|
|
286
423
|
${context}` : reason
|
|
@@ -302,27 +439,23 @@ function handlePreToolUse(data) {
|
|
|
302
439
|
}
|
|
303
440
|
const hintKey = TOOL_REDIRECT_MAP[toolName];
|
|
304
441
|
if (hintKey) {
|
|
305
|
-
const filePath = getFilePath(toolInput);
|
|
442
|
+
const filePath = toolName === "Glob" ? getGlobScopePath(toolInput) : getFilePath(toolInput);
|
|
306
443
|
if (BINARY_EXT.has(extOf(filePath))) {
|
|
307
444
|
process.exit(0);
|
|
308
445
|
}
|
|
309
|
-
const
|
|
310
|
-
|
|
311
|
-
if (claudeAllow.includes(resolvedNorm.toLowerCase())) {
|
|
446
|
+
const projectScoped = filePath ? isProjectScopedPath(filePath) : toolName === "Grep" || toolName === "Glob";
|
|
447
|
+
if (!projectScoped) {
|
|
312
448
|
process.exit(0);
|
|
313
449
|
}
|
|
314
|
-
if (resolvedNorm.includes("/.claude/")) {
|
|
315
|
-
redirect("Protected .claude/ path. Use built-in tools for .claude/ config files.");
|
|
316
|
-
}
|
|
317
450
|
if (toolName === "Read") {
|
|
318
451
|
const ext = filePath ? extOf(filePath) : "";
|
|
319
452
|
const rangeHint = isPartialRead(toolInput) ? " Preserve the same offset/limit or ranges in read_file." : "";
|
|
320
453
|
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);
|
|
454
|
+
redirect(outlineHint, "Use hex-line for project text-file reads to keep hashes, revision metadata, and graph hints in one flow.\n" + DEFERRED_HINT);
|
|
322
455
|
}
|
|
323
456
|
if (toolName === "Edit") {
|
|
324
457
|
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";
|
|
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);
|
|
458
|
+
redirect(target, "Use hash-verified edits for project text files. Locate anchors/checksums first, then call edit_file once with batched edits.\n" + DEFERRED_HINT);
|
|
326
459
|
}
|
|
327
460
|
if (toolName === "Write") {
|
|
328
461
|
const pathNote = filePath ? ` with path="${filePath}"` : "";
|
|
@@ -332,6 +465,15 @@ function handlePreToolUse(data) {
|
|
|
332
465
|
const pathNote = filePath ? ` with path="${filePath}"` : "";
|
|
333
466
|
redirect(`Use mcp__hex-line__grep_search${pathNote}`, TOOL_HINTS.Grep + "\n" + DEFERRED_HINT);
|
|
334
467
|
}
|
|
468
|
+
if (toolName === "Glob") {
|
|
469
|
+
const pattern = typeof toolInput.pattern === "string" ? toolInput.pattern : "";
|
|
470
|
+
const inspectPath = JSON.stringify(filePath || process.cwd());
|
|
471
|
+
const inspectPattern = pattern ? JSON.stringify(pattern) : '"..."';
|
|
472
|
+
redirect(
|
|
473
|
+
`Use mcp__hex-line__inspect_path(path=${inspectPath}, pattern=${inspectPattern})`,
|
|
474
|
+
"Use inspect_path(pattern=...) for project file discovery and name/path globbing. grep_search(glob=...) remains available when you also need content search.\n" + DEFERRED_HINT
|
|
475
|
+
);
|
|
476
|
+
}
|
|
335
477
|
}
|
|
336
478
|
if (toolName === "Bash") {
|
|
337
479
|
const command = (toolInput.command || "").trim();
|
|
@@ -348,24 +490,20 @@ function handlePreToolUse(data) {
|
|
|
348
490
|
}
|
|
349
491
|
}
|
|
350
492
|
if (COMPOUND_OPERATORS.test(command)) {
|
|
351
|
-
const
|
|
352
|
-
for (const
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
redirect(`Use ${toolName2} instead of piped command`, hint);
|
|
493
|
+
const segments = splitCompoundCommand(command);
|
|
494
|
+
for (const [index, segment] of segments.entries()) {
|
|
495
|
+
const redirectInfo2 = getBashRedirect(segment, index === 0);
|
|
496
|
+
if (redirectInfo2) {
|
|
497
|
+
redirect(`Use ${redirectInfo2.toolName} instead of project file inspection pipeline`, redirectInfo2.hint);
|
|
357
498
|
}
|
|
358
499
|
}
|
|
359
500
|
process.exit(0);
|
|
360
501
|
}
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
const argsNote = args ? ` \u2014 args: "${args}"` : "";
|
|
367
|
-
redirect(`Use ${toolName2}${argsNote}`, hint);
|
|
368
|
-
}
|
|
502
|
+
const redirectInfo = getBashRedirect(command, true);
|
|
503
|
+
if (redirectInfo) {
|
|
504
|
+
const args = command.split(/\s+/).slice(1).join(" ");
|
|
505
|
+
const argsNote = args ? ` \u2014 args: "${args}"` : "";
|
|
506
|
+
redirect(`Use ${redirectInfo.toolName}${argsNote}`, redirectInfo.hint);
|
|
369
507
|
}
|
|
370
508
|
}
|
|
371
509
|
process.exit(0);
|
|
@@ -423,9 +561,9 @@ function handlePostToolUse(data) {
|
|
|
423
561
|
}
|
|
424
562
|
function handleSessionStart() {
|
|
425
563
|
const settingsFiles = [
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
564
|
+
resolve(process.cwd(), ".claude/settings.local.json"),
|
|
565
|
+
resolve(process.cwd(), ".claude/settings.json"),
|
|
566
|
+
resolve(homedir(), ".claude/settings.json")
|
|
429
567
|
];
|
|
430
568
|
let styleActive = false;
|
|
431
569
|
for (const f of settingsFiles) {
|
|
@@ -439,7 +577,7 @@ function handleSessionStart() {
|
|
|
439
577
|
} catch {
|
|
440
578
|
}
|
|
441
579
|
}
|
|
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,
|
|
580
|
+
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, and text paths outside the current project root.</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. Read-only tools may inspect explicit temp-file paths outside the repo. Mutating tools stay project-scoped unless you intentionally pass allow_external=true.</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 <tip>Use inspect_path(pattern=...) for project file discovery and name/path globbing.</tip>\n </tips>\n <exceptions>Built-in tools stay OK for images, PDFs, notebooks, and text paths outside the current project root.</exceptions>\n</hex-line_instructions>";
|
|
443
581
|
safeExit(1, JSON.stringify({ systemMessage: msg }), 0);
|
|
444
582
|
}
|
|
445
583
|
var _norm = (p) => p.replace(/\\/g, "/");
|
package/dist/server.mjs
CHANGED
|
@@ -1272,6 +1272,7 @@ function serializeDiagnosticBlock(block) {
|
|
|
1272
1272
|
if (block.requestedStartLine !== null && block.requestedEndLine !== null) {
|
|
1273
1273
|
lines.push(`requested_span: ${block.requestedStartLine}-${block.requestedEndLine}`);
|
|
1274
1274
|
}
|
|
1275
|
+
lines.push(...renderMetaLines(block.meta));
|
|
1275
1276
|
lines.push(`message: ${block.message}`);
|
|
1276
1277
|
return lines.join("\n");
|
|
1277
1278
|
}
|
|
@@ -2161,7 +2162,6 @@ function collectBatchConflicts({
|
|
|
2161
2162
|
changedRanges,
|
|
2162
2163
|
conflictPolicy,
|
|
2163
2164
|
currentSnapshot,
|
|
2164
|
-
filePath,
|
|
2165
2165
|
hasBaseSnapshot,
|
|
2166
2166
|
opts,
|
|
2167
2167
|
staleRevision
|
|
@@ -2717,7 +2717,6 @@ ${snip.text}`;
|
|
|
2717
2717
|
changedRanges,
|
|
2718
2718
|
conflictPolicy,
|
|
2719
2719
|
currentSnapshot,
|
|
2720
|
-
filePath,
|
|
2721
2720
|
hasBaseSnapshot,
|
|
2722
2721
|
opts,
|
|
2723
2722
|
staleRevision
|
|
@@ -2924,6 +2923,8 @@ try {
|
|
|
2924
2923
|
var DEFAULT_LIMIT2 = 100;
|
|
2925
2924
|
var DEFAULT_TOTAL_LIMIT_CONTENT = 200;
|
|
2926
2925
|
var DEFAULT_TOTAL_LIMIT_LIST = 1e3;
|
|
2926
|
+
var DEFAULT_CONTENT_BLOCK_LIMIT = 12;
|
|
2927
|
+
var DEFAULT_CONTENT_OUTPUT_CHARS = 12e3;
|
|
2927
2928
|
var MAX_OUTPUT = 10 * 1024 * 1024;
|
|
2928
2929
|
var TIMEOUT2 = 3e4;
|
|
2929
2930
|
var MAX_SEARCH_OUTPUT_CHARS = 8e4;
|
|
@@ -2964,15 +2965,16 @@ function spawnRg(args) {
|
|
|
2964
2965
|
function grepSearch(pattern, opts = {}) {
|
|
2965
2966
|
const normPath = normalizePath(opts.path || "");
|
|
2966
2967
|
const target = normPath ? resolve2(normPath) : process.cwd();
|
|
2967
|
-
const output = opts.output || "
|
|
2968
|
+
const output = opts.output || "summary";
|
|
2968
2969
|
const plain = !!opts.plain;
|
|
2969
2970
|
const editReady = !!opts.editReady;
|
|
2971
|
+
const allowLargeOutput = !!opts.allowLargeOutput;
|
|
2970
2972
|
const defaultTotalLimit = output === "content" ? DEFAULT_TOTAL_LIMIT_CONTENT : DEFAULT_TOTAL_LIMIT_LIST;
|
|
2971
2973
|
const totalLimit = opts.totalLimit === 0 ? 0 : opts.totalLimit && opts.totalLimit > 0 ? opts.totalLimit : defaultTotalLimit;
|
|
2972
2974
|
if (output === "summary") return summaryMode(pattern, target, opts, totalLimit);
|
|
2973
2975
|
if (output === "files") return filesMode(pattern, target, opts, totalLimit);
|
|
2974
2976
|
if (output === "count") return countMode(pattern, target, opts, totalLimit);
|
|
2975
|
-
return contentMode(pattern, target, opts, plain, editReady, totalLimit);
|
|
2977
|
+
return contentMode(pattern, target, opts, plain, editReady, totalLimit, allowLargeOutput);
|
|
2976
2978
|
}
|
|
2977
2979
|
function applyListModeTotalLimit(lines, totalLimit) {
|
|
2978
2980
|
if (!totalLimit || totalLimit <= 0 || lines.length <= totalLimit) return lines.join("\n");
|
|
@@ -3054,10 +3056,24 @@ async function summaryMode(pattern, target, opts, totalLimit) {
|
|
|
3054
3056
|
}
|
|
3055
3057
|
return lines.join("\n");
|
|
3056
3058
|
}
|
|
3057
|
-
|
|
3059
|
+
function buildSearchRefineCall(target, pattern, opts) {
|
|
3060
|
+
const args = { path: String(target).replace(/\\/g, "/"), pattern };
|
|
3061
|
+
if (opts.glob) args.glob = opts.glob;
|
|
3062
|
+
if (opts.type) args.type = opts.type;
|
|
3063
|
+
return JSON.stringify({
|
|
3064
|
+
tool: "mcp__hex_line__grep_search",
|
|
3065
|
+
arguments: {
|
|
3066
|
+
...args,
|
|
3067
|
+
output: "summary"
|
|
3068
|
+
}
|
|
3069
|
+
});
|
|
3070
|
+
}
|
|
3071
|
+
async function contentMode(pattern, target, opts, plain, editReady, totalLimit, allowLargeOutput) {
|
|
3058
3072
|
const realArgs = ["--json"];
|
|
3059
3073
|
const plainOutput = plain || !editReady;
|
|
3060
3074
|
const shouldUseGraph = editReady && !plain;
|
|
3075
|
+
const contentBlockLimit = allowLargeOutput ? Number.POSITIVE_INFINITY : DEFAULT_CONTENT_BLOCK_LIMIT;
|
|
3076
|
+
const outputCharBudget = allowLargeOutput ? MAX_SEARCH_OUTPUT_CHARS : DEFAULT_CONTENT_OUTPUT_CHARS;
|
|
3061
3077
|
if (opts.caseInsensitive) realArgs.push("-i");
|
|
3062
3078
|
else if (opts.smartCase) realArgs.push("-S");
|
|
3063
3079
|
if (opts.literal) realArgs.push("-F");
|
|
@@ -3079,6 +3095,7 @@ async function contentMode(pattern, target, opts, plain, editReady, totalLimit)
|
|
|
3079
3095
|
const db = shouldUseGraph ? getGraphDB(target) : null;
|
|
3080
3096
|
const relCache = /* @__PURE__ */ new Map();
|
|
3081
3097
|
let annotationBudget = GRAPH_MATCH_ANNOTATION_BUDGET;
|
|
3098
|
+
const matchedFiles = /* @__PURE__ */ new Set();
|
|
3082
3099
|
let groupFile = null;
|
|
3083
3100
|
let groupEntries = [];
|
|
3084
3101
|
let matchCount = 0;
|
|
@@ -3123,6 +3140,7 @@ async function contentMode(pattern, target, opts, plain, editReady, totalLimit)
|
|
|
3123
3140
|
const filePath = (data.path?.text || "").replace(/\\/g, "/");
|
|
3124
3141
|
const lineNum = data.line_number;
|
|
3125
3142
|
if (!lineNum) continue;
|
|
3143
|
+
if (msg.type === "match") matchedFiles.add(filePath);
|
|
3126
3144
|
let content = data.lines?.text;
|
|
3127
3145
|
if (content === void 0 && data.lines?.bytes) {
|
|
3128
3146
|
content = Buffer.from(data.lines.bytes, "base64").toString("utf-8");
|
|
@@ -3172,6 +3190,15 @@ async function contentMode(pattern, target, opts, plain, editReady, totalLimit)
|
|
|
3172
3190
|
flushGroup();
|
|
3173
3191
|
blocks.push(buildDiagnosticBlock({
|
|
3174
3192
|
kind: "total_limit",
|
|
3193
|
+
meta: {
|
|
3194
|
+
total_matches: matchCount,
|
|
3195
|
+
shown_matches: matchCount,
|
|
3196
|
+
file_count: matchedFiles.size,
|
|
3197
|
+
shown_count: blocks.filter((block) => block.type === "edit_ready_block").length,
|
|
3198
|
+
truncated: true,
|
|
3199
|
+
next_action: "narrow_search_scope",
|
|
3200
|
+
suggested_refine_call: buildSearchRefineCall(target, pattern, opts)
|
|
3201
|
+
},
|
|
3175
3202
|
message: `Search stopped after ${totalLimit} match event(s). Narrow the query, raise total_limit, or pass total_limit=0 to disable the cap.`,
|
|
3176
3203
|
path: String(target).replace(/\\/g, "/")
|
|
3177
3204
|
}));
|
|
@@ -3181,10 +3208,18 @@ async function contentMode(pattern, target, opts, plain, editReady, totalLimit)
|
|
|
3181
3208
|
}
|
|
3182
3209
|
flushGroup();
|
|
3183
3210
|
if (db) blocks.sort((a, b) => (b.meta.graphScore || 0) - (a.meta.graphScore || 0));
|
|
3211
|
+
const searchBlocks = blocks.filter((block) => block.type === "edit_ready_block");
|
|
3212
|
+
const totalSearchBlocks = searchBlocks.length;
|
|
3184
3213
|
const parts = [];
|
|
3185
|
-
let budget =
|
|
3214
|
+
let budget = outputCharBudget;
|
|
3186
3215
|
let capped = false;
|
|
3216
|
+
let shownBlocks = 0;
|
|
3217
|
+
let shownMatches = 0;
|
|
3187
3218
|
for (const block of blocks) {
|
|
3219
|
+
if (block.type === "edit_ready_block" && shownBlocks >= contentBlockLimit) {
|
|
3220
|
+
capped = true;
|
|
3221
|
+
break;
|
|
3222
|
+
}
|
|
3188
3223
|
const serialized = block.type === "edit_ready_block" ? serializeSearchBlock(block, { plain: plainOutput }) : serializeDiagnosticBlock(block);
|
|
3189
3224
|
if (parts.length > 0 && budget - serialized.length < 0) {
|
|
3190
3225
|
capped = true;
|
|
@@ -3192,12 +3227,26 @@ async function contentMode(pattern, target, opts, plain, editReady, totalLimit)
|
|
|
3192
3227
|
}
|
|
3193
3228
|
parts.push(serialized);
|
|
3194
3229
|
budget -= serialized.length;
|
|
3230
|
+
if (block.type === "edit_ready_block") {
|
|
3231
|
+
shownBlocks++;
|
|
3232
|
+
shownMatches += Array.isArray(block.meta.matchLines) ? block.meta.matchLines.length : 0;
|
|
3233
|
+
}
|
|
3195
3234
|
}
|
|
3196
3235
|
if (capped) {
|
|
3197
|
-
const remaining =
|
|
3236
|
+
const remaining = Math.max(0, totalSearchBlocks - shownBlocks);
|
|
3198
3237
|
parts.push(serializeDiagnosticBlock(buildDiagnosticBlock({
|
|
3199
3238
|
kind: "output_capped",
|
|
3200
|
-
|
|
3239
|
+
path: String(target).replace(/\\/g, "/"),
|
|
3240
|
+
meta: {
|
|
3241
|
+
total_matches: matchCount,
|
|
3242
|
+
shown_matches: shownMatches,
|
|
3243
|
+
file_count: matchedFiles.size,
|
|
3244
|
+
shown_count: shownBlocks,
|
|
3245
|
+
truncated: true,
|
|
3246
|
+
next_action: "narrow_search_scope",
|
|
3247
|
+
suggested_refine_call: buildSearchRefineCall(target, pattern, opts)
|
|
3248
|
+
},
|
|
3249
|
+
message: `OUTPUT_CAPPED: ${remaining} more search block(s) omitted (${outputCharBudget} char limit${allowLargeOutput ? "" : `, ${DEFAULT_CONTENT_BLOCK_LIMIT} block default`}). Narrow with path= or glob= filters or pass allow_large_output=true when you intentionally need a larger payload.`
|
|
3201
3250
|
})));
|
|
3202
3251
|
}
|
|
3203
3252
|
return parts.join("\n\n");
|
|
@@ -3657,10 +3706,30 @@ function isIgnored(ig, relPath, isDir) {
|
|
|
3657
3706
|
if (!ig) return false;
|
|
3658
3707
|
return ig.ignores(isDir ? relPath + "/" : relPath);
|
|
3659
3708
|
}
|
|
3709
|
+
function topPatternGroups(matches) {
|
|
3710
|
+
const groups = /* @__PURE__ */ new Map();
|
|
3711
|
+
for (const entry of matches) {
|
|
3712
|
+
const trimmed = entry.endsWith("/") ? entry.slice(0, -1) : entry;
|
|
3713
|
+
const [head] = trimmed.split("/");
|
|
3714
|
+
const key = trimmed.includes("/") ? head : ".";
|
|
3715
|
+
groups.set(key, (groups.get(key) || 0) + 1);
|
|
3716
|
+
}
|
|
3717
|
+
return [...groups.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).slice(0, 4);
|
|
3718
|
+
}
|
|
3719
|
+
function buildPatternRefineCall(absRoot, pattern, type, groups) {
|
|
3720
|
+
const bestGroup = groups.find(([key]) => key !== ".")?.[0];
|
|
3721
|
+
const args = { path: bestGroup ? join4(absRoot, bestGroup) : absRoot, pattern };
|
|
3722
|
+
if (type && type !== "all") args.type = type;
|
|
3723
|
+
return JSON.stringify({
|
|
3724
|
+
tool: "mcp__hex_line__inspect_path",
|
|
3725
|
+
arguments: args
|
|
3726
|
+
});
|
|
3727
|
+
}
|
|
3660
3728
|
function findByPattern(dirPath, opts) {
|
|
3661
3729
|
const re = globToRegex(opts.pattern);
|
|
3662
3730
|
const filterType = opts.type || "all";
|
|
3663
3731
|
const maxDepth = opts.max_depth ?? 20;
|
|
3732
|
+
const maxEntries = opts.max_entries === 0 ? 0 : Math.max(1, Number(opts.max_entries ?? 60));
|
|
3664
3733
|
const abs = resolve4(normalizePath(dirPath));
|
|
3665
3734
|
if (!existsSync5(abs)) throw new Error(`DIRECTORY_NOT_FOUND: ${abs}`);
|
|
3666
3735
|
if (!statSync7(abs).isDirectory()) throw new Error(`Not a directory: ${abs}`);
|
|
@@ -3694,9 +3763,25 @@ function findByPattern(dirPath, opts) {
|
|
|
3694
3763
|
if (matches.length === 0) {
|
|
3695
3764
|
return `No matches for "${opts.pattern}" in ${rootName}/`;
|
|
3696
3765
|
}
|
|
3697
|
-
|
|
3698
|
-
|
|
3699
|
-
|
|
3766
|
+
const shown = maxEntries === 0 ? matches : matches.slice(0, maxEntries);
|
|
3767
|
+
const truncated = shown.length < matches.length;
|
|
3768
|
+
const groups = topPatternGroups(matches);
|
|
3769
|
+
const lines = [
|
|
3770
|
+
`Found ${matches.length} match${matches.length === 1 ? "" : "es"} for "${opts.pattern}" in ${rootName}/`,
|
|
3771
|
+
`match_count: ${matches.length}`,
|
|
3772
|
+
`shown_count: ${shown.length}`,
|
|
3773
|
+
`truncated: ${truncated}`
|
|
3774
|
+
];
|
|
3775
|
+
if (groups.length > 0) {
|
|
3776
|
+
lines.push(`top_groups: ${groups.map(([group, count]) => `${group} (${count})`).join(", ")}`);
|
|
3777
|
+
}
|
|
3778
|
+
if (truncated) {
|
|
3779
|
+
lines.push("next_action: narrow_path");
|
|
3780
|
+
lines.push(`suggested_refine_call: ${buildPatternRefineCall(abs, opts.pattern, filterType, groups)}`);
|
|
3781
|
+
}
|
|
3782
|
+
lines.push("");
|
|
3783
|
+
lines.push(...shown);
|
|
3784
|
+
return lines.join("\n");
|
|
3700
3785
|
}
|
|
3701
3786
|
function directoryTree(dirPath, opts = {}) {
|
|
3702
3787
|
if (opts.pattern) return findByPattern(dirPath, opts);
|
|
@@ -4638,7 +4723,7 @@ OUTPUT_CAPPED: Output exceeded ${MAX_BULK_OUTPUT_CHARS} chars.`;
|
|
|
4638
4723
|
}
|
|
4639
4724
|
|
|
4640
4725
|
// server.mjs
|
|
4641
|
-
var version = true ? "1.
|
|
4726
|
+
var version = true ? "1.17.1" : (await null).createRequire(import.meta.url)("./package.json").version;
|
|
4642
4727
|
var { server, StdioServerTransport } = await createServerRuntime({
|
|
4643
4728
|
name: "hex-line-mcp",
|
|
4644
4729
|
version
|
|
@@ -4801,7 +4886,8 @@ server.registerTool("grep_search", {
|
|
|
4801
4886
|
limit: flexNum().describe("Max matches per file (default: 20 for summary discovery, 100 for content)"),
|
|
4802
4887
|
total_limit: flexNum().describe("Total match events across all files; multiline matches count as 1 (default: 50 for summary discovery, 200 for content, 1000 for files/count, 0 = unlimited)"),
|
|
4803
4888
|
plain: flexBool().describe("Omit hash tags, return file:line:content"),
|
|
4804
|
-
edit_ready: flexBool().describe("Preserve hash/checksum search hunks in `content` mode. Default: false.")
|
|
4889
|
+
edit_ready: flexBool().describe("Preserve hash/checksum search hunks in `content` mode. Default: false."),
|
|
4890
|
+
allow_large_output: flexBool().describe("Bypass the default content-mode block/char caps when you intentionally need a larger payload.")
|
|
4805
4891
|
}),
|
|
4806
4892
|
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true }
|
|
4807
4893
|
}, async (rawParams) => {
|
|
@@ -4821,7 +4907,8 @@ server.registerTool("grep_search", {
|
|
|
4821
4907
|
limit,
|
|
4822
4908
|
total_limit,
|
|
4823
4909
|
plain,
|
|
4824
|
-
edit_ready
|
|
4910
|
+
edit_ready,
|
|
4911
|
+
allow_large_output
|
|
4825
4912
|
} = rawParams ?? {};
|
|
4826
4913
|
try {
|
|
4827
4914
|
const resolvedOutput = output ?? "summary";
|
|
@@ -4842,7 +4929,8 @@ server.registerTool("grep_search", {
|
|
|
4842
4929
|
limit: resolvedLimit,
|
|
4843
4930
|
totalLimit: resolvedTotalLimit,
|
|
4844
4931
|
plain,
|
|
4845
|
-
editReady: !!edit_ready
|
|
4932
|
+
editReady: !!edit_ready,
|
|
4933
|
+
allowLargeOutput: !!allow_large_output
|
|
4846
4934
|
});
|
|
4847
4935
|
return { content: [{ type: "text", text: result }] };
|
|
4848
4936
|
} catch (e) {
|
|
@@ -4893,13 +4981,14 @@ server.registerTool("inspect_path", {
|
|
|
4893
4981
|
pattern: z2.string().optional().describe('Glob filter on names (e.g. "*-mcp", "*.mjs"). Returns flat match list instead of tree'),
|
|
4894
4982
|
type: z2.enum(["file", "dir", "all"]).optional().describe('"file", "dir", or "all" (default). Like find -type f/d'),
|
|
4895
4983
|
max_depth: flexNum().describe("Max recursion depth (default: 2 for discovery, or 20 in pattern mode)"),
|
|
4984
|
+
max_entries: flexNum().describe("Max entries to show in pattern mode before truncation metadata is returned (default: 60, 0 = unlimited)"),
|
|
4896
4985
|
gitignore: flexBool().describe("Respect root .gitignore patterns (default: true). Nested .gitignore not supported"),
|
|
4897
4986
|
format: z2.enum(["compact", "full"]).optional().describe('"compact" = shorter path view, "full" = include sizes/metadata where available'),
|
|
4898
4987
|
verbosity: z2.enum(["minimal", "compact", "full"]).optional().describe("Response budget. `minimal` returns the shortest tree summary.")
|
|
4899
4988
|
}),
|
|
4900
4989
|
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true }
|
|
4901
4990
|
}, async (rawParams) => {
|
|
4902
|
-
const { path: p, max_depth, gitignore, format, pattern, type: entryType, verbosity } = rawParams ?? {};
|
|
4991
|
+
const { path: p, max_depth, max_entries, gitignore, format, pattern, type: entryType, verbosity } = rawParams ?? {};
|
|
4903
4992
|
try {
|
|
4904
4993
|
const resolvedVerbosity = verbosity ?? "minimal";
|
|
4905
4994
|
return { content: [{ type: "text", text: inspectPath(p, {
|
|
@@ -4907,7 +4996,8 @@ server.registerTool("inspect_path", {
|
|
|
4907
4996
|
gitignore,
|
|
4908
4997
|
format: format ?? (resolvedVerbosity === "full" ? "full" : "compact"),
|
|
4909
4998
|
pattern,
|
|
4910
|
-
type: entryType
|
|
4999
|
+
type: entryType,
|
|
5000
|
+
max_entries
|
|
4911
5001
|
}) }] };
|
|
4912
5002
|
} catch (e) {
|
|
4913
5003
|
return { content: [{ type: "text", text: e.message }], isError: true };
|
package/output-style.md
CHANGED
|
@@ -13,7 +13,8 @@ Prefer `hex-line` for text files you may inspect or modify. Hash-annotated reads
|
|
|
13
13
|
| Read | `mcp__hex-line__read_file` | Hash-annotated, revision-aware |
|
|
14
14
|
| Edit | `mcp__hex-line__edit_file` | Hash-verified anchors + conservative auto-rebase |
|
|
15
15
|
| Write | `mcp__hex-line__write_file` | No prior Read needed |
|
|
16
|
-
| Grep | `mcp__hex-line__grep_search` |
|
|
16
|
+
| Grep | `mcp__hex-line__grep_search` | Summary-first discovery with edit-ready escalation |
|
|
17
|
+
| Glob | `mcp__hex-line__inspect_path` | Project file discovery and name/path globbing |
|
|
17
18
|
| Text rename across files | `mcp__hex-line__bulk_replace` | Multi-file text rename/refactor inside an explicit root path |
|
|
18
19
|
| Path/tree/stat Bash | `mcp__hex-line__inspect_path` | Compact path info and pattern search |
|
|
19
20
|
| Large code read | `mcp__hex-line__outline` then `read_file` with ranges | Structure first, targeted reads |
|
|
@@ -26,7 +27,7 @@ Prefer `hex-line` for text files you may inspect or modify. Hash-annotated reads
|
|
|
26
27
|
|
|
27
28
|
| Path | Flow |
|
|
28
29
|
|------|------|
|
|
29
|
-
| Surgical | `grep_search -> edit_file` |
|
|
30
|
+
| Surgical | `grep_search(output="summary") -> grep_search(output="content", edit_ready=true) if needed -> edit_file` |
|
|
30
31
|
| Exploratory | `outline -> read_file (ranges) -> edit_file(base_revision)` |
|
|
31
32
|
| Multi-file | `bulk_replace(path=<project root>)` |
|
|
32
33
|
| Follow-up after delay | `verify(base_revision) -> reread only if STALE -> retry with returned helpers` |
|
|
@@ -36,14 +37,14 @@ Prefer `hex-line` for text files you may inspect or modify. Hash-annotated reads
|
|
|
36
37
|
- Auto-fill `path` instead of leaving scope implicit.
|
|
37
38
|
- For file tools (`read_file`, `edit_file`, `outline`, `changes` on one file), use the target file path.
|
|
38
39
|
- Read-only file tools may target explicit temp-file paths outside the repo when you intentionally inspect a scratch file.
|
|
39
|
-
- For repo-wide tools (`bulk_replace`, directory `inspect_path`, broad `grep_search`), use the resolved project root or intended directory scope.
|
|
40
|
+
- For repo-wide tools (`bulk_replace`, directory `inspect_path`, broad `grep_search`), use the resolved project root or intended directory scope, then narrow further before requesting rich output.
|
|
40
41
|
- Mutating tools stay inside the current project root by default. Add `allow_external=true` only when you intentionally edit a temp or external path.
|
|
41
42
|
- Treat missing or ambiguous scope as an error to fix, not as a reason to guess across repositories.
|
|
42
43
|
|
|
43
44
|
## Edit Discipline
|
|
44
45
|
|
|
45
|
-
- Never invent `range_checksum`. Copy it from a fresh `read_file` or `grep_search(output:"content")` block.
|
|
46
|
-
- First mutation in a file: use `grep_search` for narrow targets, or `outline -> read_file(ranges)` for structural edits.
|
|
46
|
+
- Never invent `range_checksum`. Copy it from a fresh `read_file` or `grep_search(output:"content", edit_ready=true)` block.
|
|
47
|
+
- First mutation in a file: use `grep_search(output="summary")` for narrow targets, or `outline -> read_file(ranges)` for structural edits. Escalate to `grep_search(output="content", edit_ready=true)` only when the next edit needs canonical hunks.
|
|
47
48
|
- Preserve file conventions mentally: `hex-line` hashes normalized logical text, but `edit_file` preserves the file's existing line endings and trailing-newline shape on write.
|
|
48
49
|
- Prefer `set_line` or `insert_after` for small local changes. Prefer `replace_between` for larger bounded block rewrites.
|
|
49
50
|
- Use `replace_lines` only when you already hold the exact inclusive range checksum for that block.
|
|
@@ -53,11 +54,12 @@ Prefer `hex-line` for text files you may inspect or modify. Hash-annotated reads
|
|
|
53
54
|
- Reuse `retry_checksum` when it is returned for the exact same target range.
|
|
54
55
|
- Once `hex-line` owns a file edit session, avoid mixing built-in `Edit`/`Write` on that file unless you intentionally want a new baseline.
|
|
55
56
|
- Follow `next_action` first. Treat `summary` and `snippet` as the compact local context, not as prose to reinterpret.
|
|
57
|
+
- If broad `grep_search(output="content")` or pattern `inspect_path` truncates, narrow `path`, `glob`, or query shape before retrying. Use `allow_large_output=true` only when you intentionally accept a larger payload.
|
|
56
58
|
|
|
57
59
|
## Exceptions
|
|
58
60
|
|
|
59
|
-
- Built-in `Read`/`Edit`/`Write`/`Grep` are fallback only. Built-in OK for images, PDFs, notebooks,
|
|
60
|
-
- Bash is still fine for npm, node, git, docker, curl,
|
|
61
|
+
- Built-in `Read`/`Edit`/`Write`/`Grep`/`Glob` are fallback only. With the hook active, project-scoped text calls and file discovery route to hex-line. Built-in OK for images, PDFs, notebooks, and text paths outside the current project root.
|
|
62
|
+
- Bash is still fine for npm, node, git, docker, curl, non-inspection pipelines, and other runtime workflows. Project file inspection commands route to hex-line, including Windows-native readers/searchers/listing commands.
|
|
61
63
|
|
|
62
64
|
## hex-graph
|
|
63
65
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@levnikolaevich/hex-line-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.17.1",
|
|
4
4
|
"mcpName": "io.github.levnikolaevich/hex-line-mcp",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Hash-verified file editing MCP + token efficiency hook for AI coding agents. 9 tools: inspect_path, read, edit, write, grep, outline, verify, changes, bulk_replace.",
|