Skip to content

Commit c534e63

Browse files
release: @levnikolaevich/hex-line-mcp v1.27.1
- lib/search.mjs: handle CRLF ripgrep output on Windows (split on /\r?\n/) so summary mode counts files correctly - scenarios: generic-external-repo fallback workflow suite (W1 outline-read, W2 summary-to-edit-ready, W3 verified dry-run edit) - README + output-style.md: audit_workspace bounded-output guidance - test/smoke.mjs: CRLF summary-mode regression guard Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 7bdae6b commit c534e63

8 files changed

Lines changed: 137 additions & 6 deletions

File tree

mcp/hex-line-mcp/README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,8 @@ The diagnostics run reports graph payload overhead and auto-refresh telemetry. R
106106

107107
Comparative built-in vs hex-line benchmarks are maintained outside this package. External-baseline comparisons must reuse the same scenario suite and correctness contract before making broader claims.
108108

109+
When `--repo` points at a repository outside `hex-line-mcp`, the scenario runner uses a generic external-repo suite instead of package-specific fixtures. The report labels the mode explicitly.
110+
109111
### Optional Graph Enrichment
110112

111113
If a project already has `.hex-skills/codegraph/index.db`, `hex-line` automatically adds lightweight graph hints to `read_file`, `outline`, `grep_search`, `edit_file`, and `changes`.
@@ -289,7 +291,7 @@ Search file contents using ripgrep. Default mode is `summary` for discovery. Use
289291
| `edit_ready` | boolean | no | Preserve hash/checksum search hunks in `content` mode |
290292
| `allow_large_output` | boolean | no | Bypass the default `content`-mode block/char caps when you intentionally need a larger payload |
291293

292-
`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`.
294+
`summary` mode returns counts and top files without canonical hunks. `content` mode returns canonical `search_hunk` blocks with per-hunk checksums enabling direct `replace_lines` from grep results without intermediate `read_file`.
293295

294296
- 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`.
295297
- Treat `allow_large_output: true` as an explicit override for review/debug workflows, not as the normal discovery path.

mcp/hex-line-mcp/lib/search.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ async function summaryMode(pattern, target, opts, totalLimit) {
183183
if (code === 1) return "No matches found.";
184184
if (code !== 0 && code !== null) throw new Error(`GREP_ERROR: rg exit ${code}${stderr.trim() || "unknown error"}`);
185185

186-
const rawLines = stdout.trimEnd().split("\n").filter(Boolean);
186+
const rawLines = stdout.trimEnd().split(/\r?\n/).filter(Boolean);
187187
const visible = totalLimit > 0 ? rawLines.slice(0, totalLimit) : rawLines;
188188
const fileHits = new Map();
189189
const snippets = [];

mcp/hex-line-mcp/output-style.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ Use `hex-graph` only for semantic code questions:
7272
- `find_references` and `trace_paths` for usage and blast radius
7373
- `analyze_changes`, `audit_workspace`, and `analyze_architecture` for review and audit work
7474
- Always include `path` for `hex-graph` queries, using the active project root by default.
75+
- For `audit_workspace`, start bounded: `verbosity="minimal"`, add `scope` when known, and increase `limit` or `clone_member_limit` only for intentional deeper review.
7576

7677
# Response Style
7778

mcp/hex-line-mcp/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@levnikolaevich/hex-line-mcp",
3-
"version": "1.27.0",
3+
"version": "1.27.1",
44
"mcpName": "io.github.levnikolaevich/hex-line-mcp",
55
"type": "module",
66
"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.",

mcp/hex-line-mcp/scenarios/index.mjs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,9 @@ async function main() {
6868

6969
// Build config shared across all benchmark modules
7070
const config = { allFiles, cats, largeFiles, tmpPath, tmpContent, tmpLines, repoRoot, ts };
71+
const workflowMode = getFileLines(resolve(repoRoot, "hook.mjs")) && getFileLines(resolve(repoRoot, "lib", "setup.mjs"))
72+
? "self-fixture"
73+
: "generic external repo";
7174

7275
// Run benchmark suites
7376
const workflowResults = await runWorkflows(config);
@@ -88,7 +91,7 @@ async function main() {
8891
out.push(`Date: ${new Date().toISOString().slice(0, 10)} `);
8992
out.push(`Runs per scenario: ${RUNS} (median) `);
9093
out.push("");
91-
out.push("Mode: hex-line workflow benchmark");
94+
out.push(`Mode: hex-line workflow benchmark (${workflowMode})`);
9295
out.push("");
9396
out.push("## Workflow Scenarios");
9497
out.push("");

mcp/hex-line-mcp/scenarios/workflows.mjs

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { verifyChecksums } from "../lib/verify.mjs";
2121
import { editFile } from "../lib/edit.mjs";
2222
import { bulkReplace } from "../lib/bulk-replace.mjs";
2323
import { fileOutline } from "../lib/outline.mjs";
24+
import { grepSearch } from "../lib/search.mjs";
2425
import { getFileLines, runN } from "../lib/scenario-helpers.mjs";
2526

2627
function ensureLine(lines, matcher, label) {
@@ -37,9 +38,113 @@ function copyIntoTemp(tempRoot, sourceRoot, relPath) {
3738
return dst;
3839
}
3940

41+
function selectGenericFile(allFiles, largeFiles) {
42+
return [...largeFiles, ...allFiles].find((file) => {
43+
const lines = getFileLines(file);
44+
return lines && lines.length >= 10;
45+
});
46+
}
47+
48+
function selectGenericPattern(lines) {
49+
const joined = lines.join("\n");
50+
for (const pattern of ["function", "class", "async", "import", "def", "export", "const"]) {
51+
if (joined.includes(pattern)) return pattern;
52+
}
53+
const line = lines.find((entry) => /\w{4,}/.test(entry)) || "";
54+
return line.match(/\w{4,}/)?.[0] || "TODO";
55+
}
56+
57+
async function runGenericExternalWorkflows(config) {
58+
const { repoRoot, allFiles, largeFiles, ts } = config;
59+
const workflowResults = [];
60+
const sourcePath = selectGenericFile(allFiles, largeFiles);
61+
const sourceLines = sourcePath ? getFileLines(sourcePath) : null;
62+
if (!sourcePath || !sourceLines) return workflowResults;
63+
64+
// W1: outline + targeted read on a real large file.
65+
{
66+
let chars = 0;
67+
chars += (await fileOutline(sourcePath)).length;
68+
chars += readFile(sourcePath, { offset: 1, limit: Math.min(80, sourceLines.length) }).length;
69+
workflowResults.push({
70+
id: "W1",
71+
scenario: "External repo structure-first large-file read",
72+
chars,
73+
ops: 2,
74+
});
75+
}
76+
77+
// W2: summary-first search, then edit-ready hunks only for a narrowed file.
78+
{
79+
const pattern = selectGenericPattern(sourceLines);
80+
let chars = 0;
81+
chars += (await grepSearch(pattern, {
82+
path: repoRoot,
83+
output: "summary",
84+
totalLimit: 50,
85+
limit: 10,
86+
})).length;
87+
chars += (await grepSearch(pattern, {
88+
path: sourcePath,
89+
output: "content",
90+
editReady: true,
91+
totalLimit: 20,
92+
limit: 5,
93+
})).length;
94+
workflowResults.push({
95+
id: "W2",
96+
scenario: "External repo search summary to edit-ready hunks",
97+
chars,
98+
ops: 2,
99+
});
100+
}
101+
102+
// W3: verified dry-run edit on a temp copy so external repos stay untouched.
103+
{
104+
const targetIdx = sourceLines.findIndex((line) => line.trim() && !line.trim().startsWith("//"));
105+
if (targetIdx !== -1) {
106+
const tempPath = resolve(tmpdir(), `hex-line-external-wf3-${ts}.tmp`);
107+
copyFileSync(sourcePath, tempPath);
108+
const start = Math.max(1, targetIdx + 1 - 2);
109+
const end = Math.min(sourceLines.length, targetIdx + 1 + 2);
110+
const hashes = sourceLines.slice(start - 1, end).map((line) => fnv1a(line));
111+
const checksum = rangeChecksum(hashes, start, end);
112+
const tag = lineTag(fnv1a(sourceLines[targetIdx]));
113+
const updatedLine = `${sourceLines[targetIdx]} `;
114+
const { value: chars } = runN(() => {
115+
let total = 0;
116+
total += readFile(tempPath, { offset: start, limit: end - start + 1, editReady: true, verbosity: "full" }).length;
117+
try {
118+
total += editFile(tempPath, [{ set_line: { anchor: `${tag}.${targetIdx + 1}`, new_text: updatedLine } }], { dryRun: true }).length;
119+
} catch (e) {
120+
total += e.message.length;
121+
}
122+
try {
123+
total += verifyChecksums(tempPath, [checksum]).length;
124+
} catch (e) {
125+
total += e.message.length;
126+
}
127+
return total;
128+
});
129+
workflowResults.push({
130+
id: "W3",
131+
scenario: "External repo verified dry-run edit on temp copy",
132+
chars,
133+
ops: 3,
134+
});
135+
try { unlinkSync(tempPath); } catch {}
136+
}
137+
}
138+
139+
return workflowResults;
140+
}
141+
40142
export async function runWorkflows(config) {
41143
const { repoRoot, allFiles, largeFiles } = config;
42144
const workflowResults = [];
145+
if (!getFileLines(resolve(repoRoot, "hook.mjs")) || !getFileLines(resolve(repoRoot, "lib", "setup.mjs"))) {
146+
return runGenericExternalWorkflows(config);
147+
}
43148

44149
// W1: grep tag + editFile (debug hex-line hook formatting)
45150
{

mcp/hex-line-mcp/server.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,12 @@
77
"url": "https://github.com/levnikolaevich/claude-code-skills",
88
"source": "github"
99
},
10-
"version": "1.27.0",
10+
"version": "1.27.1",
1111
"packages": [
1212
{
1313
"registryType": "npm",
1414
"identifier": "@levnikolaevich/hex-line-mcp",
15-
"version": "1.27.0",
15+
"version": "1.27.1",
1616
"transport": {
1717
"type": "stdio"
1818
}

mcp/hex-line-mcp/test/smoke.mjs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1335,6 +1335,26 @@ describe("grep_search output modes", () => {
13351335
assert.ok(!result.includes("block: search_hunk"), "summary mode omits canonical hunks");
13361336
});
13371337

1338+
it("summary mode counts files for Windows ripgrep CRLF output", async () => {
1339+
const { grepSearch } = await import("../lib/search.mjs");
1340+
const dir = makeTempRepo("hex-test-grep-summary-crlf-", {
1341+
"src/one.ts": "export function alpha() { return 1; }\n",
1342+
"src/two.ts": "export function beta() { return 2; }\n",
1343+
});
1344+
try {
1345+
const result = await grepSearch("export function", {
1346+
path: dir,
1347+
output: "summary",
1348+
limit: 20,
1349+
totalLimit: 20,
1350+
});
1351+
assert.match(result, /summary: 2 match event\(s\) across 2 file\(s\)/);
1352+
assert.ok(result.includes("top_files:"), `summary should include top files: ${result}`);
1353+
} finally {
1354+
fs.rmSync(dir, { recursive: true, force: true });
1355+
}
1356+
});
1357+
13381358
it("files mode returns only paths, count mode returns counts", async () => {
13391359
const { grepSearch } = await import("../lib/search.mjs");
13401360
const files = await grepSearch("export", { path: CWD + "/lib", output: "files" });

0 commit comments

Comments
 (0)