@levnikolaevich/hex-line-mcp 1.21.1 → 1.23.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/hook.mjs CHANGED
@@ -54,7 +54,7 @@ function normalizeOutput(text, opts = {}) {
54
54
  }
55
55
 
56
56
  // hook.mjs
57
- import { readFileSync, writeSync, writeFileSync, mkdirSync, readdirSync, statSync, unlinkSync } from "node:fs";
57
+ import { readFileSync, writeSync } from "node:fs";
58
58
  import { resolve } from "node:path";
59
59
  import { homedir } from "node:os";
60
60
 
@@ -361,53 +361,6 @@ function extractBashText(response) {
361
361
  }
362
362
  return "";
363
363
  }
364
- var ERROR_RECOVERY_DIR = ".hex-skills/logs/error_recovery";
365
- var ERROR_RECOVERY_MAX_FILES = 20;
366
- var ERROR_RECOVERY_MAX_BYTES = 1024 * 1024;
367
- function isBashFailure(response) {
368
- if (!response || typeof response !== "object") return false;
369
- if (response.is_error === true) return true;
370
- if (response.interrupted === true) return true;
371
- if (typeof response.exit_code === "number" && response.exit_code !== 0) return true;
372
- return false;
373
- }
374
- function saveErrorArtifact(rawText, commandType, command, cwd) {
375
- try {
376
- const dir = resolve(cwd, ERROR_RECOVERY_DIR);
377
- mkdirSync(dir, { recursive: true });
378
- try {
379
- const entries = readdirSync(dir).filter((name) => name.endsWith(".log")).map((name) => {
380
- try {
381
- return { name, mtime: statSync(resolve(dir, name)).mtimeMs };
382
- } catch {
383
- return null;
384
- }
385
- }).filter((e) => e !== null).sort((a, b) => b.mtime - a.mtime);
386
- for (const entry of entries.slice(ERROR_RECOVERY_MAX_FILES - 1)) {
387
- try {
388
- unlinkSync(resolve(dir, entry.name));
389
- } catch {
390
- }
391
- }
392
- } catch {
393
- }
394
- let content = `# command: ${String(command).slice(0, 500)}
395
- # type: ${commandType}
396
- # timestamp: ${(/* @__PURE__ */ new Date()).toISOString()}
397
-
398
- ${rawText}`;
399
- if (Buffer.byteLength(content, "utf-8") > ERROR_RECOVERY_MAX_BYTES) {
400
- content = content.slice(0, ERROR_RECOVERY_MAX_BYTES) + "\n[TRUNCATED at 1 MB]";
401
- }
402
- const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
403
- const fileName = `${ts}_${commandType}.log`;
404
- const filePath = resolve(dir, fileName);
405
- writeFileSync(filePath, content, "utf-8");
406
- return `${ERROR_RECOVERY_DIR}/${fileName}`;
407
- } catch {
408
- return null;
409
- }
410
- }
411
364
  var _hexLineDisabled = null;
412
365
  function isHexLineDisabled(configPath) {
413
366
  if (_hexLineDisabled !== null) return _hexLineDisabled;
@@ -600,18 +553,10 @@ function handlePostToolUse(data) {
600
553
  if (!rawText) {
601
554
  process.exit(0);
602
555
  }
603
- const failure = isBashFailure(data.tool_response);
604
556
  const type = detectCommandType(command);
605
- let recoveryPath = null;
606
- if (failure) {
607
- recoveryPath = saveErrorArtifact(rawText, type, command, process.cwd());
608
- }
609
557
  const lines = rawText.split("\n");
610
558
  const originalCount = lines.length;
611
559
  if (originalCount < HOOK_OUTPUT_POLICY.lineThreshold) {
612
- if (recoveryPath) {
613
- safeExit(2, `Full output preserved at: ${recoveryPath}`, 2);
614
- }
615
560
  process.exit(0);
616
561
  }
617
562
  const filtered = normalizeOutput(lines.join("\n"), {
@@ -620,7 +565,7 @@ function handlePostToolUse(data) {
620
565
  });
621
566
  const filteredCount = filtered.split("\n").length;
622
567
  const header = `RTK FILTERED: ${type} (${originalCount} lines -> ${filteredCount} lines)`;
623
- const outputParts = [
568
+ const output = [
624
569
  "=".repeat(50),
625
570
  header,
626
571
  "=".repeat(50),
@@ -628,42 +573,11 @@ function handlePostToolUse(data) {
628
573
  filtered,
629
574
  "",
630
575
  "-".repeat(50),
631
- `Original: ${originalCount} lines | Filtered: ${filteredCount} lines`
632
- ];
633
- if (recoveryPath) {
634
- outputParts.push(`Full output preserved at: ${recoveryPath}`);
635
- }
636
- outputParts.push("=".repeat(50));
637
- const output = outputParts.join("\n");
576
+ `Original: ${originalCount} lines | Filtered: ${filteredCount} lines`,
577
+ "=".repeat(50)
578
+ ].join("\n");
638
579
  safeExit(2, output, 2);
639
580
  }
640
- function handlePostToolUseFailure(data) {
641
- const toolName = data.tool_name || "";
642
- if (toolName !== "Bash") {
643
- process.exit(0);
644
- }
645
- const toolInput = data.tool_input || {};
646
- const command = toolInput.command || "";
647
- const error = typeof data.error === "string" ? data.error : "";
648
- const isInterrupt = data.is_interrupt === true;
649
- if (!command && !error) {
650
- process.exit(0);
651
- }
652
- const type = detectCommandType(command);
653
- const body = `error: ${error}
654
- is_interrupt: ${isInterrupt}`;
655
- const recoveryPath = saveErrorArtifact(body, type, command, process.cwd());
656
- if (!recoveryPath) {
657
- process.exit(0);
658
- }
659
- const output = {
660
- hookSpecificOutput: {
661
- hookEventName: "PostToolUseFailure",
662
- additionalContext: `Failure metadata logged at: ${recoveryPath}`
663
- }
664
- };
665
- safeExit(1, JSON.stringify(output), 0);
666
- }
667
581
  function handleSessionStart() {
668
582
  const settingsFiles = [
669
583
  resolve(process.cwd(), ".claude/settings.local.json"),
@@ -721,7 +635,6 @@ if (_norm(process.argv[1]) === _norm(fileURLToPath(import.meta.url))) {
721
635
  if (event === "SessionStart") handleSessionStart();
722
636
  else if (event === "PreToolUse") handlePreToolUse(data);
723
637
  else if (event === "PostToolUse") handlePostToolUse(data);
724
- else if (event === "PostToolUseFailure") handlePostToolUseFailure(data);
725
638
  else if (event === "PermissionDenied") handlePermissionDenied(data);
726
639
  else process.exit(0);
727
640
  } catch {
package/dist/server.mjs CHANGED
@@ -202,6 +202,13 @@ function readText(filePath) {
202
202
 
203
203
  // lib/security.mjs
204
204
  var MAX_FILE_SIZE = 10 * 1024 * 1024;
205
+ var EXTERNAL_SAFE_FOLDERS = [
206
+ ".hex-skills/",
207
+ ".claude/"
208
+ ];
209
+ function isInExternalSafeFolder(absPath) {
210
+ return EXTERNAL_SAFE_FOLDERS.some((folder) => absPath.includes(folder));
211
+ }
205
212
  function normalizePath(p) {
206
213
  if (process.platform === "win32") {
207
214
  if (p === "/tmp" || p.startsWith("/tmp/")) {
@@ -234,6 +241,7 @@ function assertProjectScopedPath(filePath, { allowExternal = false } = {}) {
234
241
  if (!filePath) throw new Error("Empty file path");
235
242
  const abs = resolveInputPath(filePath);
236
243
  if (allowExternal) return abs;
244
+ if (isInExternalSafeFolder(abs)) return abs;
237
245
  const projectRoot = resolve(process.cwd()).replace(/\\/g, "/");
238
246
  if (isWithinRoot(projectRoot, abs)) return abs;
239
247
  throw new Error(
@@ -392,6 +400,57 @@ function getGraphDB(filePath, { allowStale = false } = {}) {
392
400
  return null;
393
401
  }
394
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
+ }
395
454
  function validateContract(db) {
396
455
  try {
397
456
  for (const viewName of REQUIRED_VIEWS) {
@@ -1259,7 +1318,7 @@ function serializeSearchBlock(block, opts = {}) {
1259
1318
  }
1260
1319
  if (block.meta.summary) lines.push(`summary: ${block.meta.summary}`);
1261
1320
  lines.push(...renderMetaLines(Object.fromEntries(
1262
- Object.entries(block.meta).filter(([key]) => key !== "matchLines" && key !== "summary")
1321
+ Object.entries(block.meta).filter(([key]) => key !== "matchLines" && key !== "summary" && key !== "graphScore")
1263
1322
  )));
1264
1323
  lines.push(...block.entries.map((entry) => serializeSearchEntry(entry, opts)));
1265
1324
  lines.push(`checksum: ${block.checksum}`);
@@ -1893,8 +1952,6 @@ retry_edit: ${entry.retry_edit}`;
1893
1952
  if (entry.remapped_refs) msg += `
1894
1953
  remapped_refs: ${entry.remapped_refs}`;
1895
1954
  msg += `
1896
- summary: ${entry.summary}`;
1897
- msg += `
1898
1955
  snippet: ${entry.snippet.range}
1899
1956
  ${entry.snippet.text}`;
1900
1957
  return msg;
@@ -1929,8 +1986,7 @@ function buildSingleConflictReport({
1929
1986
  function renderSingleConflictReport(report) {
1930
1987
  let msg = `status: ${report.status}
1931
1988
  reason: ${report.reason}
1932
- revision: ${report.revision}
1933
- file: ${report.file}`;
1989
+ revision: ${report.revision}`;
1934
1990
  if (report.path) msg += `
1935
1991
  path: ${report.path}`;
1936
1992
  if (report.changed_ranges) msg += `
@@ -1950,8 +2006,6 @@ retry_plan: ${report.retry_plan}`;
1950
2006
  if (report.remapped_refs) msg += `
1951
2007
  remapped_refs: ${report.remapped_refs}`;
1952
2008
  msg += `
1953
- summary: ${report.summary}`;
1954
- msg += `
1955
2009
  snippet: ${report.snippet.range}
1956
2010
  ${report.snippet.text}`;
1957
2011
  return msg;
@@ -2005,12 +2059,11 @@ function buildSuggestedReadCall(filePath, recoveryRanges) {
2005
2059
  }
2006
2060
  });
2007
2061
  }
2008
- function buildRetryPlan(filePath, { recoveryRanges = [], retryEdit = null, retryEdits = null } = {}) {
2062
+ function buildRetryPlan(filePath, { retryEdit = null, retryEdits = null } = {}) {
2009
2063
  if (!filePath) return null;
2010
2064
  const steps = [];
2011
2065
  const parsedRetryEdits = Array.isArray(retryEdits) ? retryEdits.map(parseRetryEdit).filter(Boolean) : [];
2012
2066
  const parsedRetryEdit = parsedRetryEdits.length === 0 ? parseRetryEdit(retryEdit) : null;
2013
- const dedupedRanges = dedupeRanges(recoveryRanges);
2014
2067
  if (parsedRetryEdits.length > 0) {
2015
2068
  steps.push({
2016
2069
  tool: "mcp__hex-line__edit_file",
@@ -2029,14 +2082,6 @@ function buildRetryPlan(filePath, { recoveryRanges = [], retryEdit = null, retry
2029
2082
  conflict_policy: "conservative"
2030
2083
  }
2031
2084
  });
2032
- } else if (dedupedRanges.length > 0) {
2033
- steps.push({
2034
- tool: "mcp__hex-line__read_file",
2035
- arguments: {
2036
- path: filePath,
2037
- ranges: dedupedRanges
2038
- }
2039
- });
2040
2085
  }
2041
2086
  if (steps.length === 0) return null;
2042
2087
  return JSON.stringify({ steps });
@@ -2119,7 +2164,7 @@ function buildRetryEdit(edit, lines, options = {}) {
2119
2164
  function buildBatchConflictMessage({
2120
2165
  filePath,
2121
2166
  revision,
2122
- fileChecksum,
2167
+ fileChecksum: _fileChecksum,
2123
2168
  lines,
2124
2169
  changedRanges,
2125
2170
  conflicts
@@ -2147,7 +2192,6 @@ function buildBatchConflictMessage({
2147
2192
  let msg = `status: ${STATUS.CONFLICT}
2148
2193
  reason: ${REASON.BATCH_CONFLICT}
2149
2194
  revision: ${revision}
2150
- file: ${fileChecksum}
2151
2195
  edit_conflicts: ${conflicts.length}`;
2152
2196
  if (filePath) msg += `
2153
2197
  path: ${filePath}`;
@@ -2629,8 +2673,7 @@ function editFile(filePath, edits, opts = {}) {
2629
2673
  const nextAction = deriveNextAction({ retryEdit, suggestedReadCall });
2630
2674
  let msg2 = `status: ${STATUS.CONFLICT}
2631
2675
  reason: ${reason}
2632
- revision: ${currentSnapshot.revision}
2633
- file: ${currentSnapshot.fileChecksum}`;
2676
+ revision: ${currentSnapshot.revision}`;
2634
2677
  if (filePath) msg2 += `
2635
2678
  path: ${filePath}`;
2636
2679
  if (staleRevision && hasBaseSnapshot) msg2 += `
@@ -2648,8 +2691,6 @@ suggested_read_call: ${suggestedReadCall}`;
2648
2691
  if (retryPlan) msg2 += `
2649
2692
  retry_plan: ${retryPlan}`;
2650
2693
  msg2 += `
2651
- summary: ${summarizeConflictDetails(details)}`;
2652
- msg2 += `
2653
2694
  snippet: ${snip.start}-${snip.end}
2654
2695
  ${snip.text}`;
2655
2696
  return new Error(msg2);
@@ -2804,21 +2845,16 @@ ${snip.text}`;
2804
2845
  if (opts.dryRun) {
2805
2846
  let msg2 = `status: ${autoRebased ? STATUS.AUTO_REBASED : STATUS.OK}
2806
2847
  reason: ${REASON.DRY_RUN_PREVIEW}
2807
- revision: ${currentSnapshot.revision}
2808
- file: ${currentSnapshot.fileChecksum}`;
2848
+ revision: ${currentSnapshot.revision}`;
2809
2849
  if (staleRevision && hasBaseSnapshot) msg2 += `
2810
2850
  changed_ranges: ${describeChangedRanges(changedRanges)}`;
2811
2851
  msg2 += `
2812
2852
  summary: lines_changed=${changedSpan} diff_entries=${diffEntryCount} lines_after=${lines.length}`;
2813
2853
  msg2 += `
2814
- next_action: ${ACTION.KEEP_USING}`;
2815
- msg2 += `
2816
2854
  payload_sections: ${payloadSections(displayDiff ? ["diff"] : [])}`;
2817
- msg2 += "\ngraph_enrichment: unavailable";
2818
- msg2 += "\nsemantic_impact_count: 0";
2819
- msg2 += "\nsemantic_fact_count: 0";
2820
- msg2 += "\nclone_warning_count: 0";
2821
- msg2 += "\nprovenance_summary: edit_protocol=snapshot+anchors graph=unavailable";
2855
+ const hint = graphUnavailableHint(real);
2856
+ if (hint.length > 0) msg2 += `
2857
+ ${hint.join("\n")}`;
2822
2858
  msg2 += `
2823
2859
  Dry run: ${filePath} would change (${lines.length} lines)`;
2824
2860
  if (displayDiff) msg2 += `
@@ -2834,23 +2870,23 @@ ${displayDiff}
2834
2870
  const nextSnapshot = rememberSnapshot(real, content, { mtimeMs: nextStat.mtimeMs, size: nextStat.size });
2835
2871
  let msg = `status: ${autoRebased ? STATUS.AUTO_REBASED : STATUS.OK}
2836
2872
  reason: ${autoRebased ? REASON.EDIT_AUTO_REBASED : REASON.EDIT_APPLIED}
2837
- revision: ${nextSnapshot.revision}
2838
- file: ${nextSnapshot.fileChecksum}`;
2873
+ revision: ${nextSnapshot.revision}`;
2839
2874
  if (autoRebased && staleRevision && hasBaseSnapshot) {
2840
2875
  msg += `
2841
2876
  changed_ranges: ${describeChangedRanges(changedRanges)}`;
2842
- }
2843
- msg += `
2877
+ msg += `
2844
2878
  next_action: ${ACTION.KEEP_USING}`;
2879
+ }
2845
2880
  if (remaps.length > 0) {
2846
2881
  msg += `
2847
2882
  remapped_refs:
2848
2883
  ${remaps.map(({ from, to }) => `${from} -> ${to}`).join("\n")}`;
2849
2884
  }
2850
2885
  let hasPostEditBlock = false;
2851
- let graphEnrichment = "unavailable";
2852
2886
  let semanticImpacts = [];
2853
2887
  let cloneWarnings = [];
2888
+ let graphDbAvailable = false;
2889
+ let graphFresh = true;
2854
2890
  msg += `
2855
2891
  Updated ${filePath} (${lines.length} lines)`;
2856
2892
  if (fullDiff && minLine <= maxLine) {
@@ -2877,7 +2913,8 @@ ${serializeReadBlock(block)}`;
2877
2913
  const db = getGraphDB(real, { allowStale: true });
2878
2914
  const relFile = db ? getRelativePath(real) : null;
2879
2915
  if (db && relFile && fullDiff && minLine <= maxLine) {
2880
- graphEnrichment = "available";
2916
+ graphDbAvailable = true;
2917
+ graphFresh = ensureGraphFreshForFile(db, real);
2881
2918
  semanticImpacts = semanticImpact(db, relFile, minLine, maxLine);
2882
2919
  if (semanticImpacts.length > 0) {
2883
2920
  const sections = semanticImpacts.map((impact) => {
@@ -2910,7 +2947,7 @@ ${sections.join("\n")}`;
2910
2947
  }
2911
2948
  cloneWarnings = cloneWarning(db, relFile, minLine, maxLine);
2912
2949
  if (cloneWarnings.length > 0) {
2913
- const list = cloneWarnings.map((c) => `${c.file}:${c.line}`).join(", ");
2950
+ const list = cloneWarnings.map((c) => `${c.file}:${c.line}${c.cloneType ? ` (${c.cloneType})` : ""}`).join(", ");
2914
2951
  msg += `
2915
2952
 
2916
2953
  \u26A0 ${cloneWarnings.length} clone(s): ${list}`;
@@ -2918,21 +2955,18 @@ ${sections.join("\n")}`;
2918
2955
  }
2919
2956
  } catch {
2920
2957
  }
2958
+ if (!graphFresh) msg += "\ngraph_fresh: stale";
2921
2959
  const payloadKinds = [];
2922
2960
  if (hasPostEditBlock) payloadKinds.push("post_edit");
2923
2961
  if (semanticImpacts.length > 0) payloadKinds.push("semantic_impact");
2924
2962
  if (cloneWarnings.length > 0) payloadKinds.push("clone_warning");
2925
2963
  if (displayDiff) payloadKinds.push("diff");
2926
- const semanticFactCount = semanticImpacts.reduce((sum, impact) => sum + (impact.facts?.length || 0), 0);
2927
- const summaryLines = [
2964
+ const summaryLineParts = [
2928
2965
  `summary: lines_changed=${changedSpan} diff_entries=${diffEntryCount} lines_after=${lines.length}${editContext.corrections.length > 0 ? ` boundary_echo_stripped=${editContext.corrections.length}` : ``}`,
2929
- `payload_sections: ${payloadSections(payloadKinds)}`,
2930
- `graph_enrichment: ${graphEnrichment}`,
2931
- `semantic_impact_count: ${semanticImpacts.length}`,
2932
- `semantic_fact_count: ${semanticFactCount}`,
2933
- `clone_warning_count: ${cloneWarnings.length}`,
2934
- `provenance_summary: edit_protocol=snapshot+anchors graph=${graphEnrichment === "available" ? "hex_line_contract" : "unavailable"}`
2935
- ].join("\n");
2966
+ `payload_sections: ${payloadSections(payloadKinds)}`
2967
+ ];
2968
+ if (!graphDbAvailable) summaryLineParts.push(...graphUnavailableHint(real));
2969
+ const summaryLines = summaryLineParts.join("\n");
2936
2970
  msg = msg.replace(`
2937
2971
  Updated ${filePath} (${lines.length} lines)`, `
2938
2972
  ${summaryLines}
@@ -4050,10 +4084,6 @@ var CLAUDE_HOOKS = {
4050
4084
  PostToolUse: {
4051
4085
  matcher: "Bash",
4052
4086
  hooks: [{ type: "command", command: HOOK_COMMAND, timeout: 10 }]
4053
- },
4054
- PostToolUseFailure: {
4055
- matcher: "Bash",
4056
- hooks: [{ type: "command", command: HOOK_COMMAND, timeout: 5 }]
4057
4087
  }
4058
4088
  };
4059
4089
  function readJson(filePath) {
@@ -4114,6 +4144,15 @@ function writeHooksToFile(settingsPath) {
4114
4144
  changed = true;
4115
4145
  }
4116
4146
  }
4147
+ for (const event of Object.keys(config.hooks)) {
4148
+ if (event in CLAUDE_HOOKS) continue;
4149
+ if (!Array.isArray(config.hooks[event])) continue;
4150
+ const idx = findEntryByCommand(config.hooks[event]);
4151
+ if (idx < 0) continue;
4152
+ config.hooks[event].splice(idx, 1);
4153
+ if (config.hooks[event].length === 0) delete config.hooks[event];
4154
+ changed = true;
4155
+ }
4117
4156
  if (changed) writeJson(settingsPath, config);
4118
4157
  return changed;
4119
4158
  }
@@ -4433,12 +4472,6 @@ async function semanticGitDiff(targetPath, { baseRef = "HEAD", headRef = null }
4433
4472
  }
4434
4473
 
4435
4474
  // lib/changes.mjs
4436
- function graphEnrichmentState(db) {
4437
- return db ? "available" : "unavailable";
4438
- }
4439
- function provenanceSummary(semanticDiffState, db) {
4440
- return `semantic_diff=${semanticDiffState} graph=${db ? "hex_line_contract" : "unavailable"}`;
4441
- }
4442
4475
  function payloadSections2(sections) {
4443
4476
  return sections.length > 0 ? sections.join(",") : "summary_only";
4444
4477
  }
@@ -4483,7 +4516,7 @@ async function fileChanges(filePath, compareAgainst = "HEAD") {
4483
4516
  if (statSync11(real).isDirectory()) {
4484
4517
  const db2 = getGraphDB(join6(real, "__hex-line_probe__"));
4485
4518
  const diff2 = await semanticGitDiff(real, { baseRef: compareAgainst });
4486
- const graphEnrichment2 = graphEnrichmentState(db2);
4519
+ const graphHint2 = graphUnavailableHint(join6(real, "__hex-line_probe__"));
4487
4520
  if (diff2.summary.changed_file_count === 0) {
4488
4521
  return [
4489
4522
  "status: NO_CHANGES",
@@ -4492,16 +4525,9 @@ async function fileChanges(filePath, compareAgainst = "HEAD") {
4492
4525
  `compare_against: ${compareAgainst}`,
4493
4526
  "scope: directory",
4494
4527
  "summary: changed_files=0",
4495
- `next_action: ${ACTION.NO_ACTION}`,
4496
- `graph_enrichment: ${graphEnrichment2}`,
4497
- "risk_summary_count: 0",
4498
- "removed_api_warning_count: 0",
4499
- `payload_sections: ${payloadSections2([])}`,
4500
- `provenance_summary: ${provenanceSummary("clean", db2)}`
4528
+ ...graphHint2
4501
4529
  ].join("\n");
4502
4530
  }
4503
- const supportedCount = diff2.changed_files.filter((entry) => entry.semantic_supported).length;
4504
- const semanticDiffState = supportedCount === 0 ? "unsupported" : supportedCount === diff2.changed_files.length ? "semantic" : "mixed";
4505
4531
  let emittedRiskCount = 0;
4506
4532
  let emittedRemovedApiWarnings = 0;
4507
4533
  const sectionKinds2 = ["files"];
@@ -4513,7 +4539,7 @@ async function fileChanges(filePath, compareAgainst = "HEAD") {
4513
4539
  "scope: directory",
4514
4540
  `summary: changed_files=${diff2.summary.changed_file_count}`,
4515
4541
  `next_action: ${ACTION.INSPECT_FILE}`,
4516
- `graph_enrichment: ${graphEnrichment2}`,
4542
+ ...graphHint2,
4517
4543
  ""
4518
4544
  ];
4519
4545
  for (const file2 of diff2.changed_files) {
@@ -4535,20 +4561,15 @@ async function fileChanges(filePath, compareAgainst = "HEAD") {
4535
4561
  }
4536
4562
  if (emittedRiskCount > 0) sectionKinds2.push("risk_summary");
4537
4563
  if (emittedRemovedApiWarnings > 0) sectionKinds2.push("removed_api_warning");
4538
- sections.splice(
4539
- 8,
4540
- 0,
4541
- `risk_summary_count: ${emittedRiskCount}`,
4542
- `removed_api_warning_count: ${emittedRemovedApiWarnings}`,
4543
- `payload_sections: ${payloadSections2(sectionKinds2)}`,
4544
- `provenance_summary: ${provenanceSummary(semanticDiffState, db2)}`
4545
- );
4564
+ const spliceLines = [];
4565
+ if (sectionKinds2.length > 0) spliceLines.push(`payload_sections: ${payloadSections2(sectionKinds2)}`);
4566
+ sections.splice(7 + graphHint2.length, 0, ...spliceLines);
4546
4567
  return sections.join("\n");
4547
4568
  }
4548
4569
  const db = getGraphDB(real);
4549
4570
  const diff = await semanticGitDiff(real, { baseRef: compareAgainst });
4550
4571
  const file = diff.changed_files[0];
4551
- const graphEnrichment = graphEnrichmentState(db);
4572
+ const graphHint = graphUnavailableHint(real);
4552
4573
  if (!file) {
4553
4574
  return [
4554
4575
  "status: NO_CHANGES",
@@ -4557,12 +4578,7 @@ async function fileChanges(filePath, compareAgainst = "HEAD") {
4557
4578
  `compare_against: ${compareAgainst}`,
4558
4579
  "scope: file",
4559
4580
  "summary: added=0 removed=0 modified=0",
4560
- `next_action: ${ACTION.NO_ACTION}`,
4561
- `graph_enrichment: ${graphEnrichment}`,
4562
- "risk_summary_count: 0",
4563
- "removed_api_warning_count: 0",
4564
- `payload_sections: ${payloadSections2([])}`,
4565
- `provenance_summary: ${provenanceSummary("clean", db)}`
4581
+ ...graphHint
4566
4582
  ].join("\n");
4567
4583
  }
4568
4584
  if (!file.semantic_supported) {
@@ -4574,11 +4590,7 @@ async function fileChanges(filePath, compareAgainst = "HEAD") {
4574
4590
  "scope: file",
4575
4591
  `summary: semantic diff unavailable for ${file.extension} files`,
4576
4592
  `next_action: ${ACTION.INSPECT_RAW_DIFF}`,
4577
- `graph_enrichment: ${graphEnrichment}`,
4578
- "risk_summary_count: 0",
4579
- "removed_api_warning_count: 0",
4580
- `payload_sections: ${payloadSections2([])}`,
4581
- `provenance_summary: ${provenanceSummary("unsupported", db)}`
4593
+ ...graphHint
4582
4594
  ].join("\n");
4583
4595
  }
4584
4596
  const relFile = getRelativePath(real) || file.path?.replace(/\\/g, "/");
@@ -4592,9 +4604,7 @@ async function fileChanges(filePath, compareAgainst = "HEAD") {
4592
4604
  `compare_against: ${compareAgainst}`,
4593
4605
  "scope: file",
4594
4606
  `summary: ${symbolCountSummary(file)}`,
4595
- `graph_enrichment: ${graphEnrichment}`,
4596
- `risk_summary_count: ${riskLines.length}`,
4597
- `removed_api_warning_count: ${removedApiWarnings.length}`
4607
+ ...graphHint
4598
4608
  ];
4599
4609
  if (file.added_symbols.length) {
4600
4610
  sectionKinds.push("added");
@@ -4628,12 +4638,10 @@ async function fileChanges(filePath, compareAgainst = "HEAD") {
4628
4638
  }
4629
4639
  if (riskLines.length > 0) sectionKinds.push("risk_summary");
4630
4640
  if (removedApiWarnings.length > 0) sectionKinds.push("removed_api_warning");
4631
- parts.splice(
4632
- 9,
4633
- 0,
4634
- `payload_sections: ${payloadSections2(sectionKinds)}`,
4635
- `provenance_summary: ${provenanceSummary("semantic", db)}`
4636
- );
4641
+ if (sectionKinds.length > 0) {
4642
+ const insertIdx = 6 + graphHint.length;
4643
+ parts.splice(insertIdx, 0, `payload_sections: ${payloadSections2(sectionKinds)}`);
4644
+ }
4637
4645
  if (riskLines.length || removedApiWarnings.length) {
4638
4646
  parts.push("");
4639
4647
  parts.push("risk_summary:");
@@ -4777,8 +4785,7 @@ function result(structured, { large = false } = {}) {
4777
4785
  function errorResult(code, message, recovery, { large = false, extra = null } = {}) {
4778
4786
  const payload = {
4779
4787
  status: "ERROR",
4780
- error: { code, message, recovery },
4781
- summary: message
4788
+ error: { code, message, recovery }
4782
4789
  };
4783
4790
  if (extra && typeof extra === "object") {
4784
4791
  Object.assign(payload, extra);
@@ -4787,7 +4794,7 @@ function errorResult(code, message, recovery, { large = false, extra = null } =
4787
4794
  }
4788
4795
 
4789
4796
  // server.mjs
4790
- var version = true ? "1.21.1" : (await null).createRequire(import.meta.url)("./package.json").version;
4797
+ var version = true ? "1.23.0" : (await null).createRequire(import.meta.url)("./package.json").version;
4791
4798
  var STATUS_ENUM = z2.enum(["OK", "ERROR", "AUTO_REBASED", "CONFLICT", "STALE", "INVALID", "NO_CHANGES", "CHANGED", "UNSUPPORTED"]);
4792
4799
  var ERROR_SHAPE = z2.object({ code: z2.string(), message: z2.string(), recovery: z2.string() }).optional();
4793
4800
  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.21.1",
3
+ "version": "1.23.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.",