@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.
Files changed (2) hide show
  1. package/dist/server.mjs +101 -31
  2. 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
- if (!isFilePathFresh(cached, projectRoot, filePath)) {
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
- if (!isFilePathFresh(db, projectRoot, filePath)) {
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: "stale", projectRoot };
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 = ensureGraphFreshForFile(db, real);
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 (opts.baseRevision) {
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
- lines.push(`changed_ranges: ${describeChangedRanges(computeChangedRanges(baseSnapshot.lines, currentSnapshot.lines))}`);
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 (groups.length > 0) {
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 = getGraphDB(join6(real, "__hex-line_probe__"));
4587
+ const db2 = getGraphDBForProject(real);
4518
4588
  const diff2 = await semanticGitDiff(real, { baseRef: compareAgainst });
4519
- const graphHint2 = graphUnavailableHint(join6(real, "__hex-line_probe__"));
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 join7 } from "node:path";
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 = join7(dir, e.name);
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(join7(rootDir, ".gitignore"));
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.23.0" : (await null).createRequire(import.meta.url)("./package.json").version;
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.23.0",
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.",