@levnikolaevich/hex-line-mcp 1.22.0 → 1.23.1

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.
Files changed (2) hide show
  1. package/dist/server.mjs +87 -28
  2. package/package.json +1 -1
package/dist/server.mjs CHANGED
@@ -400,6 +400,57 @@ function getGraphDB(filePath, { allowStale = false } = {}) {
400
400
  return null;
401
401
  }
402
402
  }
403
+ function diagnoseGraph(filePath) {
404
+ if (_driverUnavailable) return { reason: "driver_missing" };
405
+ try {
406
+ const projectRoot = findProjectRoot(filePath);
407
+ if (!projectRoot) return { reason: "no_project_root" };
408
+ const dbPath = join3(projectRoot, ".hex-skills/codegraph", "index.db");
409
+ if (!existsSync2(dbPath)) return { reason: "index_missing", projectRoot };
410
+ if (_dbs.has(dbPath)) {
411
+ const cached = _dbs.get(dbPath);
412
+ if (!isFilePathFresh(cached, projectRoot, filePath)) {
413
+ return { reason: "stale", projectRoot };
414
+ }
415
+ return { reason: "ok", projectRoot };
416
+ }
417
+ const require2 = createRequire(import.meta.url);
418
+ const Database = require2("better-sqlite3");
419
+ const db = new Database(dbPath, { readonly: true });
420
+ if (!validateContract(db)) {
421
+ db.close();
422
+ return { reason: "contract_mismatch", projectRoot };
423
+ }
424
+ if (!isFilePathFresh(db, projectRoot, filePath)) {
425
+ db.close();
426
+ return { reason: "stale", projectRoot };
427
+ }
428
+ _dbs.set(dbPath, db);
429
+ return { reason: "ok", projectRoot };
430
+ } catch {
431
+ _driverUnavailable = true;
432
+ return { reason: "driver_missing" };
433
+ }
434
+ }
435
+ function graphUnavailableHint(filePath) {
436
+ const { reason, projectRoot } = diagnoseGraph(filePath);
437
+ if (reason === "ok") return [];
438
+ const at = projectRoot ? ` at ${projectRoot.replace(/\\/g, "/")}` : "";
439
+ switch (reason) {
440
+ case "driver_missing":
441
+ return ["graph_enrichment: unavailable", "graph_fix: install better-sqlite3 in hex-line-mcp package"];
442
+ case "no_project_root":
443
+ return ["graph_enrichment: unavailable", "graph_fix: file is outside any project root (no package.json / pyproject.toml / .git marker)"];
444
+ case "index_missing":
445
+ return ["graph_enrichment: unavailable", `graph_fix: run mcp__hex-graph__index_project${at}`];
446
+ case "contract_mismatch":
447
+ return ["graph_enrichment: unavailable", `graph_fix: index built by incompatible hex-graph version; re-run mcp__hex-graph__index_project${at}`];
448
+ case "stale":
449
+ return ["graph_enrichment: unavailable", `graph_fix: file modified after last index; re-run mcp__hex-graph__index_project${at} or wait for background refresh`];
450
+ default:
451
+ return ["graph_enrichment: unavailable"];
452
+ }
453
+ }
403
454
  function validateContract(db) {
404
455
  try {
405
456
  for (const viewName of REQUIRED_VIEWS) {
@@ -497,6 +548,20 @@ function ensureGraphFreshForFile(db, absoluteFilePath) {
497
548
  return true;
498
549
  }
499
550
  }
551
+ function isGraphFreshAtMtime(db, absoluteFilePath, mtimeMs) {
552
+ if (!db) return false;
553
+ try {
554
+ const projectRoot = findProjectRoot(absoluteFilePath);
555
+ if (!projectRoot) return true;
556
+ const relativeFile = normalizeRelativeFile(projectRoot, absoluteFilePath);
557
+ if (!relativeFile) return true;
558
+ const indexedMtime = lookupIndexedMtime(db, relativeFile);
559
+ if (indexedMtime == null) return false;
560
+ return mtimeMs <= indexedMtime + FRESHNESS_TOLERANCE_MS;
561
+ } catch {
562
+ return true;
563
+ }
564
+ }
500
565
  function fileAnnotations(db, file, { startLine = null, endLine = null, limit = 8 } = {}) {
501
566
  try {
502
567
  const hasRange = Number.isInteger(startLine) && Number.isInteger(endLine);
@@ -1267,7 +1332,7 @@ function serializeSearchBlock(block, opts = {}) {
1267
1332
  }
1268
1333
  if (block.meta.summary) lines.push(`summary: ${block.meta.summary}`);
1269
1334
  lines.push(...renderMetaLines(Object.fromEntries(
1270
- Object.entries(block.meta).filter(([key]) => key !== "matchLines" && key !== "summary")
1335
+ Object.entries(block.meta).filter(([key]) => key !== "matchLines" && key !== "summary" && key !== "graphScore")
1271
1336
  )));
1272
1337
  lines.push(...block.entries.map((entry) => serializeSearchEntry(entry, opts)));
1273
1338
  lines.push(`checksum: ${block.checksum}`);
@@ -2801,7 +2866,9 @@ changed_ranges: ${describeChangedRanges(changedRanges)}`;
2801
2866
  summary: lines_changed=${changedSpan} diff_entries=${diffEntryCount} lines_after=${lines.length}`;
2802
2867
  msg2 += `
2803
2868
  payload_sections: ${payloadSections(displayDiff ? ["diff"] : [])}`;
2804
- msg2 += "\ngraph_enrichment: unavailable";
2869
+ const hint = graphUnavailableHint(real);
2870
+ if (hint.length > 0) msg2 += `
2871
+ ${hint.join("\n")}`;
2805
2872
  msg2 += `
2806
2873
  Dry run: ${filePath} would change (${lines.length} lines)`;
2807
2874
  if (displayDiff) msg2 += `
@@ -2830,9 +2897,10 @@ remapped_refs:
2830
2897
  ${remaps.map(({ from, to }) => `${from} -> ${to}`).join("\n")}`;
2831
2898
  }
2832
2899
  let hasPostEditBlock = false;
2833
- let graphEnrichment = "unavailable";
2834
2900
  let semanticImpacts = [];
2835
2901
  let cloneWarnings = [];
2902
+ let graphDbAvailable = false;
2903
+ let graphFresh = true;
2836
2904
  msg += `
2837
2905
  Updated ${filePath} (${lines.length} lines)`;
2838
2906
  if (fullDiff && minLine <= maxLine) {
@@ -2859,7 +2927,8 @@ ${serializeReadBlock(block)}`;
2859
2927
  const db = getGraphDB(real, { allowStale: true });
2860
2928
  const relFile = db ? getRelativePath(real) : null;
2861
2929
  if (db && relFile && fullDiff && minLine <= maxLine) {
2862
- graphEnrichment = "available";
2930
+ graphDbAvailable = true;
2931
+ graphFresh = isGraphFreshAtMtime(db, real, currentSnapshot.mtimeMs);
2863
2932
  semanticImpacts = semanticImpact(db, relFile, minLine, maxLine);
2864
2933
  if (semanticImpacts.length > 0) {
2865
2934
  const sections = semanticImpacts.map((impact) => {
@@ -2892,7 +2961,7 @@ ${sections.join("\n")}`;
2892
2961
  }
2893
2962
  cloneWarnings = cloneWarning(db, relFile, minLine, maxLine);
2894
2963
  if (cloneWarnings.length > 0) {
2895
- const list = cloneWarnings.map((c) => `${c.file}:${c.line}`).join(", ");
2964
+ const list = cloneWarnings.map((c) => `${c.file}:${c.line}${c.cloneType ? ` (${c.cloneType})` : ""}`).join(", ");
2896
2965
  msg += `
2897
2966
 
2898
2967
  \u26A0 ${cloneWarnings.length} clone(s): ${list}`;
@@ -2900,20 +2969,17 @@ ${sections.join("\n")}`;
2900
2969
  }
2901
2970
  } catch {
2902
2971
  }
2972
+ if (!graphFresh) msg += "\ngraph_fresh: stale";
2903
2973
  const payloadKinds = [];
2904
2974
  if (hasPostEditBlock) payloadKinds.push("post_edit");
2905
2975
  if (semanticImpacts.length > 0) payloadKinds.push("semantic_impact");
2906
2976
  if (cloneWarnings.length > 0) payloadKinds.push("clone_warning");
2907
2977
  if (displayDiff) payloadKinds.push("diff");
2908
- const semanticFactCount = semanticImpacts.reduce((sum, impact) => sum + (impact.facts?.length || 0), 0);
2909
2978
  const summaryLineParts = [
2910
2979
  `summary: lines_changed=${changedSpan} diff_entries=${diffEntryCount} lines_after=${lines.length}${editContext.corrections.length > 0 ? ` boundary_echo_stripped=${editContext.corrections.length}` : ``}`,
2911
- `payload_sections: ${payloadSections(payloadKinds)}`,
2912
- `graph_enrichment: ${graphEnrichment}`
2980
+ `payload_sections: ${payloadSections(payloadKinds)}`
2913
2981
  ];
2914
- if (semanticImpacts.length > 0) summaryLineParts.push(`semantic_impact_count: ${semanticImpacts.length}`);
2915
- if (semanticFactCount > 0) summaryLineParts.push(`semantic_fact_count: ${semanticFactCount}`);
2916
- if (cloneWarnings.length > 0) summaryLineParts.push(`clone_warning_count: ${cloneWarnings.length}`);
2982
+ if (!graphDbAvailable) summaryLineParts.push(...graphUnavailableHint(real));
2917
2983
  const summaryLines = summaryLineParts.join("\n");
2918
2984
  msg = msg.replace(`
2919
2985
  Updated ${filePath} (${lines.length} lines)`, `
@@ -4420,9 +4486,6 @@ async function semanticGitDiff(targetPath, { baseRef = "HEAD", headRef = null }
4420
4486
  }
4421
4487
 
4422
4488
  // lib/changes.mjs
4423
- function graphEnrichmentState(db) {
4424
- return db ? "available" : "unavailable";
4425
- }
4426
4489
  function payloadSections2(sections) {
4427
4490
  return sections.length > 0 ? sections.join(",") : "summary_only";
4428
4491
  }
@@ -4467,7 +4530,7 @@ async function fileChanges(filePath, compareAgainst = "HEAD") {
4467
4530
  if (statSync11(real).isDirectory()) {
4468
4531
  const db2 = getGraphDB(join6(real, "__hex-line_probe__"));
4469
4532
  const diff2 = await semanticGitDiff(real, { baseRef: compareAgainst });
4470
- const graphEnrichment2 = graphEnrichmentState(db2);
4533
+ const graphHint2 = graphUnavailableHint(join6(real, "__hex-line_probe__"));
4471
4534
  if (diff2.summary.changed_file_count === 0) {
4472
4535
  return [
4473
4536
  "status: NO_CHANGES",
@@ -4476,7 +4539,7 @@ async function fileChanges(filePath, compareAgainst = "HEAD") {
4476
4539
  `compare_against: ${compareAgainst}`,
4477
4540
  "scope: directory",
4478
4541
  "summary: changed_files=0",
4479
- `graph_enrichment: ${graphEnrichment2}`
4542
+ ...graphHint2
4480
4543
  ].join("\n");
4481
4544
  }
4482
4545
  let emittedRiskCount = 0;
@@ -4490,7 +4553,7 @@ async function fileChanges(filePath, compareAgainst = "HEAD") {
4490
4553
  "scope: directory",
4491
4554
  `summary: changed_files=${diff2.summary.changed_file_count}`,
4492
4555
  `next_action: ${ACTION.INSPECT_FILE}`,
4493
- `graph_enrichment: ${graphEnrichment2}`,
4556
+ ...graphHint2,
4494
4557
  ""
4495
4558
  ];
4496
4559
  for (const file2 of diff2.changed_files) {
@@ -4513,16 +4576,14 @@ async function fileChanges(filePath, compareAgainst = "HEAD") {
4513
4576
  if (emittedRiskCount > 0) sectionKinds2.push("risk_summary");
4514
4577
  if (emittedRemovedApiWarnings > 0) sectionKinds2.push("removed_api_warning");
4515
4578
  const spliceLines = [];
4516
- if (emittedRiskCount > 0) spliceLines.push(`risk_summary_count: ${emittedRiskCount}`);
4517
- if (emittedRemovedApiWarnings > 0) spliceLines.push(`removed_api_warning_count: ${emittedRemovedApiWarnings}`);
4518
4579
  if (sectionKinds2.length > 0) spliceLines.push(`payload_sections: ${payloadSections2(sectionKinds2)}`);
4519
- sections.splice(8, 0, ...spliceLines);
4580
+ sections.splice(7 + graphHint2.length, 0, ...spliceLines);
4520
4581
  return sections.join("\n");
4521
4582
  }
4522
4583
  const db = getGraphDB(real);
4523
4584
  const diff = await semanticGitDiff(real, { baseRef: compareAgainst });
4524
4585
  const file = diff.changed_files[0];
4525
- const graphEnrichment = graphEnrichmentState(db);
4586
+ const graphHint = graphUnavailableHint(real);
4526
4587
  if (!file) {
4527
4588
  return [
4528
4589
  "status: NO_CHANGES",
@@ -4531,7 +4592,7 @@ async function fileChanges(filePath, compareAgainst = "HEAD") {
4531
4592
  `compare_against: ${compareAgainst}`,
4532
4593
  "scope: file",
4533
4594
  "summary: added=0 removed=0 modified=0",
4534
- `graph_enrichment: ${graphEnrichment}`
4595
+ ...graphHint
4535
4596
  ].join("\n");
4536
4597
  }
4537
4598
  if (!file.semantic_supported) {
@@ -4543,7 +4604,7 @@ async function fileChanges(filePath, compareAgainst = "HEAD") {
4543
4604
  "scope: file",
4544
4605
  `summary: semantic diff unavailable for ${file.extension} files`,
4545
4606
  `next_action: ${ACTION.INSPECT_RAW_DIFF}`,
4546
- `graph_enrichment: ${graphEnrichment}`
4607
+ ...graphHint
4547
4608
  ].join("\n");
4548
4609
  }
4549
4610
  const relFile = getRelativePath(real) || file.path?.replace(/\\/g, "/");
@@ -4557,10 +4618,8 @@ async function fileChanges(filePath, compareAgainst = "HEAD") {
4557
4618
  `compare_against: ${compareAgainst}`,
4558
4619
  "scope: file",
4559
4620
  `summary: ${symbolCountSummary(file)}`,
4560
- `graph_enrichment: ${graphEnrichment}`
4621
+ ...graphHint
4561
4622
  ];
4562
- if (riskLines.length > 0) parts.push(`risk_summary_count: ${riskLines.length}`);
4563
- if (removedApiWarnings.length > 0) parts.push(`removed_api_warning_count: ${removedApiWarnings.length}`);
4564
4623
  if (file.added_symbols.length) {
4565
4624
  sectionKinds.push("added");
4566
4625
  parts.push(`next_action: ${ACTION.REVIEW_RISKS}`);
@@ -4594,7 +4653,7 @@ async function fileChanges(filePath, compareAgainst = "HEAD") {
4594
4653
  if (riskLines.length > 0) sectionKinds.push("risk_summary");
4595
4654
  if (removedApiWarnings.length > 0) sectionKinds.push("removed_api_warning");
4596
4655
  if (sectionKinds.length > 0) {
4597
- const insertIdx = 7 + (riskLines.length > 0 ? 1 : 0) + (removedApiWarnings.length > 0 ? 1 : 0);
4656
+ const insertIdx = 6 + graphHint.length;
4598
4657
  parts.splice(insertIdx, 0, `payload_sections: ${payloadSections2(sectionKinds)}`);
4599
4658
  }
4600
4659
  if (riskLines.length || removedApiWarnings.length) {
@@ -4749,7 +4808,7 @@ function errorResult(code, message, recovery, { large = false, extra = null } =
4749
4808
  }
4750
4809
 
4751
4810
  // server.mjs
4752
- var version = true ? "1.22.0" : (await null).createRequire(import.meta.url)("./package.json").version;
4811
+ var version = true ? "1.23.1" : (await null).createRequire(import.meta.url)("./package.json").version;
4753
4812
  var STATUS_ENUM = z2.enum(["OK", "ERROR", "AUTO_REBASED", "CONFLICT", "STALE", "INVALID", "NO_CHANGES", "CHANGED", "UNSUPPORTED"]);
4754
4813
  var ERROR_SHAPE = z2.object({ code: z2.string(), message: z2.string(), recovery: z2.string() }).optional();
4755
4814
  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.22.0",
3
+ "version": "1.23.1",
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.",