@levnikolaevich/hex-line-mcp 1.23.1 → 1.24.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 +88 -30
  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,30 @@ 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(directoryPath) {
443
+ if (_driverUnavailable) return { reason: "driver_missing" };
444
+ try {
445
+ if (!directoryPath) return { reason: "no_project_root" };
446
+ const projectRoot = findProjectRoot(join3(directoryPath, "__hex-line_probe__"));
447
+ if (!projectRoot) return { reason: "no_project_root" };
448
+ const dbPath = join3(projectRoot, ".hex-skills/codegraph", "index.db");
449
+ if (!existsSync2(dbPath)) return { reason: "index_missing", projectRoot };
450
+ if (_dbs.has(dbPath)) {
451
+ return { reason: "ok", projectRoot };
452
+ }
453
+ const require2 = createRequire(import.meta.url);
454
+ const Database = require2("better-sqlite3");
455
+ const db = new Database(dbPath, { readonly: true });
456
+ if (!validateContract(db)) {
425
457
  db.close();
426
- return { reason: "stale", projectRoot };
458
+ return { reason: "contract_mismatch", projectRoot };
427
459
  }
428
460
  _dbs.set(dbPath, db);
429
461
  return { reason: "ok", projectRoot };
@@ -432,9 +464,15 @@ function diagnoseGraph(filePath) {
432
464
  return { reason: "driver_missing" };
433
465
  }
434
466
  }
467
+ function getGraphDBForProject(directoryPath) {
468
+ const { reason, projectRoot } = diagnoseGraphForProject(directoryPath);
469
+ if (reason !== "ok" || !projectRoot) return null;
470
+ const dbPath = join3(projectRoot, ".hex-skills/codegraph", "index.db");
471
+ return _dbs.get(dbPath) || null;
472
+ }
435
473
  function graphUnavailableHint(filePath) {
436
474
  const { reason, projectRoot } = diagnoseGraph(filePath);
437
- if (reason === "ok") return [];
475
+ if (reason === "ok" || reason === "file_not_indexed") return [];
438
476
  const at = projectRoot ? ` at ${projectRoot.replace(/\\/g, "/")}` : "";
439
477
  switch (reason) {
440
478
  case "driver_missing":
@@ -451,6 +489,23 @@ function graphUnavailableHint(filePath) {
451
489
  return ["graph_enrichment: unavailable"];
452
490
  }
453
491
  }
492
+ function graphUnavailableHintForProject(projectRoot) {
493
+ const { reason } = diagnoseGraphForProject(projectRoot);
494
+ if (reason === "ok") return [];
495
+ const at = projectRoot ? ` at ${projectRoot.replace(/\\/g, "/")}` : "";
496
+ switch (reason) {
497
+ case "driver_missing":
498
+ return ["graph_enrichment: unavailable", "graph_fix: install better-sqlite3 in hex-line-mcp package"];
499
+ case "no_project_root":
500
+ return ["graph_enrichment: unavailable", "graph_fix: directory is outside any project root"];
501
+ case "index_missing":
502
+ return ["graph_enrichment: unavailable", `graph_fix: run mcp__hex-graph__index_project${at}`];
503
+ case "contract_mismatch":
504
+ return ["graph_enrichment: unavailable", `graph_fix: index built by incompatible hex-graph version; re-run mcp__hex-graph__index_project${at}`];
505
+ default:
506
+ return ["graph_enrichment: unavailable"];
507
+ }
508
+ }
454
509
  function validateContract(db) {
455
510
  try {
456
511
  for (const viewName of REQUIRED_VIEWS) {
@@ -3707,16 +3762,17 @@ function entrySummary(entry) {
3707
3762
  if (entry.status === "STALE") return "content changed since checksum capture";
3708
3763
  return entry.reason;
3709
3764
  }
3710
- function renderEntry(entry, index, total) {
3765
+ function renderEntry(entry, index, total, topLevelNextAction) {
3711
3766
  const parts = [
3712
3767
  `entry: ${index}/${total}`,
3713
3768
  `status: ${entry.status}`,
3714
3769
  entry.span ? `span: ${entry.span}` : null,
3715
3770
  `checksum: ${entry.checksum}`,
3716
- entry.currentChecksum && entry.currentChecksum !== entry.checksum ? `current_checksum: ${entry.currentChecksum}` : null,
3717
- `next_action: ${entryNextAction(entry)}`,
3718
- `summary: ${entrySummary(entry)}`
3771
+ entry.currentChecksum && entry.currentChecksum !== entry.checksum ? `current_checksum: ${entry.currentChecksum}` : null
3719
3772
  ].filter(Boolean);
3773
+ const action = entryNextAction(entry);
3774
+ if (action !== topLevelNextAction) parts.push(`next_action: ${action}`);
3775
+ if (entry.status !== "VALID") parts.push(`summary: ${entrySummary(entry)}`);
3720
3776
  return parts.join(" | ");
3721
3777
  }
3722
3778
  function verifyChecksums(filePath, checksums, opts = {}) {
@@ -3730,18 +3786,20 @@ function verifyChecksums(filePath, checksums, opts = {}) {
3730
3786
  const summary = summarizeStatuses(results);
3731
3787
  const status = summary.invalid > 0 ? STATUS.INVALID : summary.stale > 0 ? STATUS.STALE : STATUS.OK;
3732
3788
  const staleRanges = results.filter((entry) => entry.status === "STALE" && entry.span).map((entry) => entry.span);
3789
+ const topLevelNextAction = overallNextAction(summary);
3790
+ const verboseSummary = results.length > 1 || summary.stale > 0 || summary.invalid > 0;
3733
3791
  const lines = [
3734
3792
  `status: ${status}`,
3735
3793
  `reason: ${overallReason(status)}`,
3736
- `revision: ${currentSnapshot.revision}`,
3737
- `file: ${currentSnapshot.fileChecksum}`,
3738
- `summary: valid=${summary.valid} stale=${summary.stale} invalid=${summary.invalid}`,
3739
- `next_action: ${overallNextAction(summary)}`
3794
+ `revision: ${currentSnapshot.revision}`
3740
3795
  ];
3741
- if (opts.baseRevision) {
3796
+ if (verboseSummary) lines.push(`summary: valid=${summary.valid} stale=${summary.stale} invalid=${summary.invalid}`);
3797
+ lines.push(`next_action: ${topLevelNextAction}`);
3798
+ if (opts.baseRevision && opts.baseRevision !== currentSnapshot.revision) {
3742
3799
  lines.push(`base_revision: ${opts.baseRevision}`);
3743
3800
  if (hasBaseSnapshot) {
3744
- lines.push(`changed_ranges: ${describeChangedRanges(computeChangedRanges(baseSnapshot.lines, currentSnapshot.lines))}`);
3801
+ const changed = describeChangedRanges(computeChangedRanges(baseSnapshot.lines, currentSnapshot.lines));
3802
+ if (changed !== "none") lines.push(`changed_ranges: ${changed}`);
3745
3803
  } else {
3746
3804
  lines.push("base_revision_status: evicted");
3747
3805
  }
@@ -3749,7 +3807,7 @@ function verifyChecksums(filePath, checksums, opts = {}) {
3749
3807
  const suggestedReadCall = buildSuggestedReadCall2(filePath, staleRanges);
3750
3808
  if (suggestedReadCall) lines.push(`suggested_read_call: ${suggestedReadCall}`);
3751
3809
  if (results.length > 0) {
3752
- lines.push("", ...results.map((entry, index) => renderEntry(entry, index + 1, results.length)));
3810
+ lines.push("", ...results.map((entry, index) => renderEntry(entry, index + 1, results.length, topLevelNextAction)));
3753
3811
  }
3754
3812
  return lines.join("\n");
3755
3813
  }
@@ -3851,12 +3909,13 @@ function findByPattern(dirPath, opts) {
3851
3909
  const truncated = shown.length < matches.length;
3852
3910
  const groups = topPatternGroups(matches);
3853
3911
  const lines = [
3854
- `Found ${matches.length} match${matches.length === 1 ? "" : "es"} for "${opts.pattern}" in ${rootName}/`,
3855
- `match_count: ${matches.length}`,
3856
- `shown_count: ${shown.length}`,
3857
- `truncated: ${truncated}`
3912
+ `Found ${matches.length} match${matches.length === 1 ? "" : "es"} for "${opts.pattern}" in ${rootName}/`
3858
3913
  ];
3859
- if (groups.length > 0) {
3914
+ if (truncated) {
3915
+ lines.push(`shown_count: ${shown.length}`);
3916
+ lines.push(`truncated: true`);
3917
+ }
3918
+ if (groups.length > 1) {
3860
3919
  lines.push(`top_groups: ${groups.map(([group, count]) => `${group} (${count})`).join(", ")}`);
3861
3920
  }
3862
3921
  if (truncated) {
@@ -4225,7 +4284,6 @@ function autoSync() {
4225
4284
 
4226
4285
  // lib/changes.mjs
4227
4286
  import { statSync as statSync11 } from "node:fs";
4228
- import { join as join6 } from "node:path";
4229
4287
 
4230
4288
  // ../hex-common/src/git/semantic-diff.mjs
4231
4289
  import { execFileSync } from "node:child_process";
@@ -4528,9 +4586,9 @@ async function fileChanges(filePath, compareAgainst = "HEAD") {
4528
4586
  filePath = normalizePath(filePath);
4529
4587
  const real = validatePath(filePath);
4530
4588
  if (statSync11(real).isDirectory()) {
4531
- const db2 = getGraphDB(join6(real, "__hex-line_probe__"));
4589
+ const db2 = getGraphDBForProject(real);
4532
4590
  const diff2 = await semanticGitDiff(real, { baseRef: compareAgainst });
4533
- const graphHint2 = graphUnavailableHint(join6(real, "__hex-line_probe__"));
4591
+ const graphHint2 = graphUnavailableHintForProject(real);
4534
4592
  if (diff2.summary.changed_file_count === 0) {
4535
4593
  return [
4536
4594
  "status: NO_CHANGES",
@@ -4667,7 +4725,7 @@ async function fileChanges(filePath, compareAgainst = "HEAD") {
4667
4725
 
4668
4726
  // lib/bulk-replace.mjs
4669
4727
  import { writeFileSync as writeFileSync3, readdirSync as readdirSync3, renameSync as renameSync2, unlinkSync as unlinkSync2 } from "node:fs";
4670
- import { resolve as resolve9, relative as relative4, join as join7 } from "node:path";
4728
+ import { resolve as resolve9, relative as relative4, join as join6 } from "node:path";
4671
4729
  var ignoreMod;
4672
4730
  try {
4673
4731
  ignoreMod = await import("ignore");
@@ -4683,7 +4741,7 @@ function walkFiles(dir, rootDir, ig) {
4683
4741
  }
4684
4742
  for (const e of entries) {
4685
4743
  if (e.name === ".git" || e.name === "node_modules") continue;
4686
- const full = join7(dir, e.name);
4744
+ const full = join6(dir, e.name);
4687
4745
  const rel = relative4(rootDir, full).replace(/\\/g, "/");
4688
4746
  if (ig && ig.ignores(rel)) continue;
4689
4747
  if (e.isDirectory()) {
@@ -4702,7 +4760,7 @@ function loadGitignore2(rootDir) {
4702
4760
  if (!ignoreMod) return null;
4703
4761
  const ig = (ignoreMod.default || ignoreMod)();
4704
4762
  try {
4705
- const content = readText(join7(rootDir, ".gitignore"));
4763
+ const content = readText(join6(rootDir, ".gitignore"));
4706
4764
  ig.add(content);
4707
4765
  } catch {
4708
4766
  }
@@ -4808,7 +4866,7 @@ function errorResult(code, message, recovery, { large = false, extra = null } =
4808
4866
  }
4809
4867
 
4810
4868
  // server.mjs
4811
- var version = true ? "1.23.1" : (await null).createRequire(import.meta.url)("./package.json").version;
4869
+ var version = true ? "1.24.1" : (await null).createRequire(import.meta.url)("./package.json").version;
4812
4870
  var STATUS_ENUM = z2.enum(["OK", "ERROR", "AUTO_REBASED", "CONFLICT", "STALE", "INVALID", "NO_CHANGES", "CHANGED", "UNSUPPORTED"]);
4813
4871
  var ERROR_SHAPE = z2.object({ code: z2.string(), message: z2.string(), recovery: z2.string() }).optional();
4814
4872
  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.1",
3
+ "version": "1.24.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.",