@levnikolaevich/hex-line-mcp 1.23.0 → 1.24.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/dist/server.mjs +101 -31
- package/package.json +1 -1
package/dist/server.mjs
CHANGED
|
@@ -400,6 +400,20 @@ function getGraphDB(filePath, { allowStale = false } = {}) {
|
|
|
400
400
|
return null;
|
|
401
401
|
}
|
|
402
402
|
}
|
|
403
|
+
function diagnoseFreshness(db, projectRoot, filePath) {
|
|
404
|
+
try {
|
|
405
|
+
const stat = statSync3(filePath);
|
|
406
|
+
if (!stat.isFile()) return "ok";
|
|
407
|
+
const relativeFile = normalizeRelativeFile(projectRoot, filePath);
|
|
408
|
+
if (!relativeFile) return "ok";
|
|
409
|
+
const indexedMtime = lookupIndexedMtime(db, relativeFile);
|
|
410
|
+
if (indexedMtime == null) return "file_not_indexed";
|
|
411
|
+
if (Math.abs(indexedMtime - stat.mtimeMs) < FRESHNESS_TOLERANCE_MS) return "ok";
|
|
412
|
+
return "stale";
|
|
413
|
+
} catch {
|
|
414
|
+
return "ok";
|
|
415
|
+
}
|
|
416
|
+
}
|
|
403
417
|
function diagnoseGraph(filePath) {
|
|
404
418
|
if (_driverUnavailable) return { reason: "driver_missing" };
|
|
405
419
|
try {
|
|
@@ -409,10 +423,7 @@ function diagnoseGraph(filePath) {
|
|
|
409
423
|
if (!existsSync2(dbPath)) return { reason: "index_missing", projectRoot };
|
|
410
424
|
if (_dbs.has(dbPath)) {
|
|
411
425
|
const cached = _dbs.get(dbPath);
|
|
412
|
-
|
|
413
|
-
return { reason: "stale", projectRoot };
|
|
414
|
-
}
|
|
415
|
-
return { reason: "ok", projectRoot };
|
|
426
|
+
return { reason: diagnoseFreshness(cached, projectRoot, filePath), projectRoot };
|
|
416
427
|
}
|
|
417
428
|
const require2 = createRequire(import.meta.url);
|
|
418
429
|
const Database = require2("better-sqlite3");
|
|
@@ -421,9 +432,28 @@ function diagnoseGraph(filePath) {
|
|
|
421
432
|
db.close();
|
|
422
433
|
return { reason: "contract_mismatch", projectRoot };
|
|
423
434
|
}
|
|
424
|
-
|
|
435
|
+
_dbs.set(dbPath, db);
|
|
436
|
+
return { reason: diagnoseFreshness(db, projectRoot, filePath), projectRoot };
|
|
437
|
+
} catch {
|
|
438
|
+
_driverUnavailable = true;
|
|
439
|
+
return { reason: "driver_missing" };
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
function diagnoseGraphForProject(projectRoot) {
|
|
443
|
+
if (_driverUnavailable) return { reason: "driver_missing" };
|
|
444
|
+
try {
|
|
445
|
+
if (!projectRoot) return { reason: "no_project_root" };
|
|
446
|
+
const dbPath = join3(projectRoot, ".hex-skills/codegraph", "index.db");
|
|
447
|
+
if (!existsSync2(dbPath)) return { reason: "index_missing", projectRoot };
|
|
448
|
+
if (_dbs.has(dbPath)) {
|
|
449
|
+
return { reason: "ok", projectRoot };
|
|
450
|
+
}
|
|
451
|
+
const require2 = createRequire(import.meta.url);
|
|
452
|
+
const Database = require2("better-sqlite3");
|
|
453
|
+
const db = new Database(dbPath, { readonly: true });
|
|
454
|
+
if (!validateContract(db)) {
|
|
425
455
|
db.close();
|
|
426
|
-
return { reason: "
|
|
456
|
+
return { reason: "contract_mismatch", projectRoot };
|
|
427
457
|
}
|
|
428
458
|
_dbs.set(dbPath, db);
|
|
429
459
|
return { reason: "ok", projectRoot };
|
|
@@ -432,9 +462,15 @@ function diagnoseGraph(filePath) {
|
|
|
432
462
|
return { reason: "driver_missing" };
|
|
433
463
|
}
|
|
434
464
|
}
|
|
465
|
+
function getGraphDBForProject(projectRoot) {
|
|
466
|
+
const { reason } = diagnoseGraphForProject(projectRoot);
|
|
467
|
+
if (reason !== "ok") return null;
|
|
468
|
+
const dbPath = join3(projectRoot, ".hex-skills/codegraph", "index.db");
|
|
469
|
+
return _dbs.get(dbPath) || null;
|
|
470
|
+
}
|
|
435
471
|
function graphUnavailableHint(filePath) {
|
|
436
472
|
const { reason, projectRoot } = diagnoseGraph(filePath);
|
|
437
|
-
if (reason === "ok") return [];
|
|
473
|
+
if (reason === "ok" || reason === "file_not_indexed") return [];
|
|
438
474
|
const at = projectRoot ? ` at ${projectRoot.replace(/\\/g, "/")}` : "";
|
|
439
475
|
switch (reason) {
|
|
440
476
|
case "driver_missing":
|
|
@@ -451,6 +487,23 @@ function graphUnavailableHint(filePath) {
|
|
|
451
487
|
return ["graph_enrichment: unavailable"];
|
|
452
488
|
}
|
|
453
489
|
}
|
|
490
|
+
function graphUnavailableHintForProject(projectRoot) {
|
|
491
|
+
const { reason } = diagnoseGraphForProject(projectRoot);
|
|
492
|
+
if (reason === "ok") return [];
|
|
493
|
+
const at = projectRoot ? ` at ${projectRoot.replace(/\\/g, "/")}` : "";
|
|
494
|
+
switch (reason) {
|
|
495
|
+
case "driver_missing":
|
|
496
|
+
return ["graph_enrichment: unavailable", "graph_fix: install better-sqlite3 in hex-line-mcp package"];
|
|
497
|
+
case "no_project_root":
|
|
498
|
+
return ["graph_enrichment: unavailable", "graph_fix: directory is outside any project root"];
|
|
499
|
+
case "index_missing":
|
|
500
|
+
return ["graph_enrichment: unavailable", `graph_fix: run mcp__hex-graph__index_project${at}`];
|
|
501
|
+
case "contract_mismatch":
|
|
502
|
+
return ["graph_enrichment: unavailable", `graph_fix: index built by incompatible hex-graph version; re-run mcp__hex-graph__index_project${at}`];
|
|
503
|
+
default:
|
|
504
|
+
return ["graph_enrichment: unavailable"];
|
|
505
|
+
}
|
|
506
|
+
}
|
|
454
507
|
function validateContract(db) {
|
|
455
508
|
try {
|
|
456
509
|
for (const viewName of REQUIRED_VIEWS) {
|
|
@@ -548,6 +601,20 @@ function ensureGraphFreshForFile(db, absoluteFilePath) {
|
|
|
548
601
|
return true;
|
|
549
602
|
}
|
|
550
603
|
}
|
|
604
|
+
function isGraphFreshAtMtime(db, absoluteFilePath, mtimeMs) {
|
|
605
|
+
if (!db) return false;
|
|
606
|
+
try {
|
|
607
|
+
const projectRoot = findProjectRoot(absoluteFilePath);
|
|
608
|
+
if (!projectRoot) return true;
|
|
609
|
+
const relativeFile = normalizeRelativeFile(projectRoot, absoluteFilePath);
|
|
610
|
+
if (!relativeFile) return true;
|
|
611
|
+
const indexedMtime = lookupIndexedMtime(db, relativeFile);
|
|
612
|
+
if (indexedMtime == null) return false;
|
|
613
|
+
return mtimeMs <= indexedMtime + FRESHNESS_TOLERANCE_MS;
|
|
614
|
+
} catch {
|
|
615
|
+
return true;
|
|
616
|
+
}
|
|
617
|
+
}
|
|
551
618
|
function fileAnnotations(db, file, { startLine = null, endLine = null, limit = 8 } = {}) {
|
|
552
619
|
try {
|
|
553
620
|
const hasRange = Number.isInteger(startLine) && Number.isInteger(endLine);
|
|
@@ -2914,7 +2981,7 @@ ${serializeReadBlock(block)}`;
|
|
|
2914
2981
|
const relFile = db ? getRelativePath(real) : null;
|
|
2915
2982
|
if (db && relFile && fullDiff && minLine <= maxLine) {
|
|
2916
2983
|
graphDbAvailable = true;
|
|
2917
|
-
graphFresh =
|
|
2984
|
+
graphFresh = isGraphFreshAtMtime(db, real, currentSnapshot.mtimeMs);
|
|
2918
2985
|
semanticImpacts = semanticImpact(db, relFile, minLine, maxLine);
|
|
2919
2986
|
if (semanticImpacts.length > 0) {
|
|
2920
2987
|
const sections = semanticImpacts.map((impact) => {
|
|
@@ -3693,16 +3760,17 @@ function entrySummary(entry) {
|
|
|
3693
3760
|
if (entry.status === "STALE") return "content changed since checksum capture";
|
|
3694
3761
|
return entry.reason;
|
|
3695
3762
|
}
|
|
3696
|
-
function renderEntry(entry, index, total) {
|
|
3763
|
+
function renderEntry(entry, index, total, topLevelNextAction) {
|
|
3697
3764
|
const parts = [
|
|
3698
3765
|
`entry: ${index}/${total}`,
|
|
3699
3766
|
`status: ${entry.status}`,
|
|
3700
3767
|
entry.span ? `span: ${entry.span}` : null,
|
|
3701
3768
|
`checksum: ${entry.checksum}`,
|
|
3702
|
-
entry.currentChecksum && entry.currentChecksum !== entry.checksum ? `current_checksum: ${entry.currentChecksum}` : null
|
|
3703
|
-
`next_action: ${entryNextAction(entry)}`,
|
|
3704
|
-
`summary: ${entrySummary(entry)}`
|
|
3769
|
+
entry.currentChecksum && entry.currentChecksum !== entry.checksum ? `current_checksum: ${entry.currentChecksum}` : null
|
|
3705
3770
|
].filter(Boolean);
|
|
3771
|
+
const action = entryNextAction(entry);
|
|
3772
|
+
if (action !== topLevelNextAction) parts.push(`next_action: ${action}`);
|
|
3773
|
+
if (entry.status !== "VALID") parts.push(`summary: ${entrySummary(entry)}`);
|
|
3706
3774
|
return parts.join(" | ");
|
|
3707
3775
|
}
|
|
3708
3776
|
function verifyChecksums(filePath, checksums, opts = {}) {
|
|
@@ -3716,18 +3784,20 @@ function verifyChecksums(filePath, checksums, opts = {}) {
|
|
|
3716
3784
|
const summary = summarizeStatuses(results);
|
|
3717
3785
|
const status = summary.invalid > 0 ? STATUS.INVALID : summary.stale > 0 ? STATUS.STALE : STATUS.OK;
|
|
3718
3786
|
const staleRanges = results.filter((entry) => entry.status === "STALE" && entry.span).map((entry) => entry.span);
|
|
3787
|
+
const topLevelNextAction = overallNextAction(summary);
|
|
3788
|
+
const verboseSummary = results.length > 1 || summary.stale > 0 || summary.invalid > 0;
|
|
3719
3789
|
const lines = [
|
|
3720
3790
|
`status: ${status}`,
|
|
3721
3791
|
`reason: ${overallReason(status)}`,
|
|
3722
|
-
`revision: ${currentSnapshot.revision}
|
|
3723
|
-
`file: ${currentSnapshot.fileChecksum}`,
|
|
3724
|
-
`summary: valid=${summary.valid} stale=${summary.stale} invalid=${summary.invalid}`,
|
|
3725
|
-
`next_action: ${overallNextAction(summary)}`
|
|
3792
|
+
`revision: ${currentSnapshot.revision}`
|
|
3726
3793
|
];
|
|
3727
|
-
if (
|
|
3794
|
+
if (verboseSummary) lines.push(`summary: valid=${summary.valid} stale=${summary.stale} invalid=${summary.invalid}`);
|
|
3795
|
+
lines.push(`next_action: ${topLevelNextAction}`);
|
|
3796
|
+
if (opts.baseRevision && opts.baseRevision !== currentSnapshot.revision) {
|
|
3728
3797
|
lines.push(`base_revision: ${opts.baseRevision}`);
|
|
3729
3798
|
if (hasBaseSnapshot) {
|
|
3730
|
-
|
|
3799
|
+
const changed = describeChangedRanges(computeChangedRanges(baseSnapshot.lines, currentSnapshot.lines));
|
|
3800
|
+
if (changed !== "none") lines.push(`changed_ranges: ${changed}`);
|
|
3731
3801
|
} else {
|
|
3732
3802
|
lines.push("base_revision_status: evicted");
|
|
3733
3803
|
}
|
|
@@ -3735,7 +3805,7 @@ function verifyChecksums(filePath, checksums, opts = {}) {
|
|
|
3735
3805
|
const suggestedReadCall = buildSuggestedReadCall2(filePath, staleRanges);
|
|
3736
3806
|
if (suggestedReadCall) lines.push(`suggested_read_call: ${suggestedReadCall}`);
|
|
3737
3807
|
if (results.length > 0) {
|
|
3738
|
-
lines.push("", ...results.map((entry, index) => renderEntry(entry, index + 1, results.length)));
|
|
3808
|
+
lines.push("", ...results.map((entry, index) => renderEntry(entry, index + 1, results.length, topLevelNextAction)));
|
|
3739
3809
|
}
|
|
3740
3810
|
return lines.join("\n");
|
|
3741
3811
|
}
|
|
@@ -3837,12 +3907,13 @@ function findByPattern(dirPath, opts) {
|
|
|
3837
3907
|
const truncated = shown.length < matches.length;
|
|
3838
3908
|
const groups = topPatternGroups(matches);
|
|
3839
3909
|
const lines = [
|
|
3840
|
-
`Found ${matches.length} match${matches.length === 1 ? "" : "es"} for "${opts.pattern}" in ${rootName}
|
|
3841
|
-
`match_count: ${matches.length}`,
|
|
3842
|
-
`shown_count: ${shown.length}`,
|
|
3843
|
-
`truncated: ${truncated}`
|
|
3910
|
+
`Found ${matches.length} match${matches.length === 1 ? "" : "es"} for "${opts.pattern}" in ${rootName}/`
|
|
3844
3911
|
];
|
|
3845
|
-
if (
|
|
3912
|
+
if (truncated) {
|
|
3913
|
+
lines.push(`shown_count: ${shown.length}`);
|
|
3914
|
+
lines.push(`truncated: true`);
|
|
3915
|
+
}
|
|
3916
|
+
if (groups.length > 1) {
|
|
3846
3917
|
lines.push(`top_groups: ${groups.map(([group, count]) => `${group} (${count})`).join(", ")}`);
|
|
3847
3918
|
}
|
|
3848
3919
|
if (truncated) {
|
|
@@ -4211,7 +4282,6 @@ function autoSync() {
|
|
|
4211
4282
|
|
|
4212
4283
|
// lib/changes.mjs
|
|
4213
4284
|
import { statSync as statSync11 } from "node:fs";
|
|
4214
|
-
import { join as join6 } from "node:path";
|
|
4215
4285
|
|
|
4216
4286
|
// ../hex-common/src/git/semantic-diff.mjs
|
|
4217
4287
|
import { execFileSync } from "node:child_process";
|
|
@@ -4514,9 +4584,9 @@ async function fileChanges(filePath, compareAgainst = "HEAD") {
|
|
|
4514
4584
|
filePath = normalizePath(filePath);
|
|
4515
4585
|
const real = validatePath(filePath);
|
|
4516
4586
|
if (statSync11(real).isDirectory()) {
|
|
4517
|
-
const db2 =
|
|
4587
|
+
const db2 = getGraphDBForProject(real);
|
|
4518
4588
|
const diff2 = await semanticGitDiff(real, { baseRef: compareAgainst });
|
|
4519
|
-
const graphHint2 =
|
|
4589
|
+
const graphHint2 = graphUnavailableHintForProject(real);
|
|
4520
4590
|
if (diff2.summary.changed_file_count === 0) {
|
|
4521
4591
|
return [
|
|
4522
4592
|
"status: NO_CHANGES",
|
|
@@ -4653,7 +4723,7 @@ async function fileChanges(filePath, compareAgainst = "HEAD") {
|
|
|
4653
4723
|
|
|
4654
4724
|
// lib/bulk-replace.mjs
|
|
4655
4725
|
import { writeFileSync as writeFileSync3, readdirSync as readdirSync3, renameSync as renameSync2, unlinkSync as unlinkSync2 } from "node:fs";
|
|
4656
|
-
import { resolve as resolve9, relative as relative4, join as
|
|
4726
|
+
import { resolve as resolve9, relative as relative4, join as join6 } from "node:path";
|
|
4657
4727
|
var ignoreMod;
|
|
4658
4728
|
try {
|
|
4659
4729
|
ignoreMod = await import("ignore");
|
|
@@ -4669,7 +4739,7 @@ function walkFiles(dir, rootDir, ig) {
|
|
|
4669
4739
|
}
|
|
4670
4740
|
for (const e of entries) {
|
|
4671
4741
|
if (e.name === ".git" || e.name === "node_modules") continue;
|
|
4672
|
-
const full =
|
|
4742
|
+
const full = join6(dir, e.name);
|
|
4673
4743
|
const rel = relative4(rootDir, full).replace(/\\/g, "/");
|
|
4674
4744
|
if (ig && ig.ignores(rel)) continue;
|
|
4675
4745
|
if (e.isDirectory()) {
|
|
@@ -4688,7 +4758,7 @@ function loadGitignore2(rootDir) {
|
|
|
4688
4758
|
if (!ignoreMod) return null;
|
|
4689
4759
|
const ig = (ignoreMod.default || ignoreMod)();
|
|
4690
4760
|
try {
|
|
4691
|
-
const content = readText(
|
|
4761
|
+
const content = readText(join6(rootDir, ".gitignore"));
|
|
4692
4762
|
ig.add(content);
|
|
4693
4763
|
} catch {
|
|
4694
4764
|
}
|
|
@@ -4794,7 +4864,7 @@ function errorResult(code, message, recovery, { large = false, extra = null } =
|
|
|
4794
4864
|
}
|
|
4795
4865
|
|
|
4796
4866
|
// server.mjs
|
|
4797
|
-
var version = true ? "1.
|
|
4867
|
+
var version = true ? "1.24.0" : (await null).createRequire(import.meta.url)("./package.json").version;
|
|
4798
4868
|
var STATUS_ENUM = z2.enum(["OK", "ERROR", "AUTO_REBASED", "CONFLICT", "STALE", "INVALID", "NO_CHANGES", "CHANGED", "UNSUPPORTED"]);
|
|
4799
4869
|
var ERROR_SHAPE = z2.object({ code: z2.string(), message: z2.string(), recovery: z2.string() }).optional();
|
|
4800
4870
|
var LINE_REPORT_KEYS = /* @__PURE__ */ new Set([
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@levnikolaevich/hex-line-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.24.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. 9 tools: inspect_path, read, edit, write, grep, outline, verify, changes, bulk_replace.",
|