Skip to content

Commit c3bc91e

Browse files
release: @levnikolaevich/hex-line-mcp v1.12.1, @levnikolaevich/hex-graph-mcp v0.9.0, @levnikolaevich/hex-ssh-mcp v1.4.1
hex-line: engine bump to Node >=20.19.0, magic number extraction hex-graph: graceful shutdown cleanup, GRAPH_DB_UNREADABLE error code with FIX_DB_ACCESS action, store/use-case rework, idle-close hex-ssh: engine bump to Node >=20.19.0 hex-common: engine bump, SEMVER_PART_COUNT constant Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 174192c commit c3bc91e

26 files changed

Lines changed: 1142 additions & 833 deletions

File tree

CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,20 @@
22

33
<!-- SCOPE: User-facing changes only. Max 5 bullets per entry. Focus: new capabilities, workflow changes, breaking changes. -->
44

5+
## 2026-04-05
6+
7+
- **hex-line v1.12.0** — conservative conflict recovery (retry_edit/retry_edits/retry_plan), hook policy extraction, canonical output contracts for verify/changes
8+
- **hex-graph v0.8.0** — output contract alignment (pruneEmpty, STATUS/ACTION constants, error mapping, DB busy handling), refactored platform and use-case API
9+
- **hex-ssh v1.4.0** — remotePlatform parameter (auto/posix/windows), requirePosixRemotePath validation, transfer improvements
10+
- **GitHub Actions Node.js 24** — upgraded checkout@v6, setup-node@v6, deploy-pages@v5
11+
- **MCP Registry fix** — shortened tool descriptions to ≤100 chars for hex-line and hex-graph
12+
13+
## 2026-04-01
14+
15+
- **hex-line v1.10.0** — tool descriptions refresh, graph-enrich refactor, hook safeExit, edit/read improvements
16+
- **hex-graph v0.6.0** — new flow.mjs dataflow module with find_dataflows source/sink API, store expansion
17+
- **CI fixes** — guards.mjs split path fix, hex-ssh sftp test isolation, hex-graph cross-platform workspace discovery, better-sqlite3 native binary rebuild on CI
18+
519
## 2026-03-31
620

721
- **Gemini auto-model selection** — removed hardcoded `-m gemini-3-flash-preview` from agent registry; Gemini CLI now auto-selects best available model. ln-011 post-install disables Conseca safety checker

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Claude Code Skills
22

3-
![Version](https://img.shields.io/badge/version-2026.03.27-blue)
3+
![Version](https://img.shields.io/badge/version-2026.04.05-blue)
44
![Skills](https://img.shields.io/badge/skills-129-green)
55
![License](https://img.shields.io/badge/license-MIT-green)
66
[![GitHub stars](https://img.shields.io/github/stars/levnikolaevich/claude-code-skills?style=social)](https://github.com/levnikolaevich/claude-code-skills)
@@ -116,7 +116,7 @@ Bundled MCP servers extend agent capabilities — hash-verified editing, code in
116116
Deterministic scope rule: `hex-line` and `hex-graph` keep `path` as the project anchor. In normal use the agent fills it automatically from the active file or project root, so users usually do not need to type it manually. `hex-ssh` runs on Windows/macOS/Linux hosts; remote shell tools stay POSIX-oriented, while SFTP transfers support platform-aware remote paths.
117117

118118
<!-- GENERATED:HEX_GRAPH_MCP_STATUS:START -->
119-
`hex-graph-mcp` quality snapshot: `83/83` tests passing, `1` curated corpus, `1` pinned external corpora, parser-first `green`.
119+
`hex-graph-mcp` quality snapshot: `84/84` tests passing, `1` curated corpus, `1` pinned external corpora, parser-first `green`.
120120
<!-- GENERATED:HEX_GRAPH_MCP_STATUS:END -->
121121

122122
### External servers

mcp/hex-common/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@
2020
"src/",
2121
"artifacts/"
2222
],
23+
"engines": {
24+
"node": ">=20.19.0"
25+
},
2326
"scripts": {
2427
"test": "node scripts/verify-tree-sitter-artifacts.mjs && node --test test/*.mjs"
2528
},

mcp/hex-common/src/runtime/update-check.mjs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { tmpdir } from "node:os";
55
const CACHE_FILE = join(tmpdir(), "hex-common-update.json");
66
const CHECK_INTERVAL = 24 * 60 * 60 * 1000;
77
const TIMEOUT = 3000;
8+
const SEMVER_PART_COUNT = 3;
89

910
async function readCache() {
1011
try {
@@ -35,7 +36,7 @@ async function fetchLatest(packageName) {
3536
function compareVersions(a, b) {
3637
const pa = a.split(".").map(Number);
3738
const pb = b.split(".").map(Number);
38-
for (let i = 0; i < 3; i++) {
39+
for (let i = 0; i < SEMVER_PART_COUNT; i++) {
3940
if ((pa[i] || 0) < (pb[i] || 0)) return -1;
4041
if ((pa[i] || 0) > (pb[i] || 0)) return 1;
4142
}

mcp/hex-graph-mcp/README.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,9 @@ hex-graph-mcp/
173173
### Storage
174174

175175
- **SQLite** via `better-sqlite3`
176+
- **Query lifecycle** uses readonly query stores that auto-close after a short idle window
177+
- **Write lifecycle** opens writable stores only for `index_project`, reindex, and SCIP import work, then checkpoints and closes them
178+
- **Lock behavior** on Windows is therefore normally caused by another live `hex-graph-mcp` / editor session for the same project, not by cross-project reuse
176179
- **Nodes** for symbols, module pseudo-nodes, and synthetic framework entrypoints
177180
- **Edges** as the semantic source of truth across syntax, symbol, module, type, flow, precise, and framework layers
178181
- **FTS5** for symbol discovery
@@ -229,7 +232,7 @@ Inline `quality` metadata is currently surfaced by:
229232
### Generated Snapshot
230233

231234
- MCP tools registered in server contract: `14`
232-
- Semantic suite: `83/83` passing
235+
- Semantic suite: `84/84` passing
233236
- Corpora: `1` curated, `1` pinned external
234237
- Lanes: parser-first `green`, precise overlay `provider_conditional`
235238

@@ -308,7 +311,7 @@ Atomic query comparisons and index-cost output still live under `benchmark/`, bu
308311
| `picomatch` | Scope/glob matching |
309312
| `zod` | Input schema validation |
310313

311-
Requires Node.js >= 20.0.0.
314+
Requires Node.js >= 20.19.0.
312315

313316
## FAQ
314317

mcp/hex-graph-mcp/evals/artifacts/quality-report.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"summary": {
44
"semantic_suite": {
55
"runner": "node --test test/*.mjs",
6-
"passed": 83,
6+
"passed": 84,
77
"failed": 0,
88
"status": "pass"
99
},

mcp/hex-graph-mcp/lib/indexer.mjs

Lines changed: 75 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
import { readFileSync, statSync, readdirSync, existsSync, mkdirSync } from "node:fs";
1414
import { resolve, extname, relative, join, basename } from "node:path";
1515
import { createHash } from "node:crypto";
16-
import { getStore, CODEGRAPH_DIR } from "./store.mjs";
16+
import { getStore, hasOpenStore, CODEGRAPH_DIR } from "./store.mjs";
1717
import { runFrameworkOverlay } from "./framework.mjs";
1818
import { parseFile, languageFor, supportedExtensions } from "./parser.mjs";
1919
import { discoverWorkspace, persistWorkspace } from "./workspace.mjs";
@@ -61,12 +61,15 @@ function referenceEdgeEvidence(ref) {
6161
export async function indexProject(projectPath, { languages } = {}) {
6262
const absPath = resolve(projectPath);
6363
const t0 = Date.now();
64+
let store;
65+
const shouldCloseStore = !hasOpenStore(absPath, { mode: "write" });
6466

6567
// Ensure .hex-skills/codegraph dir exists
6668
const dbDir = join(absPath, CODEGRAPH_DIR);
6769
if (!existsSync(dbDir)) mkdirSync(dbDir, { recursive: true });
6870

69-
const store = getStore(absPath);
71+
try {
72+
store = getStore(absPath);
7073

7174
// Filter extensions by language if specified
7275
const allowedExts = languages
@@ -170,18 +173,24 @@ export async function indexProject(projectPath, { languages } = {}) {
170173
const elapsed = Date.now() - t0;
171174
const stats = store.stats();
172175

173-
return [
174-
`Indexed ${stats.files} files, ${stats.nodes} symbols, ${stats.edges} edges in ${elapsed}ms`,
175-
purged > 0 ? `Purged ${purged} deleted files` : null,
176-
`Parsed ${parsed} files (${filesToIndex.length - parsed} skipped, unchanged)`,
177-
`Built ${edgeCount} new call edges`,
178-
precise.precise_edges > 0 ? `Added ${precise.precise_edges} precise overlay edges` : null,
179-
framework.edge_count > 0 ? `Added ${framework.edge_count} framework overlay edges` : null,
180-
...(precise.providers || [])
181-
.filter(provider => provider.status && provider.status !== "available")
182-
.map(provider => provider.message)
183-
.filter(Boolean),
184-
].filter(Boolean).join("\n");
176+
return [
177+
`Indexed ${stats.files} files, ${stats.nodes} symbols, ${stats.edges} edges in ${elapsed}ms`,
178+
purged > 0 ? `Purged ${purged} deleted files` : null,
179+
`Parsed ${parsed} files (${filesToIndex.length - parsed} skipped, unchanged)`,
180+
`Built ${edgeCount} new call edges`,
181+
precise.precise_edges > 0 ? `Added ${precise.precise_edges} precise overlay edges` : null,
182+
framework.edge_count > 0 ? `Added ${framework.edge_count} framework overlay edges` : null,
183+
...(precise.providers || [])
184+
.filter(provider => provider.status && provider.status !== "available")
185+
.map(provider => provider.message)
186+
.filter(Boolean),
187+
].filter(Boolean).join("\n");
188+
} finally {
189+
if (store && shouldCloseStore) {
190+
try { store.checkpoint(); } catch { /* best-effort WAL flush */ }
191+
store.close();
192+
}
193+
}
185194
}
186195

187196
/**
@@ -192,52 +201,61 @@ export async function indexProject(projectPath, { languages } = {}) {
192201
export async function reindexFile(projectPath, filePath) {
193202
const absPath = resolve(projectPath);
194203
const fullPath = resolve(absPath, filePath);
204+
let store;
205+
const shouldCloseStore = !hasOpenStore(absPath, { mode: "write" });
195206

196-
if (!existsSync(fullPath)) {
197-
const store = getStore(absPath);
198-
store.deleteFile(filePath);
199-
return;
200-
}
207+
try {
208+
if (!existsSync(fullPath)) {
209+
store = getStore(absPath);
210+
store.deleteFile(filePath);
211+
return;
212+
}
201213

202-
const source = readFileSync(fullPath, "utf-8").replace(/\r\n/g, "\n");
203-
const hash = createHash("md5").update(source).digest("hex").slice(0, 12);
204-
const stat = statSync(fullPath);
205-
const language = languageFor(extname(filePath).toLowerCase());
206-
if (!language) return;
214+
const source = readFileSync(fullPath, "utf-8").replace(/\r\n/g, "\n");
215+
const hash = createHash("md5").update(source).digest("hex").slice(0, 12);
216+
const stat = statSync(fullPath);
217+
const language = languageFor(extname(filePath).toLowerCase());
218+
if (!language) return;
219+
220+
store = getStore(absPath);
221+
const allSourceFiles = [];
222+
walkDir(absPath, absPath, new Set(supportedExtensions()), store, [], allSourceFiles);
223+
const workspace = persistWorkspace(store, discoverWorkspace(absPath, allSourceFiles));
224+
const projectLanguages = [...new Set(allSourceFiles.map(file => file.language).filter(Boolean))];
225+
const { definitions, imports, calls, references, flow_ir, exports: fileExports, defaultExport, reexports } = await parseFile(fullPath, source, { cloneDetection: true });
226+
const nodeIds = store.bulkInsert(
227+
filePath,
228+
stat.mtimeMs,
229+
hash,
230+
language,
231+
definitions,
232+
imports,
233+
workspace.ownershipIds.get(filePath) || null,
234+
);
207235

208-
const store = getStore(absPath);
209-
const allSourceFiles = [];
210-
walkDir(absPath, absPath, new Set(supportedExtensions()), store, [], allSourceFiles);
211-
const workspace = persistWorkspace(store, discoverWorkspace(absPath, allSourceFiles));
212-
const projectLanguages = [...new Set(allSourceFiles.map(file => file.language).filter(Boolean))];
213-
const { definitions, imports, calls, references, flow_ir, exports: fileExports, defaultExport, reexports } = await parseFile(fullPath, source, { cloneDetection: true });
214-
const nodeIds = store.bulkInsert(
215-
filePath,
216-
stat.mtimeMs,
217-
hash,
218-
language,
219-
definitions,
220-
imports,
221-
workspace.ownershipIds.get(filePath) || null,
222-
);
223-
224-
persistCloneData(store, definitions, nodeIds);
225-
resolveFileEdges(store, workspace, filePath, {
226-
source, definitions, imports, calls, references, flow_ir,
227-
exports: fileExports, defaultExport, reexports, nodeIds, language,
228-
});
229-
store.rebuildAllModuleLayerEdges();
230-
await runPreciseOverlay({
231-
projectPath: absPath,
232-
store,
233-
languages: projectLanguages,
234-
sourceFiles: allSourceFiles.map(file => file.relPath),
235-
});
236-
runFrameworkOverlay({
237-
projectPath: absPath,
238-
store,
239-
sourceFiles: allSourceFiles.map(file => file.relPath),
240-
});
236+
persistCloneData(store, definitions, nodeIds);
237+
resolveFileEdges(store, workspace, filePath, {
238+
source, definitions, imports, calls, references, flow_ir,
239+
exports: fileExports, defaultExport, reexports, nodeIds, language,
240+
});
241+
store.rebuildAllModuleLayerEdges();
242+
await runPreciseOverlay({
243+
projectPath: absPath,
244+
store,
245+
languages: projectLanguages,
246+
sourceFiles: allSourceFiles.map(file => file.relPath),
247+
});
248+
runFrameworkOverlay({
249+
projectPath: absPath,
250+
store,
251+
sourceFiles: allSourceFiles.map(file => file.relPath),
252+
});
253+
} finally {
254+
if (store && shouldCloseStore) {
255+
try { store.checkpoint(); } catch { /* best-effort WAL flush */ }
256+
store.close();
257+
}
258+
}
241259
}
242260

243261
// --- Helpers ---

mcp/hex-graph-mcp/lib/output-contract.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export const ACTION = {
1818
REVIEW_DELETED_API: "review_deleted_api",
1919
REVIEW_DUPLICATES: "review_duplicates",
2020
FIX_DB_LOCK: "fix_db_lock",
21+
FIX_DB_ACCESS: "fix_db_access",
2122
FIX_PATH: "fix_path",
2223
CHECK_PROVIDER_SETUP: "check_provider_setup",
2324
CHECK_SCIP_INPUTS: "check_scip_inputs",

mcp/hex-graph-mcp/lib/pr-impact.mjs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import { semanticGitDiff } from "@levnikolaevich/hex-common/git/semantic-diff";
22
import { resolveStore, tracePaths } from "./store.mjs";
33

4+
export const DEFAULT_PR_IMPACT_MAX_SYMBOLS = 25;
5+
export const DEFAULT_PR_IMPACT_MAX_PATHS = 10;
6+
47
const IMPACT_EDGE_KINDS = new Set([
58
"calls",
69
"ref_read",
@@ -160,8 +163,8 @@ export async function getPrImpact({
160163
baseRef,
161164
headRef = null,
162165
includePaths = false,
163-
maxSymbols = 25,
164-
maxPaths = 10,
166+
maxSymbols = DEFAULT_PR_IMPACT_MAX_SYMBOLS,
167+
maxPaths = DEFAULT_PR_IMPACT_MAX_PATHS,
165168
}) {
166169
const store = resolveStore(path);
167170
if (!store) {

0 commit comments

Comments
 (0)