@levnikolaevich/hex-line-mcp 1.3.6 → 1.4.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 +16 -28
- package/dist/hook.mjs +4 -4
- package/dist/server.mjs +66 -43
- package/output-style.md +5 -3
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -42,7 +42,7 @@ Advanced / occasional:
|
|
|
42
42
|
| `get_file_info` | File metadata without reading content | Size, lines, mtime, type, binary detection |
|
|
43
43
|
| `setup_hooks` | Configure Claude hooks + install output style | Gemini/Codex get guidance only; no hooks |
|
|
44
44
|
| `changes` | Compare file against git ref, shows added/removed/modified symbols | AST-level semantic diff |
|
|
45
|
-
| `bulk_replace` | Search-and-replace across multiple files by glob |
|
|
45
|
+
| `bulk_replace` | Search-and-replace across multiple files by glob | Compact summary (default) or capped diffs via `format`, dry_run, max_files |
|
|
46
46
|
|
|
47
47
|
### Hooks (PreToolUse + PostToolUse)
|
|
48
48
|
|
|
@@ -90,45 +90,33 @@ The `setup_hooks` tool automatically installs the output style to `~/.claude/out
|
|
|
90
90
|
|
|
91
91
|
## Benchmarking
|
|
92
92
|
|
|
93
|
-
|
|
93
|
+
Two-tier benchmark architecture:
|
|
94
94
|
|
|
95
|
-
- `
|
|
96
|
-
- `
|
|
97
|
-
- `diagnostics` — modeled tool-level measurements for engineering inspection
|
|
98
|
-
|
|
99
|
-
Public benchmark mode reports only comparative multi-step workflows:
|
|
95
|
+
- `/benchmark-compare` — real 1:1 comparison (runs inside Claude Code, calls BOTH built-in and hex-line tools on same files)
|
|
96
|
+
- `npm run benchmark` — hex-line standalone metrics (Node.js, all real library calls, no simulations)
|
|
100
97
|
|
|
101
98
|
```bash
|
|
102
99
|
npm run benchmark -- --repo /path/to/repo
|
|
103
|
-
```
|
|
104
|
-
|
|
105
|
-
Optional diagnostics stay available separately:
|
|
106
|
-
|
|
107
|
-
```bash
|
|
108
100
|
npm run benchmark:diagnostic -- --repo /path/to/repo
|
|
109
|
-
npm run benchmark:diagnostic:graph -- --repo /path/to/repo
|
|
110
101
|
```
|
|
111
102
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
Current sample run on the `hex-line-mcp` repo with session-derived workflows:
|
|
115
|
-
|
|
116
|
-
| ID | Workflow | Built-in | hex-line | Savings | Ops |
|
|
117
|
-
|----|----------|---------:|---------:|--------:|----:|
|
|
118
|
-
| W1 | Debug hook file-listing redirect | 23,143 chars | 882 chars | 96% | 3→2 |
|
|
119
|
-
| W2 | Adjust `setup_hooks` guidance and verify | 24,877 chars | 1,637 chars | 93% | 3→3 |
|
|
120
|
-
| W3 | Repo-wide benchmark wording refresh | 137,796 chars | 38,918 chars | 72% | 15→1 |
|
|
121
|
-
| W4 | Inspect large smoke test before edit | 49,566 chars | 2,104 chars | 96% | 3→3 |
|
|
103
|
+
Current hex-line workflow metrics on the `hex-line-mcp` repo (all real library calls):
|
|
122
104
|
|
|
123
|
-
|
|
105
|
+
| # | Workflow | Hex-line output | Ops |
|
|
106
|
+
|---|----------|---------:|----:|
|
|
107
|
+
| W1 | Debug hook file-listing redirect | 882 chars | 2 |
|
|
108
|
+
| W2 | Adjust `setup_hooks` guidance and verify | 1,719 chars | 3 |
|
|
109
|
+
| W3 | Repo-wide benchmark wording refresh | 213 chars | 1 |
|
|
110
|
+
| W4 | Inspect large smoke test before edit | 2,322 chars | 3 |
|
|
111
|
+
| W5 | Follow-up edit after unrelated line shift | 1,267 chars | 3 |
|
|
124
112
|
|
|
125
|
-
|
|
113
|
+
Workflow total: `6,403` chars across `12` ops. Run `/benchmark-compare` in Claude Code for full built-in vs hex-line comparison with real tool calls on both sides.
|
|
126
114
|
|
|
127
115
|
### Optional Graph Enrichment
|
|
128
116
|
|
|
129
|
-
If a project already has `.codegraph/index.db`, `hex-line` can add lightweight graph hints to `read_file`, `outline`, `grep_search`, and `edit_file`.
|
|
117
|
+
If a project already has `.hex-skills/codegraph/index.db`, `hex-line` can add lightweight graph hints to `read_file`, `outline`, `grep_search`, and `edit_file`.
|
|
130
118
|
|
|
131
|
-
- Graph enrichment is optional. If `.codegraph/index.db` is missing, `hex-line` falls back to standard behavior silently.
|
|
119
|
+
- Graph enrichment is optional. If `.hex-skills/codegraph/index.db` is missing, `hex-line` falls back to standard behavior silently.
|
|
132
120
|
- `better-sqlite3` is optional. If it is unavailable, `hex-line` still works without graph hints.
|
|
133
121
|
- `edit_file` reports **Call impact**, not full semantic blast radius. The warning uses call-graph callers only.
|
|
134
122
|
|
|
@@ -160,7 +148,7 @@ Use `replace_between` inside `edit_file` when you know stable start/end anchors
|
|
|
160
148
|
|
|
161
149
|
### Literal rename / refactor
|
|
162
150
|
|
|
163
|
-
Use `bulk_replace` for text rename patterns across one or more files. Do not use it as a substitute for structured block rewrites.
|
|
151
|
+
Use `bulk_replace` for text rename patterns across one or more files. Returns compact summary by default; pass `format: "full"` for capped diffs. Do not use it as a substitute for structured block rewrites.
|
|
164
152
|
|
|
165
153
|
### read_file
|
|
166
154
|
|
package/dist/hook.mjs
CHANGED
|
@@ -91,13 +91,13 @@ var BINARY_EXT = /* @__PURE__ */ new Set([
|
|
|
91
91
|
]);
|
|
92
92
|
var REVERSE_TOOL_HINTS = {
|
|
93
93
|
"mcp__hex-line__read_file": "Read (file_path, offset, limit)",
|
|
94
|
-
"mcp__hex-line__edit_file": "Edit (
|
|
94
|
+
"mcp__hex-line__edit_file": "Edit (old_string, new_string, replace_all)",
|
|
95
95
|
"mcp__hex-line__write_file": "Write (file_path, content)",
|
|
96
96
|
"mcp__hex-line__grep_search": "Grep (pattern, path)",
|
|
97
97
|
"mcp__hex-line__directory_tree": "Glob (pattern) or Bash(ls)",
|
|
98
98
|
"mcp__hex-line__get_file_info": "Bash(stat/wc)",
|
|
99
99
|
"mcp__hex-line__outline": "Read with offset/limit",
|
|
100
|
-
"mcp__hex-line__verify": "
|
|
100
|
+
"mcp__hex-line__verify": "Read (re-read file to check freshness)",
|
|
101
101
|
"mcp__hex-line__changes": "Bash(git diff)",
|
|
102
102
|
"mcp__hex-line__bulk_replace": "Edit (text rename/refactor across files)",
|
|
103
103
|
"mcp__hex-line__setup_hooks": "Not available (hex-line disabled)"
|
|
@@ -114,10 +114,10 @@ var TOOL_HINTS = {
|
|
|
114
114
|
stat: "mcp__hex-line__get_file_info (not stat/wc/file)",
|
|
115
115
|
grep: "mcp__hex-line__grep_search (not grep/rg). Params: output, literal, context_before, context_after, multiline",
|
|
116
116
|
sed: "mcp__hex-line__edit_file for hash edits, or mcp__hex-line__bulk_replace for text rename (not sed -i)",
|
|
117
|
-
diff: "mcp__hex-line__changes (not diff). Git
|
|
117
|
+
diff: "mcp__hex-line__changes (not diff). Git diff with change symbols",
|
|
118
118
|
outline: "mcp__hex-line__outline (before reading large code files)",
|
|
119
119
|
verify: "mcp__hex-line__verify (staleness / revision check without re-read)",
|
|
120
|
-
changes: "mcp__hex-line__changes (
|
|
120
|
+
changes: "mcp__hex-line__changes (git diff with change symbols)",
|
|
121
121
|
bulk: "mcp__hex-line__bulk_replace (multi-file search-replace)",
|
|
122
122
|
setup: "mcp__hex-line__setup_hooks (configure hooks for agents)"
|
|
123
123
|
};
|
package/dist/server.mjs
CHANGED
|
@@ -6,7 +6,7 @@ import { dirname as dirname4 } from "node:path";
|
|
|
6
6
|
import { z as z2 } from "zod";
|
|
7
7
|
|
|
8
8
|
// ../hex-common/src/runtime/mcp-bootstrap.mjs
|
|
9
|
-
async function createServerRuntime({ name, version: version2
|
|
9
|
+
async function createServerRuntime({ name, version: version2 }) {
|
|
10
10
|
let McpServer, StdioServerTransport2;
|
|
11
11
|
try {
|
|
12
12
|
({ McpServer } = await import("@modelcontextprotocol/sdk/server/mcp.js"));
|
|
@@ -14,11 +14,16 @@ async function createServerRuntime({ name, version: version2, installDir }) {
|
|
|
14
14
|
} catch {
|
|
15
15
|
process.stderr.write(
|
|
16
16
|
`${name}: @modelcontextprotocol/sdk not found.
|
|
17
|
-
Run:
|
|
17
|
+
Run: npm install @modelcontextprotocol/sdk
|
|
18
18
|
`
|
|
19
19
|
);
|
|
20
20
|
process.exit(1);
|
|
21
21
|
}
|
|
22
|
+
const shutdown = () => {
|
|
23
|
+
process.exit(0);
|
|
24
|
+
};
|
|
25
|
+
process.on("SIGTERM", shutdown);
|
|
26
|
+
process.on("SIGINT", shutdown);
|
|
22
27
|
return {
|
|
23
28
|
server: new McpServer({ name, version: version2 }),
|
|
24
29
|
StdioServerTransport: StdioServerTransport2
|
|
@@ -250,6 +255,8 @@ function listDirectory(dirPath, opts = {}) {
|
|
|
250
255
|
}
|
|
251
256
|
var MAX_OUTPUT_CHARS = 8e4;
|
|
252
257
|
var MAX_DIFF_CHARS = 3e4;
|
|
258
|
+
var MAX_BULK_OUTPUT_CHARS = 3e4;
|
|
259
|
+
var MAX_PER_FILE_DIFF_LINES = 50;
|
|
253
260
|
function readText(filePath) {
|
|
254
261
|
return readFileSync(filePath, "utf-8").replace(/\r\n/g, "\n");
|
|
255
262
|
}
|
|
@@ -340,7 +347,7 @@ function getGraphDB(filePath) {
|
|
|
340
347
|
try {
|
|
341
348
|
const projectRoot = findProjectRoot(filePath);
|
|
342
349
|
if (!projectRoot) return null;
|
|
343
|
-
const dbPath = join3(projectRoot, ".codegraph", "index.db");
|
|
350
|
+
const dbPath = join3(projectRoot, ".hex-skills/codegraph", "index.db");
|
|
344
351
|
if (!existsSync2(dbPath)) return null;
|
|
345
352
|
if (_dbs.has(dbPath)) return _dbs.get(dbPath);
|
|
346
353
|
const require2 = createRequire(import.meta.url);
|
|
@@ -454,7 +461,7 @@ function getRelativePath(filePath) {
|
|
|
454
461
|
function findProjectRoot(filePath) {
|
|
455
462
|
let dir = dirname2(filePath);
|
|
456
463
|
for (let i = 0; i < 10; i++) {
|
|
457
|
-
if (existsSync2(join3(dir, ".codegraph", "index.db"))) return dir;
|
|
464
|
+
if (existsSync2(join3(dir, ".hex-skills/codegraph", "index.db"))) return dir;
|
|
458
465
|
const parent = dirname2(dir);
|
|
459
466
|
if (parent === dir) break;
|
|
460
467
|
dir = parent;
|
|
@@ -1964,19 +1971,18 @@ function fileInfo(filePath) {
|
|
|
1964
1971
|
}
|
|
1965
1972
|
|
|
1966
1973
|
// lib/setup.mjs
|
|
1967
|
-
import { readFileSync as readFileSync4, writeFileSync as writeFileSync2, existsSync as existsSync5, mkdirSync } from "node:fs";
|
|
1968
|
-
import { resolve as resolve6, dirname as dirname3 } from "node:path";
|
|
1974
|
+
import { readFileSync as readFileSync4, writeFileSync as writeFileSync2, existsSync as existsSync5, mkdirSync, copyFileSync } from "node:fs";
|
|
1975
|
+
import { resolve as resolve6, dirname as dirname3, join as join5 } from "node:path";
|
|
1969
1976
|
import { fileURLToPath } from "node:url";
|
|
1970
1977
|
import { homedir } from "node:os";
|
|
1978
|
+
var STABLE_HOOK_DIR = resolve6(homedir(), ".claude", "hex-line");
|
|
1979
|
+
var STABLE_HOOK_PATH = join5(STABLE_HOOK_DIR, "hook.mjs").replace(/\\/g, "/");
|
|
1980
|
+
var HOOK_COMMAND = `node ${STABLE_HOOK_PATH}`;
|
|
1971
1981
|
var __filename = fileURLToPath(import.meta.url);
|
|
1972
1982
|
var __dirname = dirname3(__filename);
|
|
1973
|
-
var
|
|
1974
|
-
var
|
|
1975
|
-
var HOOK_SIGNATURE = "hex-line
|
|
1976
|
-
var NPX_MARKERS = ["_npx", "npx-cache", ".npm/_npx"];
|
|
1977
|
-
function isEphemeralInstall(scriptPath) {
|
|
1978
|
-
return NPX_MARKERS.some((m) => scriptPath.includes(m));
|
|
1979
|
-
}
|
|
1983
|
+
var SOURCE_HOOK = resolve6(__dirname, "..", "hook.mjs");
|
|
1984
|
+
var DIST_HOOK = resolve6(__dirname, "hook.mjs");
|
|
1985
|
+
var HOOK_SIGNATURE = "hex-line";
|
|
1980
1986
|
var CLAUDE_HOOKS = {
|
|
1981
1987
|
SessionStart: {
|
|
1982
1988
|
matcher: "*",
|
|
@@ -2038,7 +2044,7 @@ function writeHooksToFile(settingsPath, label) {
|
|
|
2038
2044
|
return `Claude (${label}): already configured`;
|
|
2039
2045
|
}
|
|
2040
2046
|
writeJson(settingsPath, config);
|
|
2041
|
-
return `Claude (${label}): hooks -> ${
|
|
2047
|
+
return `Claude (${label}): hooks -> ${STABLE_HOOK_PATH} OK`;
|
|
2042
2048
|
}
|
|
2043
2049
|
function cleanLocalHooks() {
|
|
2044
2050
|
const localPath = resolve6(process.cwd(), ".claude/settings.local.json");
|
|
@@ -2084,10 +2090,14 @@ function installOutputStyle() {
|
|
|
2084
2090
|
return msg;
|
|
2085
2091
|
}
|
|
2086
2092
|
function setupClaude() {
|
|
2087
|
-
if (isEphemeralInstall(HOOK_SCRIPT)) {
|
|
2088
|
-
return "Claude: SKIPPED \u2014 hook.mjs is in npx cache (ephemeral). Install permanently: npm i -g @levnikolaevich/hex-line-mcp, then re-run setup_hooks.";
|
|
2089
|
-
}
|
|
2090
2093
|
const results = [];
|
|
2094
|
+
const hookSource = existsSync5(DIST_HOOK) ? DIST_HOOK : SOURCE_HOOK;
|
|
2095
|
+
if (!existsSync5(hookSource)) {
|
|
2096
|
+
return "Claude: FAILED \u2014 hook.mjs not found. Reinstall @levnikolaevich/hex-line-mcp.";
|
|
2097
|
+
}
|
|
2098
|
+
mkdirSync(STABLE_HOOK_DIR, { recursive: true });
|
|
2099
|
+
copyFileSync(hookSource, STABLE_HOOK_PATH);
|
|
2100
|
+
results.push(`hook.mjs -> ${STABLE_HOOK_PATH}`);
|
|
2091
2101
|
const globalPath = resolve6(homedir(), ".claude/settings.json");
|
|
2092
2102
|
results.push(writeHooksToFile(globalPath, "global"));
|
|
2093
2103
|
results.push(cleanLocalHooks());
|
|
@@ -2266,7 +2276,7 @@ Summary: ${summary}`);
|
|
|
2266
2276
|
|
|
2267
2277
|
// lib/bulk-replace.mjs
|
|
2268
2278
|
import { writeFileSync as writeFileSync3, readdirSync as readdirSync3 } from "node:fs";
|
|
2269
|
-
import { resolve as resolve7, relative as relative3, join as
|
|
2279
|
+
import { resolve as resolve7, relative as relative3, join as join6 } from "node:path";
|
|
2270
2280
|
var ignoreMod;
|
|
2271
2281
|
try {
|
|
2272
2282
|
ignoreMod = await import("ignore");
|
|
@@ -2282,7 +2292,7 @@ function walkFiles(dir, rootDir, ig) {
|
|
|
2282
2292
|
}
|
|
2283
2293
|
for (const e of entries) {
|
|
2284
2294
|
if (e.name === ".git" || e.name === "node_modules") continue;
|
|
2285
|
-
const full =
|
|
2295
|
+
const full = join6(dir, e.name);
|
|
2286
2296
|
const rel = relative3(rootDir, full).replace(/\\/g, "/");
|
|
2287
2297
|
if (ig && ig.ignores(rel)) continue;
|
|
2288
2298
|
if (e.isDirectory()) {
|
|
@@ -2294,21 +2304,21 @@ function walkFiles(dir, rootDir, ig) {
|
|
|
2294
2304
|
return results;
|
|
2295
2305
|
}
|
|
2296
2306
|
function globMatch(filename, pattern) {
|
|
2297
|
-
const re = pattern.replace(/\./g, "\\.").replace(/\*\*/g, "\0").replace(/\*/g, "[^/]*").replace(/\0/g, ".*").replace(/\?/g, ".");
|
|
2307
|
+
const re = pattern.replace(/\./g, "\\.").replace(/\{([^}]+)\}/g, (_, alts) => "(" + alts.split(",").join("|") + ")").replace(/\*\*/g, "\0").replace(/\*/g, "[^/]*").replace(/\0/g, ".*").replace(/\?/g, ".");
|
|
2298
2308
|
return new RegExp("^" + re + "$").test(filename);
|
|
2299
2309
|
}
|
|
2300
2310
|
function loadGitignore2(rootDir) {
|
|
2301
2311
|
if (!ignoreMod) return null;
|
|
2302
2312
|
const ig = (ignoreMod.default || ignoreMod)();
|
|
2303
2313
|
try {
|
|
2304
|
-
const content = readText(
|
|
2314
|
+
const content = readText(join6(rootDir, ".gitignore"));
|
|
2305
2315
|
ig.add(content);
|
|
2306
2316
|
} catch {
|
|
2307
2317
|
}
|
|
2308
2318
|
return ig;
|
|
2309
2319
|
}
|
|
2310
2320
|
function bulkReplace(rootDir, globPattern, replacements, opts = {}) {
|
|
2311
|
-
const { dryRun = false, maxFiles = 100 } = opts;
|
|
2321
|
+
const { dryRun = false, maxFiles = 100, format = "compact" } = opts;
|
|
2312
2322
|
const abs = resolve7(normalizePath(rootDir));
|
|
2313
2323
|
const ig = loadGitignore2(abs);
|
|
2314
2324
|
const allFiles = walkFiles(abs, abs, ig);
|
|
@@ -2321,51 +2331,63 @@ function bulkReplace(rootDir, globPattern, replacements, opts = {}) {
|
|
|
2321
2331
|
return `TOO_MANY_FILES: Found ${files.length} files, max_files is ${maxFiles}. Use more specific glob or increase max_files.`;
|
|
2322
2332
|
}
|
|
2323
2333
|
const results = [];
|
|
2324
|
-
let changed = 0, skipped = 0, errors = 0;
|
|
2325
|
-
const MAX_OUTPUT2 = MAX_OUTPUT_CHARS;
|
|
2326
|
-
let totalChars = 0;
|
|
2334
|
+
let changed = 0, skipped = 0, errors = 0, totalReplacements = 0;
|
|
2327
2335
|
for (const file of files) {
|
|
2328
2336
|
try {
|
|
2329
2337
|
const original = readText(file);
|
|
2330
2338
|
let content = original;
|
|
2339
|
+
let replacementCount = 0;
|
|
2331
2340
|
for (const { old: oldText, new: newText } of replacements) {
|
|
2332
|
-
|
|
2341
|
+
if (oldText === newText) continue;
|
|
2342
|
+
const parts = content.split(oldText);
|
|
2343
|
+
replacementCount += parts.length - 1;
|
|
2344
|
+
content = parts.join(newText);
|
|
2333
2345
|
}
|
|
2334
2346
|
if (content === original) {
|
|
2335
2347
|
skipped++;
|
|
2336
2348
|
continue;
|
|
2337
2349
|
}
|
|
2338
|
-
const diff = simpleDiff(original.split("\n"), content.split("\n"));
|
|
2339
2350
|
if (!dryRun) {
|
|
2340
2351
|
writeFileSync3(file, content, "utf-8");
|
|
2341
2352
|
}
|
|
2342
|
-
const relPath =
|
|
2343
|
-
|
|
2344
|
-
${diff || "(no visible diff)"}`);
|
|
2353
|
+
const relPath = relative3(abs, file).replace(/\\/g, "/");
|
|
2354
|
+
totalReplacements += replacementCount;
|
|
2345
2355
|
changed++;
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2356
|
+
if (format === "full") {
|
|
2357
|
+
const diff = simpleDiff(original.split("\n"), content.split("\n"));
|
|
2358
|
+
let diffText = diff || "(no visible diff)";
|
|
2359
|
+
const diffLines3 = diffText.split("\n");
|
|
2360
|
+
if (diffLines3.length > MAX_PER_FILE_DIFF_LINES) {
|
|
2361
|
+
const omitted = diffLines3.length - MAX_PER_FILE_DIFF_LINES;
|
|
2362
|
+
diffText = diffLines3.slice(0, MAX_PER_FILE_DIFF_LINES).join("\n") + `
|
|
2363
|
+
--- ${omitted} lines omitted ---`;
|
|
2364
|
+
}
|
|
2365
|
+
results.push(`--- ${relPath}: ${replacementCount} replacements
|
|
2366
|
+
${diffText}`);
|
|
2367
|
+
} else {
|
|
2368
|
+
results.push(`--- ${relPath}: ${replacementCount} replacements`);
|
|
2351
2369
|
}
|
|
2352
2370
|
} catch (e) {
|
|
2353
2371
|
results.push(`ERROR: ${file}: ${e.message}`);
|
|
2354
2372
|
errors++;
|
|
2355
2373
|
}
|
|
2356
2374
|
}
|
|
2357
|
-
const header = `Bulk replace: ${changed} files changed, ${skipped} skipped, ${errors} errors (dry_run: ${dryRun})`;
|
|
2358
|
-
|
|
2375
|
+
const header = `Bulk replace: ${changed} files changed (${totalReplacements} replacements), ${skipped} skipped, ${errors} errors (dry_run: ${dryRun})`;
|
|
2376
|
+
let output = results.length ? `${header}
|
|
2359
2377
|
|
|
2360
2378
|
${results.join("\n\n")}` : header;
|
|
2379
|
+
if (output.length > MAX_BULK_OUTPUT_CHARS) {
|
|
2380
|
+
output = output.slice(0, MAX_BULK_OUTPUT_CHARS) + `
|
|
2381
|
+
OUTPUT_CAPPED: Output exceeded ${MAX_BULK_OUTPUT_CHARS} chars.`;
|
|
2382
|
+
}
|
|
2383
|
+
return output;
|
|
2361
2384
|
}
|
|
2362
2385
|
|
|
2363
2386
|
// server.mjs
|
|
2364
|
-
var version = true ? "1.
|
|
2387
|
+
var version = true ? "1.4.0" : (await null).createRequire(import.meta.url)("./package.json").version;
|
|
2365
2388
|
var { server, StdioServerTransport } = await createServerRuntime({
|
|
2366
2389
|
name: "hex-line-mcp",
|
|
2367
|
-
version
|
|
2368
|
-
installDir: "mcp/hex-line-mcp"
|
|
2390
|
+
version
|
|
2369
2391
|
});
|
|
2370
2392
|
var replacementPairsSchema = z2.array(
|
|
2371
2393
|
z2.object({ old: z2.string().min(1), new: z2.string() })
|
|
@@ -2645,13 +2667,14 @@ server.registerTool("changes", {
|
|
|
2645
2667
|
});
|
|
2646
2668
|
server.registerTool("bulk_replace", {
|
|
2647
2669
|
title: "Bulk Replace",
|
|
2648
|
-
description: "Search-and-replace across multiple files. Finds files by glob, applies ordered text replacements
|
|
2670
|
+
description: "Search-and-replace across multiple files. Finds files by glob, applies ordered text replacements. Default format is compact (summary only); use format:'full' for capped diffs. Use dry_run:true to preview. For single-file rename, set glob to the filename.",
|
|
2649
2671
|
inputSchema: z2.object({
|
|
2650
2672
|
replacements: z2.union([z2.string(), replacementPairsSchema]).describe('JSON array of {old, new} pairs: [{"old":"foo","new":"bar"}]'),
|
|
2651
2673
|
glob: z2.string().optional().describe('File glob (default: "**/*.{md,mjs,json,yml,ts,js}")'),
|
|
2652
2674
|
path: z2.string().optional().describe("Root directory (default: cwd)"),
|
|
2653
2675
|
dry_run: flexBool().describe("Preview without writing (default: false)"),
|
|
2654
|
-
max_files: flexNum().describe("Max files to process (default: 100)")
|
|
2676
|
+
max_files: flexNum().describe("Max files to process (default: 100)"),
|
|
2677
|
+
format: z2.enum(["compact", "full"]).optional().describe('"compact" (default) = summary only, "full" = include capped diffs')
|
|
2655
2678
|
}),
|
|
2656
2679
|
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false }
|
|
2657
2680
|
}, async (rawParams) => {
|
|
@@ -2669,7 +2692,7 @@ server.registerTool("bulk_replace", {
|
|
|
2669
2692
|
params.path || process.cwd(),
|
|
2670
2693
|
params.glob || "**/*.{md,mjs,json,yml,ts,js}",
|
|
2671
2694
|
replacements,
|
|
2672
|
-
{ dryRun: params.dry_run || false, maxFiles: params.max_files || 100 }
|
|
2695
|
+
{ dryRun: params.dry_run || false, maxFiles: params.max_files || 100, format: params.format }
|
|
2673
2696
|
);
|
|
2674
2697
|
return { content: [{ type: "text", text: result }] };
|
|
2675
2698
|
} catch (e) {
|
package/output-style.md
CHANGED
|
@@ -12,15 +12,17 @@ keep-coding-instructions: true
|
|
|
12
12
|
|-----------|-----|-----|
|
|
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
|
-
| Write | `mcp__hex-line__write_file` |
|
|
15
|
+
| Write | `mcp__hex-line__write_file` | No prior Read needed |
|
|
16
16
|
| Grep | `mcp__hex-line__grep_search` | Hash-annotated matches |
|
|
17
17
|
| Edit (text rename) | `mcp__hex-line__bulk_replace` | Multi-file text rename/refactor |
|
|
18
|
+
| Bash `find`/`tree` | `mcp__hex-line__directory_tree` | Pattern search, gitignore-aware |
|
|
18
19
|
|
|
19
20
|
## Efficient File Reading
|
|
20
21
|
|
|
21
22
|
For UNFAMILIAR code files >100 lines, PREFER:
|
|
22
|
-
1. `outline` first (
|
|
23
|
+
1. `outline` first (code files only — not .md/.json/.yaml)
|
|
23
24
|
2. `read_file` with offset/limit for the specific section you need
|
|
25
|
+
3. Batch: `paths` array reads multiple files in one call
|
|
24
26
|
|
|
25
27
|
Avoid reading a large file in full — outline+targeted read saves 75% tokens.
|
|
26
28
|
|
|
@@ -33,7 +35,7 @@ Prefer:
|
|
|
33
35
|
1. collect all known hunks for one file
|
|
34
36
|
2. send one `edit_file` call with batched edits
|
|
35
37
|
3. carry `revision` from `read_file` into `base_revision` on follow-up edits
|
|
36
|
-
4.
|
|
38
|
+
4. edit types: `set_line` (1 line), `replace_lines` (range + checksum), `insert_after`, `replace_between` (large blocks)
|
|
37
39
|
5. use `verify` before rereading a file after staleness
|
|
38
40
|
|
|
39
41
|
Avoid:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@levnikolaevich/hex-line-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
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. 11 tools: read, edit, write, grep, outline, verify, directory_tree, file_info, setup_hooks, changes, bulk_replace.",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"_dep_notes": {
|
|
29
29
|
"web-tree-sitter": "Pinned ^0.25.0: v0.26 ABI incompatible with tree-sitter-wasms 0.1.x (built with tree-sitter-cli 0.20.8). Language.load() silently fails.",
|
|
30
30
|
"zod": "Pinned ^3.25.0: zod 4 breaks zod-to-json-schema (used by MCP SDK internally). Tool parameter descriptions not sent to clients. Revisit when MCP SDK switches to z.toJSONSchema().",
|
|
31
|
-
"better-sqlite3": "Optional. Used only by lib/graph-enrich.mjs for readonly access to hex-graph .codegraph/index.db. Graceful fallback if absent."
|
|
31
|
+
"better-sqlite3": "Optional. Used only by lib/graph-enrich.mjs for readonly access to hex-graph .hex-skills/codegraph/index.db. Graceful fallback if absent."
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
34
|
"@modelcontextprotocol/sdk": "^1.27.0",
|