@levnikolaevich/hex-line-mcp 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +76 -19
- package/benchmark.mjs +524 -598
- package/hook.mjs +62 -19
- package/lib/benchmark-helpers.mjs +541 -0
- package/lib/bulk-replace.mjs +8 -0
- package/lib/coerce.mjs +5 -0
- package/lib/edit.mjs +71 -31
- package/lib/read.mjs +44 -19
- package/lib/setup.mjs +134 -16
- package/lib/tree.mjs +84 -9
- package/output-style.md +27 -0
- package/package.json +3 -2
- package/server.mjs +23 -24
package/server.mjs
CHANGED
|
@@ -54,7 +54,7 @@ try {
|
|
|
54
54
|
process.exit(1);
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
const server = new McpServer({ name: "hex-line-mcp", version: "1.
|
|
57
|
+
const server = new McpServer({ name: "hex-line-mcp", version: "1.1.0" });
|
|
58
58
|
|
|
59
59
|
|
|
60
60
|
// ==================== read_file ====================
|
|
@@ -62,10 +62,10 @@ const server = new McpServer({ name: "hex-line-mcp", version: "1.0.0" });
|
|
|
62
62
|
server.registerTool("read_file", {
|
|
63
63
|
title: "Read File",
|
|
64
64
|
description:
|
|
65
|
-
"Read a file with FNV-1a hash-annotated lines
|
|
66
|
-
"
|
|
67
|
-
"For
|
|
68
|
-
"
|
|
65
|
+
"Read a file with FNV-1a hash-annotated lines and range checksums. " +
|
|
66
|
+
"Directory listing if path is a directory. " +
|
|
67
|
+
"For files >100 lines: ALWAYS use outline first, then read_file with offset/limit for specific sections. " +
|
|
68
|
+
"Reading a 500+ line file in full wastes 75% of context tokens.",
|
|
69
69
|
inputSchema: z.object({
|
|
70
70
|
path: z.string().optional().describe("File or directory path"),
|
|
71
71
|
paths: z.array(z.string()).optional().describe("Array of file paths to read (batch mode)"),
|
|
@@ -102,9 +102,8 @@ server.registerTool("edit_file", {
|
|
|
102
102
|
title: "Edit File",
|
|
103
103
|
description:
|
|
104
104
|
"Edit a file using hash-verified anchors or text replacement. Returns diff. " +
|
|
105
|
-
"
|
|
106
|
-
"
|
|
107
|
-
"For anchor-based edits, use read_file first to get hashes. For text replace, read_file is optional.",
|
|
105
|
+
"new_text replaces anchor range exactly — include boundary lines if you want to keep them. " +
|
|
106
|
+
"Preserve indentation from read_file. For anchor edits, read_file first to get hashes.",
|
|
108
107
|
inputSchema: z.object({
|
|
109
108
|
path: z.string().describe("File to edit"),
|
|
110
109
|
edits: z.string().describe(
|
|
@@ -115,14 +114,15 @@ server.registerTool("edit_file", {
|
|
|
115
114
|
'{"replace":{"old_text":"find","new_text":"replace","all":false}} — text match',
|
|
116
115
|
),
|
|
117
116
|
dry_run: flexBool().describe("Preview changes without writing"),
|
|
117
|
+
restore_indent: flexBool().describe("Auto-fix indentation to match anchor (default: false)"),
|
|
118
118
|
}),
|
|
119
119
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false },
|
|
120
120
|
}, async (rawParams) => {
|
|
121
|
-
const { path: p, edits: json, dry_run } = coerceParams(rawParams);
|
|
121
|
+
const { path: p, edits: json, dry_run, restore_indent } = coerceParams(rawParams);
|
|
122
122
|
try {
|
|
123
123
|
const parsed = JSON.parse(json);
|
|
124
124
|
if (!Array.isArray(parsed) || !parsed.length) throw new Error("Edits: non-empty JSON array required");
|
|
125
|
-
return { content: [{ type: "text", text: editFile(p, parsed, { dryRun: dry_run }) }] };
|
|
125
|
+
return { content: [{ type: "text", text: editFile(p, parsed, { dryRun: dry_run, restoreIndent: restore_indent }) }] };
|
|
126
126
|
} catch (e) {
|
|
127
127
|
return { content: [{ type: "text", text: e.message }], isError: true };
|
|
128
128
|
}
|
|
@@ -160,9 +160,7 @@ server.registerTool("grep_search", {
|
|
|
160
160
|
title: "Search Files",
|
|
161
161
|
description:
|
|
162
162
|
"Search file contents with ripgrep. Returns hash-annotated matches for direct editing. " +
|
|
163
|
-
"ALWAYS prefer over shell grep/rg/findstr
|
|
164
|
-
"Use to find code locations before read_file or edit_file. " +
|
|
165
|
-
"When codegraph DB available (.codegraph/index.db), matches annotated with symbol type and call counts [fn N↓ M↑].",
|
|
163
|
+
"ALWAYS prefer over shell grep/rg/findstr. Use to find code before read_file or edit_file.",
|
|
166
164
|
inputSchema: z.object({
|
|
167
165
|
pattern: z.string().describe("Regex search pattern"),
|
|
168
166
|
path: z.string().optional().describe("Search dir/file (default: cwd)"),
|
|
@@ -193,10 +191,8 @@ server.registerTool("outline", {
|
|
|
193
191
|
title: "File Outline",
|
|
194
192
|
description:
|
|
195
193
|
"AST-based structural outline: functions, classes, interfaces with line ranges. " +
|
|
196
|
-
"Code files only (.js/.ts/.py/.go/.rs/.java/.c/.cpp/.cs/.rb/.php/.kt/.swift/.sh). " +
|
|
197
|
-
"NOT for .md/.json/.yaml/.txt — use read_file directly for those. " +
|
|
198
194
|
"10-20 lines instead of 500 — 95% token reduction. " +
|
|
199
|
-
"
|
|
195
|
+
"Use before reading large code files. NOT for .md/.json/.yaml — use read_file.",
|
|
200
196
|
inputSchema: z.object({
|
|
201
197
|
path: z.string().describe("Source file path"),
|
|
202
198
|
}),
|
|
@@ -218,8 +214,7 @@ server.registerTool("verify", {
|
|
|
218
214
|
title: "Verify Checksums",
|
|
219
215
|
description:
|
|
220
216
|
"Check if range checksums from prior reads are still valid. " +
|
|
221
|
-
"Single-line response when nothing changed.
|
|
222
|
-
"Use to check if file changed since last read, without re-reading.",
|
|
217
|
+
"Single-line response when nothing changed. Use to check file staleness without re-reading.",
|
|
223
218
|
inputSchema: z.object({
|
|
224
219
|
path: z.string().describe("File path"),
|
|
225
220
|
checksums: z.string().describe('JSON array of checksum strings, e.g. ["1-50:f7e2a1b0", "51-100:abcd1234"]'),
|
|
@@ -242,20 +237,23 @@ server.registerTool("verify", {
|
|
|
242
237
|
server.registerTool("directory_tree", {
|
|
243
238
|
title: "Directory Tree",
|
|
244
239
|
description:
|
|
245
|
-
"Compact directory tree with
|
|
246
|
-
"
|
|
240
|
+
"Compact directory tree with .gitignore support. " +
|
|
241
|
+
"Supports pattern glob to find files/dirs by name (like find -name). " +
|
|
242
|
+
"Use to understand repo structure or find specific files/dirs. " +
|
|
247
243
|
"Skips node_modules, .git, dist by default.",
|
|
248
244
|
inputSchema: z.object({
|
|
249
245
|
path: z.string().describe("Directory path"),
|
|
250
|
-
|
|
246
|
+
pattern: z.string().optional().describe('Glob filter on names (e.g. "*-mcp", "*.mjs"). Returns flat match list instead of tree'),
|
|
247
|
+
type: z.enum(["file", "dir", "all"]).optional().describe('"file", "dir", or "all" (default). Like find -type f/d'),
|
|
248
|
+
max_depth: flexNum().describe("Max recursion depth (default: 3, or 20 in pattern mode)"),
|
|
251
249
|
gitignore: flexBool().describe("Respect .gitignore patterns (default: true)"),
|
|
252
250
|
format: z.enum(["compact", "full"]).optional().describe('"compact" = names only, no sizes, depth 1. "full" = default with sizes'),
|
|
253
251
|
}),
|
|
254
252
|
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true },
|
|
255
253
|
}, async (rawParams) => {
|
|
256
|
-
const { path: p, max_depth, gitignore, format } = coerceParams(rawParams);
|
|
254
|
+
const { path: p, max_depth, gitignore, format, pattern, type: entryType } = coerceParams(rawParams);
|
|
257
255
|
try {
|
|
258
|
-
return { content: [{ type: "text", text: directoryTree(p, { max_depth, gitignore, format }) }] };
|
|
256
|
+
return { content: [{ type: "text", text: directoryTree(p, { max_depth, gitignore, format, pattern, type: entryType }) }] };
|
|
259
257
|
} catch (e) {
|
|
260
258
|
return { content: [{ type: "text", text: e.message }], isError: true };
|
|
261
259
|
}
|
|
@@ -289,7 +287,8 @@ server.registerTool("setup_hooks", {
|
|
|
289
287
|
title: "Setup Hooks",
|
|
290
288
|
description:
|
|
291
289
|
"Configure hex-line hooks in CLI agent settings. " +
|
|
292
|
-
"Claude: writes
|
|
290
|
+
"Claude: writes hooks to ~/.claude/settings.json (global) with absolute path, " +
|
|
291
|
+
"removes old hooks from per-project settings.local.json. " +
|
|
293
292
|
"Gemini/Codex: returns guidance (no hook support). " +
|
|
294
293
|
"Idempotent: re-running produces no changes if already configured.",
|
|
295
294
|
inputSchema: z.object({
|