homegraph 1.1.3 → 1.2.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 (246) hide show
  1. package/README.md +43 -18
  2. package/dist/arkts/ohos-api-index.d.ts +15 -0
  3. package/dist/arkts/ohos-api-index.d.ts.map +1 -0
  4. package/dist/arkts/ohos-api-index.js +190 -0
  5. package/dist/arkts/ohos-api-index.js.map +1 -0
  6. package/dist/arkts/ohos-sdk-input.d.ts +36 -0
  7. package/dist/arkts/ohos-sdk-input.d.ts.map +1 -0
  8. package/dist/arkts/ohos-sdk-input.js +214 -0
  9. package/dist/arkts/ohos-sdk-input.js.map +1 -0
  10. package/dist/bin/command-supervision.d.ts +12 -0
  11. package/dist/bin/command-supervision.d.ts.map +1 -0
  12. package/dist/bin/command-supervision.js +86 -0
  13. package/dist/bin/command-supervision.js.map +1 -0
  14. package/dist/bin/homegraph.d.ts +1 -1
  15. package/dist/bin/homegraph.js +706 -168
  16. package/dist/bin/homegraph.js.map +1 -1
  17. package/dist/db/index.d.ts +20 -0
  18. package/dist/db/index.d.ts.map +1 -1
  19. package/dist/db/index.js +39 -0
  20. package/dist/db/index.js.map +1 -1
  21. package/dist/db/migrations.d.ts +1 -1
  22. package/dist/db/migrations.d.ts.map +1 -1
  23. package/dist/db/migrations.js +40 -1
  24. package/dist/db/migrations.js.map +1 -1
  25. package/dist/db/queries.d.ts +14 -0
  26. package/dist/db/queries.d.ts.map +1 -1
  27. package/dist/db/queries.js +174 -3
  28. package/dist/db/queries.js.map +1 -1
  29. package/dist/db/schema.sql +20 -0
  30. package/dist/directory.d.ts +32 -0
  31. package/dist/directory.d.ts.map +1 -1
  32. package/dist/directory.js +83 -0
  33. package/dist/directory.js.map +1 -1
  34. package/dist/extraction/arkts-batch-worker.d.ts +2 -0
  35. package/dist/extraction/arkts-batch-worker.d.ts.map +1 -0
  36. package/dist/extraction/arkts-batch-worker.js +28 -0
  37. package/dist/extraction/arkts-batch-worker.js.map +1 -0
  38. package/dist/extraction/context.d.ts +11 -0
  39. package/dist/extraction/context.d.ts.map +1 -1
  40. package/dist/extraction/context.js +20 -0
  41. package/dist/extraction/context.js.map +1 -1
  42. package/dist/extraction/index.d.ts +16 -2
  43. package/dist/extraction/index.d.ts.map +1 -1
  44. package/dist/extraction/index.js +619 -380
  45. package/dist/extraction/index.js.map +1 -1
  46. package/dist/extraction/languages/arkts-state-decorators.d.ts +13 -0
  47. package/dist/extraction/languages/arkts-state-decorators.d.ts.map +1 -0
  48. package/dist/extraction/languages/arkts-state-decorators.js +26 -0
  49. package/dist/extraction/languages/arkts-state-decorators.js.map +1 -0
  50. package/dist/extraction/languages/arkts-viewtree.d.ts +4 -2
  51. package/dist/extraction/languages/arkts-viewtree.d.ts.map +1 -1
  52. package/dist/extraction/languages/arkts-viewtree.js +21 -6
  53. package/dist/extraction/languages/arkts-viewtree.js.map +1 -1
  54. package/dist/extraction/languages/arkts.d.ts +98 -2
  55. package/dist/extraction/languages/arkts.d.ts.map +1 -1
  56. package/dist/extraction/languages/arkts.js +1290 -61
  57. package/dist/extraction/languages/arkts.js.map +1 -1
  58. package/dist/extraction/languages/c-cpp.d.ts +56 -0
  59. package/dist/extraction/languages/c-cpp.d.ts.map +1 -1
  60. package/dist/extraction/languages/c-cpp.js +198 -1
  61. package/dist/extraction/languages/c-cpp.js.map +1 -1
  62. package/dist/extraction/languages/ohos-api-consumer.d.ts +34 -0
  63. package/dist/extraction/languages/ohos-api-consumer.d.ts.map +1 -0
  64. package/dist/extraction/languages/ohos-api-consumer.js +283 -0
  65. package/dist/extraction/languages/ohos-api-consumer.js.map +1 -0
  66. package/dist/extraction/parse-pool.d.ts +126 -0
  67. package/dist/extraction/parse-pool.d.ts.map +1 -0
  68. package/dist/extraction/parse-pool.js +319 -0
  69. package/dist/extraction/parse-pool.js.map +1 -0
  70. package/dist/extraction/tree-sitter-types.d.ts +17 -0
  71. package/dist/extraction/tree-sitter-types.d.ts.map +1 -1
  72. package/dist/extraction/tree-sitter.d.ts +21 -0
  73. package/dist/extraction/tree-sitter.d.ts.map +1 -1
  74. package/dist/extraction/tree-sitter.js +198 -27
  75. package/dist/extraction/tree-sitter.js.map +1 -1
  76. package/dist/graph/traversal.d.ts.map +1 -1
  77. package/dist/graph/traversal.js +76 -17
  78. package/dist/graph/traversal.js.map +1 -1
  79. package/dist/index.d.ts +25 -0
  80. package/dist/index.d.ts.map +1 -1
  81. package/dist/index.js +78 -4
  82. package/dist/index.js.map +1 -1
  83. package/dist/mcp/daemon-paths.d.ts +30 -3
  84. package/dist/mcp/daemon-paths.d.ts.map +1 -1
  85. package/dist/mcp/daemon-paths.js +50 -10
  86. package/dist/mcp/daemon-paths.js.map +1 -1
  87. package/dist/mcp/daemon-registry.d.ts.map +1 -1
  88. package/dist/mcp/daemon-registry.js +7 -3
  89. package/dist/mcp/daemon-registry.js.map +1 -1
  90. package/dist/mcp/daemon.d.ts +48 -0
  91. package/dist/mcp/daemon.d.ts.map +1 -1
  92. package/dist/mcp/daemon.js +196 -32
  93. package/dist/mcp/daemon.js.map +1 -1
  94. package/dist/mcp/engine.d.ts +17 -0
  95. package/dist/mcp/engine.d.ts.map +1 -1
  96. package/dist/mcp/engine.js +73 -1
  97. package/dist/mcp/engine.js.map +1 -1
  98. package/dist/mcp/index.d.ts.map +1 -1
  99. package/dist/mcp/index.js +25 -43
  100. package/dist/mcp/index.js.map +1 -1
  101. package/dist/mcp/ppid-watchdog.d.ts +18 -0
  102. package/dist/mcp/ppid-watchdog.d.ts.map +1 -1
  103. package/dist/mcp/ppid-watchdog.js +37 -0
  104. package/dist/mcp/ppid-watchdog.js.map +1 -1
  105. package/dist/mcp/query-cache.d.ts +25 -0
  106. package/dist/mcp/query-cache.d.ts.map +1 -0
  107. package/dist/mcp/query-cache.js +191 -0
  108. package/dist/mcp/query-cache.js.map +1 -0
  109. package/dist/mcp/query-pool.d.ts +94 -0
  110. package/dist/mcp/query-pool.d.ts.map +1 -0
  111. package/dist/mcp/query-pool.js +297 -0
  112. package/dist/mcp/query-pool.js.map +1 -0
  113. package/dist/mcp/query-worker.d.ts +24 -0
  114. package/dist/mcp/query-worker.d.ts.map +1 -0
  115. package/dist/mcp/query-worker.js +87 -0
  116. package/dist/mcp/query-worker.js.map +1 -0
  117. package/dist/mcp/server-instructions.d.ts +5 -7
  118. package/dist/mcp/server-instructions.d.ts.map +1 -1
  119. package/dist/mcp/server-instructions.js +12 -14
  120. package/dist/mcp/server-instructions.js.map +1 -1
  121. package/dist/mcp/tools.d.ts +77 -0
  122. package/dist/mcp/tools.d.ts.map +1 -1
  123. package/dist/mcp/tools.js +696 -60
  124. package/dist/mcp/tools.js.map +1 -1
  125. package/dist/project-config.d.ts +20 -0
  126. package/dist/project-config.d.ts.map +1 -1
  127. package/dist/project-config.js +42 -2
  128. package/dist/project-config.js.map +1 -1
  129. package/dist/reasoning/login.js +1 -1
  130. package/dist/reasoning/login.js.map +1 -1
  131. package/dist/resolution/c-fnptr-synthesizer.d.ts +0 -28
  132. package/dist/resolution/c-fnptr-synthesizer.d.ts.map +1 -1
  133. package/dist/resolution/c-fnptr-synthesizer.js +765 -79
  134. package/dist/resolution/c-fnptr-synthesizer.js.map +1 -1
  135. package/dist/resolution/callback-synthesizer.d.ts +1 -1
  136. package/dist/resolution/callback-synthesizer.d.ts.map +1 -1
  137. package/dist/resolution/callback-synthesizer.js +72 -11
  138. package/dist/resolution/callback-synthesizer.js.map +1 -1
  139. package/dist/resolution/cooperative-yield.d.ts +32 -0
  140. package/dist/resolution/cooperative-yield.d.ts.map +1 -0
  141. package/dist/resolution/cooperative-yield.js +42 -0
  142. package/dist/resolution/cooperative-yield.js.map +1 -0
  143. package/dist/resolution/index.d.ts +11 -2
  144. package/dist/resolution/index.d.ts.map +1 -1
  145. package/dist/resolution/index.js +72 -4
  146. package/dist/resolution/index.js.map +1 -1
  147. package/dist/resolution/name-matcher.d.ts +22 -0
  148. package/dist/resolution/name-matcher.d.ts.map +1 -1
  149. package/dist/resolution/name-matcher.js +317 -20
  150. package/dist/resolution/name-matcher.js.map +1 -1
  151. package/dist/spec/config.d.ts +7 -11
  152. package/dist/spec/config.d.ts.map +1 -1
  153. package/dist/spec/config.js +77 -48
  154. package/dist/spec/config.js.map +1 -1
  155. package/dist/spec/db/fragment-node.d.ts +1 -0
  156. package/dist/spec/db/fragment-node.d.ts.map +1 -1
  157. package/dist/spec/db/fragment-node.js +8 -0
  158. package/dist/spec/db/fragment-node.js.map +1 -1
  159. package/dist/spec/db/fts.d.ts +14 -0
  160. package/dist/spec/db/fts.d.ts.map +1 -1
  161. package/dist/spec/db/fts.js +43 -4
  162. package/dist/spec/db/fts.js.map +1 -1
  163. package/dist/spec/db/schema.d.ts +6 -3
  164. package/dist/spec/db/schema.d.ts.map +1 -1
  165. package/dist/spec/db/schema.js +36 -4
  166. package/dist/spec/db/schema.js.map +1 -1
  167. package/dist/spec/db/schema.sql +11 -1
  168. package/dist/spec/evolve/logic-checker.d.ts +2 -2
  169. package/dist/spec/evolve/logic-checker.d.ts.map +1 -1
  170. package/dist/spec/evolve/logic-checker.js +3 -27
  171. package/dist/spec/evolve/logic-checker.js.map +1 -1
  172. package/dist/spec/evolve/pipeline.d.ts +21 -19
  173. package/dist/spec/evolve/pipeline.d.ts.map +1 -1
  174. package/dist/spec/evolve/pipeline.js +284 -31
  175. package/dist/spec/evolve/pipeline.js.map +1 -1
  176. package/dist/spec/evolve/spec-rewriter.d.ts +2 -2
  177. package/dist/spec/evolve/spec-rewriter.d.ts.map +1 -1
  178. package/dist/spec/evolve/spec-rewriter.js +7 -31
  179. package/dist/spec/evolve/spec-rewriter.js.map +1 -1
  180. package/dist/spec/graph/queries.d.ts +97 -0
  181. package/dist/spec/graph/queries.d.ts.map +1 -1
  182. package/dist/spec/graph/queries.js +225 -0
  183. package/dist/spec/graph/queries.js.map +1 -1
  184. package/dist/spec/llm/client.d.ts +29 -0
  185. package/dist/spec/llm/client.d.ts.map +1 -0
  186. package/dist/spec/llm/client.js +123 -0
  187. package/dist/spec/llm/client.js.map +1 -0
  188. package/dist/spec/llm/index.d.ts +3 -0
  189. package/dist/spec/llm/index.d.ts.map +1 -0
  190. package/dist/spec/llm/index.js +11 -0
  191. package/dist/spec/llm/index.js.map +1 -0
  192. package/dist/spec/llm/prompts.d.ts +13 -0
  193. package/dist/spec/llm/prompts.d.ts.map +1 -0
  194. package/dist/spec/llm/prompts.js +75 -0
  195. package/dist/spec/llm/prompts.js.map +1 -0
  196. package/dist/spec/mining/git-scanner.d.ts +12 -0
  197. package/dist/spec/mining/git-scanner.d.ts.map +1 -1
  198. package/dist/spec/mining/git-scanner.js +41 -0
  199. package/dist/spec/mining/git-scanner.js.map +1 -1
  200. package/dist/spec/mining/pipeline.d.ts.map +1 -1
  201. package/dist/spec/mining/pipeline.js +14 -1
  202. package/dist/spec/mining/pipeline.js.map +1 -1
  203. package/dist/spec/utils.d.ts +2 -1
  204. package/dist/spec/utils.d.ts.map +1 -1
  205. package/dist/spec/utils.js +3 -1
  206. package/dist/spec/utils.js.map +1 -1
  207. package/dist/sync/worktree.d.ts +9 -0
  208. package/dist/sync/worktree.d.ts.map +1 -1
  209. package/dist/sync/worktree.js +40 -0
  210. package/dist/sync/worktree.js.map +1 -1
  211. package/dist/types.d.ts +6 -1
  212. package/dist/types.d.ts.map +1 -1
  213. package/dist/ui/shimmer-progress.d.ts +2 -0
  214. package/dist/ui/shimmer-progress.d.ts.map +1 -1
  215. package/dist/ui/shimmer-progress.js +19 -2
  216. package/dist/ui/shimmer-progress.js.map +1 -1
  217. package/dist/upgrade/index.js +1 -1
  218. package/dist/upgrade/index.js.map +1 -1
  219. package/package.json +3 -3
  220. package/scripts/agent-eval/repro-concurrent-explore.mjs +119 -0
  221. package/scripts/agent-eval/repro-daemon-clients.mjs +125 -0
  222. package/scripts/exp_boundary_eval/README.md +247 -0
  223. package/scripts/exp_boundary_eval/__pycache__/_utils.cpython-310.pyc +0 -0
  224. package/scripts/exp_boundary_eval/__pycache__/analyze.cpython-310.pyc +0 -0
  225. package/scripts/exp_boundary_eval/__pycache__/deveco_arm.cpython-310.pyc +0 -0
  226. package/scripts/exp_boundary_eval/__pycache__/run_one.cpython-310.pyc +0 -0
  227. package/scripts/exp_boundary_eval/__pycache__/run_session.cpython-310.pyc +0 -0
  228. package/scripts/exp_boundary_eval/__pycache__/setup.cpython-310.pyc +0 -0
  229. package/scripts/exp_boundary_eval/_test_mcp_chain.py +78 -0
  230. package/scripts/exp_boundary_eval/_test_stdin.py +8 -0
  231. package/scripts/exp_boundary_eval/_utils.py +1116 -0
  232. package/scripts/exp_boundary_eval/analyze.py +1313 -0
  233. package/scripts/exp_boundary_eval/data/agents.json +109 -0
  234. package/scripts/exp_boundary_eval/data/experiments.json +140 -0
  235. package/scripts/exp_boundary_eval/deveco_arm.py +519 -0
  236. package/scripts/exp_boundary_eval/run_all.py +378 -0
  237. package/scripts/exp_boundary_eval/run_one.py +165 -0
  238. package/scripts/exp_boundary_eval/run_session.py +158 -0
  239. package/scripts/exp_boundary_eval/setup.py +120 -0
  240. package/scripts/exp_boundary_eval/win_mcp_launcher.py +73 -0
  241. package/scripts/exp_boundary_eval/win_mcp_stdio_wrap.js +36 -0
  242. package/scripts/exp_boundary_eval/win_node_launcher.py +24 -0
  243. package/scripts/npm-shim.js +25 -3
  244. package/scripts/ohos-sdk-publish.mjs +133 -0
  245. package/scripts/qa_eval/README.md +1 -1
  246. package/scripts/qa_eval/agent_runner.py +1 -1
@@ -20,7 +20,7 @@
20
20
  * homegraph callers <symbol> Find what calls a function/method
21
21
  * homegraph callees <symbol> Find what a function/method calls
22
22
  * homegraph impact <symbol> Analyze what code is affected by changing a symbol
23
- * homegraph affected [files] Find test files affected by changes
23
+ * homegraph index-api <tools> Build OHOS API db from command-line-tools
24
24
  * homegraph upgrade [version] Update HomeGraph to the latest release
25
25
  */
26
26
  var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
@@ -60,14 +60,15 @@ Object.defineProperty(exports, "__esModule", { value: true });
60
60
  const commander_1 = require("commander");
61
61
  const path = __importStar(require("path"));
62
62
  const fs = __importStar(require("fs"));
63
- const utils_1 = require("../spec/utils");
64
63
  const directory_1 = require("../directory");
64
+ const utils_1 = require("../spec/utils");
65
65
  const worktree_1 = require("../sync/worktree");
66
66
  const shimmer_progress_1 = require("../ui/shimmer-progress");
67
67
  const glyphs_1 = require("../ui/glyphs");
68
68
  const node_version_check_1 = require("./node-version-check");
69
69
  const fatal_handler_1 = require("./fatal-handler");
70
70
  const wasm_runtime_flags_1 = require("../extraction/wasm-runtime-flags");
71
+ const command_supervision_1 = require("./command-supervision");
71
72
  const extraction_version_1 = require("../extraction/extraction-version");
72
73
  const telemetry_1 = require("../telemetry");
73
74
  // Lazy-load heavy modules (HomeGraph, runInstaller) to keep CLI startup fast.
@@ -416,6 +417,19 @@ function main() {
416
417
  fs.unlinkSync(logPath);
417
418
  }
418
419
  }
420
+ const ohosApiWarnings = result.errors.filter((e) => e.severity === 'warning' && e.code?.startsWith('ohos_api_'));
421
+ if (ohosApiWarnings.length > 0) {
422
+ clack.log.warn('OHOS SDK API db not attached — project index is complete; explore uses project code only.');
423
+ clack.note(ohosApiWarnings.map((e) => e.message).join('\n'), 'OHOS API notice');
424
+ }
425
+ const arktsDegraded = result.errors.filter((e) => e.code === 'arkts_degraded');
426
+ if (arktsDegraded.length > 0) {
427
+ clack.log.warn('ArkTS index quality notice — see details below.');
428
+ clack.note(`${arktsDegraded[0].message}\n\n` +
429
+ 'Large ArkTS repos on Windows use an isolated indexer with enableMethodBodyBuild=true.\n' +
430
+ 'If indexing failed, set HOMEGRAPH_ARKTS_STACK_SIZES_KB=65536,131072,262144 and retry:\n' +
431
+ ' node --stack-size=65536 dist/bin/homegraph.js index <path>', 'ArkTS quality notice');
432
+ }
419
433
  }
420
434
  /**
421
435
  * Write detailed error log to .homegraph/errors.log
@@ -512,20 +526,27 @@ function main() {
512
526
  // Indexing runs by default now. The legacy -i/--index flag is still
513
527
  // accepted (so existing muscle memory and scripts don't break) but is a
514
528
  // no-op — initializing always builds the initial index.
529
+ // Supervise the index: self-terminate if orphaned or wedged (#999).
530
+ const supervision = (0, command_supervision_1.installCommandSupervision)('init');
515
531
  let result;
516
- if (options.verbose) {
517
- result = await cg.indexAll({
518
- onProgress: createVerboseProgress(),
519
- verbose: true,
520
- });
532
+ try {
533
+ if (options.verbose) {
534
+ result = await cg.indexAll({
535
+ onProgress: createVerboseProgress(),
536
+ verbose: true,
537
+ });
538
+ }
539
+ else {
540
+ process.stdout.write(`${colors.dim}${(0, glyphs_1.getGlyphs)().rail}${colors.reset}\n`);
541
+ const progress = (0, shimmer_progress_1.createShimmerProgress)();
542
+ result = await cg.indexAll({
543
+ onProgress: progress.onProgress,
544
+ });
545
+ await progress.stop();
546
+ }
521
547
  }
522
- else {
523
- process.stdout.write(`${colors.dim}${(0, glyphs_1.getGlyphs)().rail}${colors.reset}\n`);
524
- const progress = (0, shimmer_progress_1.createShimmerProgress)();
525
- result = await cg.indexAll({
526
- onProgress: progress.onProgress,
527
- });
528
- await progress.stop();
548
+ finally {
549
+ supervision.stop();
529
550
  }
530
551
  printIndexResult(clack, result, projectPath);
531
552
  await recordIndexTelemetry(cg, result);
@@ -620,47 +641,56 @@ function main() {
620
641
  process.exit(1);
621
642
  }
622
643
  const { default: HomeGraph } = await loadHomeGraph();
623
- const cg = await HomeGraph.open(projectPath);
624
- if (options.quiet) {
625
- // Quiet mode: no UI, just run. `index` is a full re-index, so clear the
626
- // existing graph and rebuild from scratch (see the note below — #874).
627
- cg.clear();
628
- const result = await cg.indexAll();
629
- if (!result.success)
644
+ // `index` is a FULL re-index — identical to a fresh `init`. RECREATE the
645
+ // database from scratch (discard .homegraph/homegraph.db + its WAL) rather
646
+ // than opening the old graph and DELETE-ing every row. The clear-then-index
647
+ // approach reported "0 nodes" without the clear (#874); the recreate keeps
648
+ // that fixed AND avoids the failure mode where, on a large or pre-fix
649
+ // poisoned index, the per-row FTS delete churn wedged the main thread long
650
+ // enough to trip the liveness watchdog before scanning even began (#1067).
651
+ // recreate() hands back a fresh, empty instance — no clear() needed. For
652
+ // fast incremental updates use `sync`.
653
+ const cg = await HomeGraph.recreate(projectPath);
654
+ // Supervise the indexer: self-terminate if orphaned (parent shim killed)
655
+ // or if the main thread wedges — neither was guarded on this path (#999).
656
+ const supervision = (0, command_supervision_1.installCommandSupervision)('index');
657
+ try {
658
+ if (options.quiet) {
659
+ // Quiet mode: no UI, just run against the freshly-recreated graph.
660
+ const result = await cg.indexAll();
661
+ if (!result.success)
662
+ process.exit(1);
663
+ cg.destroy();
664
+ return;
665
+ }
666
+ const clack = await importESM('@clack/prompts');
667
+ clack.intro('Indexing project');
668
+ let result;
669
+ if (options.verbose) {
670
+ result = await cg.indexAll({
671
+ onProgress: createVerboseProgress(),
672
+ verbose: true,
673
+ });
674
+ }
675
+ else {
676
+ process.stdout.write(`${colors.dim}${(0, glyphs_1.getGlyphs)().rail}${colors.reset}\n`);
677
+ const progress = (0, shimmer_progress_1.createShimmerProgress)();
678
+ result = await cg.indexAll({
679
+ onProgress: progress.onProgress,
680
+ });
681
+ await progress.stop();
682
+ }
683
+ printIndexResult(clack, result, projectPath);
684
+ await recordIndexTelemetry(cg, result);
685
+ if (!result.success) {
630
686
  process.exit(1);
687
+ }
688
+ clack.outro('Done');
631
689
  cg.destroy();
632
- return;
633
690
  }
634
- const clack = await importESM('@clack/prompts');
635
- clack.intro('Indexing project');
636
- // `index` is a FULL re-index: clear the existing graph and rebuild it from
637
- // scratch so the result is identical to a fresh `init`. Without the clear,
638
- // indexAll() skips every unchanged file by its content hash and reports
639
- // "0 nodes, 0 edges" against the already-populated graph — which reads as
640
- // "index wiped my index" (#874). For fast incremental updates use `sync`.
641
- cg.clear();
642
- let result;
643
- if (options.verbose) {
644
- result = await cg.indexAll({
645
- onProgress: createVerboseProgress(),
646
- verbose: true,
647
- });
691
+ finally {
692
+ supervision.stop();
648
693
  }
649
- else {
650
- process.stdout.write(`${colors.dim}${(0, glyphs_1.getGlyphs)().rail}${colors.reset}\n`);
651
- const progress = (0, shimmer_progress_1.createShimmerProgress)();
652
- result = await cg.indexAll({
653
- onProgress: progress.onProgress,
654
- });
655
- await progress.stop();
656
- }
657
- printIndexResult(clack, result, projectPath);
658
- await recordIndexTelemetry(cg, result);
659
- if (!result.success) {
660
- process.exit(1);
661
- }
662
- clack.outro('Done');
663
- cg.destroy();
664
694
  }
665
695
  catch (err) {
666
696
  error(`Failed to index: ${err instanceof Error ? err.message : String(err)}`);
@@ -763,6 +793,7 @@ function main() {
763
793
  const journalMode = cg.getJournalMode();
764
794
  const buildInfo = cg.getIndexBuildInfo();
765
795
  const reindexRecommended = cg.isIndexStale();
796
+ const ohosApi = cg.getOhosApiBinding();
766
797
  // JSON output mode
767
798
  if (options.json) {
768
799
  const lastIndexedMs = cg.getLastIndexedAt();
@@ -788,6 +819,9 @@ function main() {
788
819
  worktreeMismatch: worktreeMismatch
789
820
  ? { worktreeRoot: worktreeMismatch.worktreeRoot, indexRoot: worktreeMismatch.indexRoot }
790
821
  : null,
822
+ ohosApi: ohosApi
823
+ ? { version: ohosApi.version, packageName: ohosApi.packageName, dbPath: ohosApi.dbPath }
824
+ : null,
791
825
  index: {
792
826
  builtWithVersion: buildInfo.version,
793
827
  builtWithExtractionVersion: buildInfo.extractionVersion,
@@ -842,6 +876,13 @@ function main() {
842
876
  console.log(` ${lang.padEnd(15)} ${formatNumber(count)}`);
843
877
  }
844
878
  console.log();
879
+ if (ohosApi) {
880
+ console.log(chalk.bold('OHOS API Database:'));
881
+ console.log(` Version: ${ohosApi.version}`);
882
+ console.log(` Package: ${ohosApi.packageName}`);
883
+ console.log(` DB: ${ohosApi.dbPath}`);
884
+ console.log();
885
+ }
845
886
  // Pending changes
846
887
  const totalChanges = changes.added.length + changes.modified.length + changes.removed.length;
847
888
  if (totalChanges > 0) {
@@ -918,13 +959,16 @@ function main() {
918
959
  }
919
960
  else {
920
961
  console.log(chalk.bold(`\nSearch Results for "${search}":\n`));
962
+ // Results arrive already ranked by relevance, so the order conveys
963
+ // it. We don't print the raw score: it's an unbounded BM25/FTS value
964
+ // (relative-ranking only), and the old `(score * 100)%` rendered it
965
+ // as nonsensical percentages like "12042%" (#1045). The MCP search
966
+ // tool likewise shows no score. Raw `score` stays in --json output.
921
967
  for (const result of results) {
922
968
  const node = result.node;
923
969
  const location = `${node.filePath}:${node.startLine}`;
924
- const score = chalk.dim(`(${(result.score * 100).toFixed(0)}%)`);
925
970
  console.log(chalk.cyan(node.kind.padEnd(12)) +
926
- chalk.white(node.name) +
927
- ' ' + score);
971
+ chalk.white(node.name));
928
972
  console.log(chalk.dim(` ${location}`));
929
973
  if (node.signature) {
930
974
  console.log(chalk.dim(` ${node.signature}`));
@@ -1019,11 +1063,15 @@ function main() {
1019
1063
  return;
1020
1064
  }
1021
1065
  const prompt = String(input.prompt || '');
1022
- // Gate: only structural / flow / impact / where-how prompts get context.
1023
- // A cheap regex keeps every other prompt ("fix this typo") a zero-cost
1024
- // no-op so we never add latency where there's no structural answer to give.
1025
- const STRUCTURAL = /\b(how|where|trace|flow|path|reach(?:es|ed)?|call(?:s|ed|er|ers|ee)?|depend|impact|affect|wired?|connect|implement|architect|structure|breaks?|what calls|why does)\b/i;
1026
- if (!prompt || !STRUCTURAL.test(prompt))
1066
+ // Gate: only structural / flow / impact / where-how prompts get context, so
1067
+ // every other prompt ("fix this typo") stays a zero-cost no-op. Language-aware
1068
+ // (English + CJK keywords, plus code-shaped tokens) so it fires for non-English
1069
+ // prompts too (issue #994). A keyword fires on its own; a code-token is only a
1070
+ // CANDIDATE verified against the graph below, so a tech brand ("JavaScript")
1071
+ // that looks like a symbol but isn't one here doesn't inject spurious context.
1072
+ const keyworded = (0, directory_1.hasStructuralKeyword)(prompt);
1073
+ const codeTokens = keyworded ? [] : (0, directory_1.extractCodeTokens)(prompt);
1074
+ if (!keyworded && codeTokens.length === 0)
1027
1075
  return;
1028
1076
  // Decide what to inject, shaped by WHERE the index(es) are: the nearest
1029
1077
  // indexed ancestor of cwd, or — when cwd is an un-indexed workspace root
@@ -1043,6 +1091,13 @@ function main() {
1043
1091
  const { default: HomeGraph } = await loadHomeGraph();
1044
1092
  const cg = await HomeGraph.open(plan.exploreRoot);
1045
1093
  try {
1094
+ // Code-token-only prompt: require that at least one token is a REAL symbol
1095
+ // in THIS index before front-loading. Without it, a brand name or common
1096
+ // word that merely looks like code ("JavaScript", "GitHub") would run
1097
+ // explore and inject ~16KB of low-relevance context (issue #994 follow-up).
1098
+ // A keyword-bearing prompt skips this — the keyword is signal enough.
1099
+ if (!keyworded && !codeTokens.some((t) => cg.getNodesByName(t).length > 0))
1100
+ return;
1046
1101
  const { ToolHandler } = await Promise.resolve().then(() => __importStar(require('../mcp/tools')));
1047
1102
  const handler = new ToolHandler(cg);
1048
1103
  const result = await handler.execute('homegraph_explore', { query: prompt });
@@ -1078,14 +1133,17 @@ function main() {
1078
1133
  }
1079
1134
  });
1080
1135
  /**
1081
- * homegraph node <name>
1136
+ * homegraph node [name]
1082
1137
  *
1083
1138
  * The CLI face of the MCP homegraph_node tool: one symbol's source +
1084
1139
  * caller/callee trail, or a whole file with line numbers + dependents
1085
1140
  * (Read-parity). Same subagent/non-MCP rationale as `explore`.
1141
+ *
1142
+ * `name` is OPTIONAL because `--file` (file-read mode) carries no symbol —
1143
+ * a required `<name>` made `codegraph node -f <file>` unreachable (#1044).
1086
1144
  */
1087
1145
  program
1088
- .command('node <name>')
1146
+ .command('node [name]')
1089
1147
  .description('One symbol\'s source + caller/callee trail, or read a file with line numbers + dependents (same output as the homegraph_node MCP tool)')
1090
1148
  .option('-p, --path <path>', 'Project path')
1091
1149
  .option('-f, --file <file>', 'Treat as file mode (or disambiguate a symbol to this file)')
@@ -1093,6 +1151,13 @@ function main() {
1093
1151
  .option('--limit <number>', 'File mode: maximum lines')
1094
1152
  .option('--symbols-only', 'File mode: just the symbol map + dependents')
1095
1153
  .action(async (name, options) => {
1154
+ // Need a symbol (positional) OR a file (--file / a path-like positional).
1155
+ // With [name] optional, a bare `codegraph node` reaches here with neither
1156
+ // and must be told what to pass, rather than crashing downstream.
1157
+ if (!name && !options.file) {
1158
+ error("Pass a symbol name (e.g. 'codegraph node parseToken') or a file (e.g. 'codegraph node -f src/auth.ts', or 'codegraph node src/auth.ts').");
1159
+ process.exit(1);
1160
+ }
1096
1161
  const projectPath = resolveProjectPath(options.path);
1097
1162
  try {
1098
1163
  if (!(0, directory_1.isInitialized)(projectPath)) {
@@ -1114,10 +1179,10 @@ function main() {
1114
1179
  if (name && name !== options.file)
1115
1180
  args.symbol = name;
1116
1181
  }
1117
- else if (name.includes('/') || name.includes('\\')) {
1182
+ else if (name && (name.includes('/') || name.includes('\\'))) {
1118
1183
  args.file = name.replace(/\\/g, '/');
1119
1184
  }
1120
- else {
1185
+ else if (name) {
1121
1186
  args.symbol = name;
1122
1187
  args.includeCode = true;
1123
1188
  }
@@ -1850,6 +1915,92 @@ function main() {
1850
1915
  process.exit(1);
1851
1916
  }
1852
1917
  });
1918
+ /**
1919
+ * homegraph install
1920
+ */
1921
+ program
1922
+ .command('install')
1923
+ .description('Install homegraph MCP server into one or more agents (Claude Code, Cursor, Codex CLI, opencode, DevEco Code, CodeBuddy, Hermes Agent)')
1924
+ .option('-t, --target <ids>', 'Target agent(s): comma-separated ids, or "auto"|"all"|"none". Default: prompt')
1925
+ .option('-l, --location <where>', 'Install location: "global" or "local". Default: prompt')
1926
+ .option('-y, --yes', 'Non-interactive: defaults to --location=global --target=auto, auto-allow on')
1927
+ .option('--no-permissions', 'Skip writing the auto-allow permissions list (Claude Code only)')
1928
+ .option('--print-config <id>', 'Print MCP config snippet for the named agent and exit (no file writes)')
1929
+ .action(async (opts) => {
1930
+ if (opts.printConfig) {
1931
+ const { getTarget, listTargetIds } = await Promise.resolve().then(() => __importStar(require('../installer/targets/registry')));
1932
+ const target = getTarget(opts.printConfig);
1933
+ if (!target) {
1934
+ const known = listTargetIds().join(', ');
1935
+ error(`Unknown target "${opts.printConfig}". Known: ${known}.`);
1936
+ process.exit(1);
1937
+ }
1938
+ const loc = (opts.location === 'local' ? 'local' : 'global');
1939
+ process.stdout.write(target.printConfig(loc));
1940
+ return;
1941
+ }
1942
+ const { runInstallerWithOptions } = await Promise.resolve().then(() => __importStar(require('../installer')));
1943
+ if (opts.location && opts.location !== 'global' && opts.location !== 'local') {
1944
+ error(`--location must be "global" or "local" (got "${opts.location}").`);
1945
+ process.exit(1);
1946
+ }
1947
+ try {
1948
+ // Commander's `--no-permissions` makes `opts.permissions === false`;
1949
+ // omitting the flag leaves it `true` (the positive-form default).
1950
+ // We MUST treat the default-true as "user did not override — let
1951
+ // the orchestrator prompt" and only forward an explicit `false`
1952
+ // (or `true` when --yes implies it). Otherwise the auto-allow
1953
+ // prompt is silently skipped on every interactive run.
1954
+ const explicitNoPermissions = opts.permissions === false;
1955
+ const autoAllow = explicitNoPermissions
1956
+ ? false
1957
+ : opts.yes
1958
+ ? true
1959
+ : undefined;
1960
+ await runInstallerWithOptions({
1961
+ target: opts.target,
1962
+ location: opts.location,
1963
+ autoAllow,
1964
+ yes: opts.yes,
1965
+ });
1966
+ }
1967
+ catch (err) {
1968
+ error(err instanceof Error ? err.message : String(err));
1969
+ process.exit(1);
1970
+ }
1971
+ });
1972
+ /**
1973
+ * homegraph uninstall
1974
+ *
1975
+ * Inverse of `install`. Removes the homegraph MCP server entry,
1976
+ * instructions block, and permissions from every agent (or a
1977
+ * `--target` subset). Prompts global-vs-local when not given. Does NOT
1978
+ * delete the `.homegraph/` index — that's `homegraph uninit`.
1979
+ */
1980
+ program
1981
+ .command('uninstall')
1982
+ .description('Remove homegraph from your agents (Claude Code, Cursor, Codex CLI, opencode, DevEco Code, CodeBuddy, Hermes Agent)')
1983
+ .option('-t, --target <ids>', 'Target agent(s): comma-separated ids, or "all". Default: all')
1984
+ .option('-l, --location <where>', 'Uninstall location: "global" or "local". Default: prompt')
1985
+ .option('-y, --yes', 'Non-interactive: defaults to --location=global --target=all')
1986
+ .action(async (opts) => {
1987
+ const { runUninstaller } = await Promise.resolve().then(() => __importStar(require('../installer')));
1988
+ if (opts.location && opts.location !== 'global' && opts.location !== 'local') {
1989
+ error(`--location must be "global" or "local" (got "${opts.location}").`);
1990
+ process.exit(1);
1991
+ }
1992
+ try {
1993
+ await runUninstaller({
1994
+ target: opts.target,
1995
+ location: opts.location,
1996
+ yes: opts.yes,
1997
+ });
1998
+ }
1999
+ catch (err) {
2000
+ error(err instanceof Error ? err.message : String(err));
2001
+ process.exit(1);
2002
+ }
2003
+ });
1853
2004
  // =============================================================================
1854
2005
  // Spec commands (Commit4Spec knowledge graph)
1855
2006
  // =============================================================================
@@ -1926,7 +2077,6 @@ function main() {
1926
2077
  const repoPath = resolveSpecProjectPath(options.path);
1927
2078
  const { createDatabase } = await Promise.resolve().then(() => __importStar(require('../db/sqlite-adapter')));
1928
2079
  const { resolveDbPath, computeBudgetProfile } = await Promise.resolve().then(() => __importStar(require('../spec/utils')));
1929
- const { initSpecSchema } = await Promise.resolve().then(() => __importStar(require('../spec/db/schema')));
1930
2080
  const { searchAndGetContext } = await Promise.resolve().then(() => __importStar(require('../spec/graph/queries')));
1931
2081
  const dbPath = resolveDbPath(repoPath, options.dbPath);
1932
2082
  if (!fs.existsSync(dbPath)) {
@@ -1935,7 +2085,6 @@ function main() {
1935
2085
  }
1936
2086
  const created = createDatabase(dbPath);
1937
2087
  db = created.db;
1938
- initSpecSchema(db);
1939
2088
  const topK = Math.max(1, Math.min(parseInt(options.topK || '5', 10) || 5, 50));
1940
2089
  const includeFragments = options.fragments !== false;
1941
2090
  const contexts = searchAndGetContext(db, text, topK, includeFragments);
@@ -1996,6 +2145,298 @@ function main() {
1996
2145
  catch { /* best effort */ }
1997
2146
  }
1998
2147
  });
2148
+ /**
2149
+ * homegraph spec find <filePath>
2150
+ */
2151
+ specCommand
2152
+ .command('find <filePath>')
2153
+ .description('Find specs related to the given file path via code-fragment matching')
2154
+ .option('-p, --path <path>', 'Path to the repository')
2155
+ .option('--db-path <path>', 'Path to the SQLite database file')
2156
+ .option('-j, --json', 'Output as JSON')
2157
+ .action(async (filePath, options) => {
2158
+ let db;
2159
+ try {
2160
+ const repoPath = resolveSpecProjectPath(options.path);
2161
+ const { createDatabase } = await Promise.resolve().then(() => __importStar(require('../db/sqlite-adapter')));
2162
+ const { resolveDbPath } = await Promise.resolve().then(() => __importStar(require('../spec/utils')));
2163
+ const { findSpecsByFilePath } = await Promise.resolve().then(() => __importStar(require('../spec/graph/queries')));
2164
+ const dbPath = resolveDbPath(repoPath, options.dbPath);
2165
+ if (!fs.existsSync(dbPath)) {
2166
+ error(`Database not found at ${dbPath}. Run 'homegraph spec mine' first.`);
2167
+ process.exit(1);
2168
+ }
2169
+ const created = createDatabase(dbPath);
2170
+ db = created.db;
2171
+ const result = findSpecsByFilePath(db, filePath);
2172
+ if (options.json) {
2173
+ console.log(JSON.stringify({
2174
+ filePath,
2175
+ matched_count: result.matched_count,
2176
+ truncated: result.truncated,
2177
+ results: result.results,
2178
+ }, null, 2));
2179
+ }
2180
+ else {
2181
+ console.log(chalk.bold(`\n${result.matched_count} spec${result.matched_count !== 1 ? 's' : ''} matched for ${filePath}:\n`));
2182
+ for (const r of result.results) {
2183
+ console.log(` ${chalk.cyan(r.id.padEnd(16))} ${r.title.padEnd(32)} ${chalk.green(r.status.padEnd(12))} v${r.version} ${chalk.dim(r.filePath)}`);
2184
+ }
2185
+ console.log();
2186
+ if (result.truncated) {
2187
+ console.log(chalk.yellow(` ... and more (showing first ${result.matched_count} of >${result.matched_count} results)`));
2188
+ console.log();
2189
+ }
2190
+ if (result.matched_count === 0) {
2191
+ info(`No specs found for file path "${filePath}". Try a partial path (e.g. "src/auth" instead of "src/auth/login.ts").`);
2192
+ }
2193
+ }
2194
+ }
2195
+ catch (err) {
2196
+ error(`Find failed: ${err instanceof Error ? err.message : String(err)}`);
2197
+ process.exit(1);
2198
+ }
2199
+ finally {
2200
+ try {
2201
+ db?.close();
2202
+ }
2203
+ catch { /* best effort */ }
2204
+ }
2205
+ });
2206
+ /**
2207
+ * homegraph spec trace <symbol>
2208
+ */
2209
+ specCommand
2210
+ .command('trace <symbol>')
2211
+ .description('Trace a code symbol back to its associated design Specs')
2212
+ .option('-f, --file <path>', 'File path for symbol disambiguation')
2213
+ .option('-l, --line <number>', 'Line number for symbol disambiguation')
2214
+ .option('-p, --path <path>', 'Path to the repository')
2215
+ .option('--db-path <path>', 'Path to the SQLite database file')
2216
+ .option('--top-k <number>', 'Number of results to return', '10')
2217
+ .option('-j, --json', 'Output as JSON')
2218
+ .action(async (symbol, options) => {
2219
+ let db;
2220
+ let cg;
2221
+ try {
2222
+ const repoPath = resolveSpecProjectPath(options.path);
2223
+ // isNaN check so --line 0 is handled correctly (parseInt('0') → 0 is falsy)
2224
+ const lineRaw = options.line !== undefined ? parseInt(options.line, 10) : NaN;
2225
+ const line = isNaN(lineRaw) ? undefined : lineRaw;
2226
+ // Open HomeGraph code index
2227
+ const { default: HomeGraph } = await Promise.resolve().then(() => __importStar(require('../index')));
2228
+ cg = await HomeGraph.open(repoPath);
2229
+ // Inline symbol resolution (mirrors findSymbolMatches in tools.ts)
2230
+ const isQualified = /[.\/]|::/.test(symbol);
2231
+ const { isGeneratedFile } = await Promise.resolve().then(() => __importStar(require('../extraction/generated-detection')));
2232
+ let nodes = [];
2233
+ if (!isQualified) {
2234
+ const exact = cg.getNodesByName(symbol);
2235
+ if (exact.length > 0) {
2236
+ nodes = [...exact].sort((a, b) => (isGeneratedFile(a.filePath) ? 1 : 0) - (isGeneratedFile(b.filePath) ? 1 : 0));
2237
+ }
2238
+ else {
2239
+ const fuzzy = cg.searchNodes(symbol, { limit: 10 });
2240
+ if (fuzzy[0])
2241
+ nodes = [fuzzy[0].node];
2242
+ }
2243
+ }
2244
+ else {
2245
+ let results = cg.searchNodes(symbol, { limit: 50 });
2246
+ if (results.length === 0) {
2247
+ const parts = symbol.split(/::|[./]/).filter((p) => p.length > 0);
2248
+ const tail = parts[parts.length - 1];
2249
+ if (tail && tail !== symbol)
2250
+ results = cg.searchNodes(tail, { limit: 50 });
2251
+ }
2252
+ // Rough matchesSymbol equivalent for CLI
2253
+ const exactMatches = results.filter((r) => {
2254
+ const n = r.node;
2255
+ if (n.name === symbol)
2256
+ return true;
2257
+ const fileBase = n.filePath.split('/').pop()?.replace(/\.[^.]+$/, '');
2258
+ if (fileBase === symbol)
2259
+ return true;
2260
+ const parts = symbol.split(/::|[./]/);
2261
+ if (parts[parts.length - 1] === n.name) {
2262
+ const firstPart = parts[0];
2263
+ if (n.qualifiedName.includes(firstPart))
2264
+ return true;
2265
+ if (n.filePath.includes(firstPart))
2266
+ return true;
2267
+ return parts.length === 1;
2268
+ }
2269
+ return false;
2270
+ });
2271
+ if (exactMatches.length > 0) {
2272
+ nodes = exactMatches
2273
+ .sort((a, b) => (isGeneratedFile(a.node.filePath) ? 1 : 0) - (isGeneratedFile(b.node.filePath) ? 1 : 0))
2274
+ .map((r) => r.node);
2275
+ }
2276
+ }
2277
+ if (nodes.length === 0) {
2278
+ console.log(chalk.yellow(`No code entities found for symbol "${symbol}".`));
2279
+ return;
2280
+ }
2281
+ // Disambiguate
2282
+ if (options.file) {
2283
+ // Use endsWith for precise file name matching (not substring matching)
2284
+ const filePattern = options.file;
2285
+ nodes = nodes.filter((n) => n.filePath.endsWith(filePattern));
2286
+ }
2287
+ if (line !== undefined && nodes.length > 1) {
2288
+ const closest = nodes.reduce((best, n) => {
2289
+ const bestDist = Math.abs(best.startLine - line) + Math.abs(best.endLine - line);
2290
+ const curDist = Math.abs(n.startLine - line) + Math.abs(n.endLine - line);
2291
+ return curDist < bestDist ? n : best;
2292
+ });
2293
+ nodes = [closest];
2294
+ }
2295
+ const node = nodes[0];
2296
+ if (!node) {
2297
+ console.log(chalk.yellow(`Could not resolve symbol "${symbol}" to a specific code entity.`));
2298
+ return;
2299
+ }
2300
+ // Open Spec database
2301
+ const { createDatabase } = await Promise.resolve().then(() => __importStar(require('../db/sqlite-adapter')));
2302
+ const { resolveDbPath } = await Promise.resolve().then(() => __importStar(require('../spec/utils')));
2303
+ const { findSpecsByCodeSymbol } = await Promise.resolve().then(() => __importStar(require('../spec/graph/queries')));
2304
+ const { initSpecSchema, runSpecMigrations, getCurrentSpecVersion, CURRENT_SPEC_SCHEMA_VERSION } = await Promise.resolve().then(() => __importStar(require('../spec/db/schema')));
2305
+ const dbPath = resolveDbPath(repoPath, options.dbPath);
2306
+ if (!fs.existsSync(dbPath)) {
2307
+ console.log(chalk.yellow(`Database not found at ${dbPath}. Run 'homegraph spec mine' first.`));
2308
+ return;
2309
+ }
2310
+ const created = createDatabase(dbPath);
2311
+ db = created.db;
2312
+ initSpecSchema(db);
2313
+ const currentVersion = getCurrentSpecVersion(db);
2314
+ if (currentVersion < CURRENT_SPEC_SCHEMA_VERSION) {
2315
+ runSpecMigrations(db, currentVersion);
2316
+ }
2317
+ const topKRaw = parseInt(options.topK || '10', 10);
2318
+ const topK = Math.max(1, Math.min(isNaN(topKRaw) ? 10 : topKRaw, 50));
2319
+ console.log(chalk.dim(`Resolved: ${node.name} (${node.kind}) in ${node.filePath}:${node.startLine}-${node.endLine}`));
2320
+ const result = findSpecsByCodeSymbol(db, {
2321
+ name: node.name,
2322
+ qualifiedName: node.qualifiedName,
2323
+ kind: node.kind,
2324
+ filePath: node.filePath,
2325
+ startLine: node.startLine,
2326
+ endLine: node.endLine,
2327
+ }, topK);
2328
+ if (options.json) {
2329
+ console.log(JSON.stringify({
2330
+ symbol,
2331
+ entity: {
2332
+ name: node.name,
2333
+ qualifiedName: node.qualifiedName,
2334
+ kind: node.kind,
2335
+ filePath: node.filePath,
2336
+ startLine: node.startLine,
2337
+ endLine: node.endLine,
2338
+ },
2339
+ matched_count: result.matches.length,
2340
+ total_candidates: result.totalCandidates,
2341
+ matches: result.matches.map((m) => ({
2342
+ spec_id: m.spec.id,
2343
+ title: m.spec.title,
2344
+ status: m.spec.status,
2345
+ version: m.spec.version,
2346
+ file_path: m.spec.filePath,
2347
+ score: Math.round(m.score * 1000) / 1000,
2348
+ score_detail: {
2349
+ file_path: Math.round(m.scoreDetail.filePathScore * 1000) / 1000,
2350
+ content: Math.round(m.scoreDetail.contentScore * 1000) / 1000,
2351
+ name: Math.round(m.scoreDetail.nameScore * 1000) / 1000,
2352
+ recency: Math.round(m.scoreDetail.recencyScore * 1000) / 1000,
2353
+ line_overlap: Math.round(m.scoreDetail.overlapScore * 1000) / 1000,
2354
+ },
2355
+ fragment_count: m.fragmentCount,
2356
+ commit_count: m.commitCount,
2357
+ })),
2358
+ }, null, 2));
2359
+ }
2360
+ else {
2361
+ console.log(chalk.bold(`\n${result.matches.length} spec${result.matches.length !== 1 ? 's' : ''} matched (${result.totalCandidates} candidates):\n`));
2362
+ for (const m of result.matches) {
2363
+ const scoreColor = m.score >= 0.7 ? chalk.green : m.score >= 0.4 ? chalk.yellow : chalk.red;
2364
+ console.log(` ${chalk.cyan(m.spec.id.padEnd(14))} ${m.spec.title.padEnd(36)} ${scoreColor((m.score * 100).toFixed(1) + '%')}`);
2365
+ console.log(chalk.dim(` fp=${m.scoreDetail.filePathScore.toFixed(2)} content=${m.scoreDetail.contentScore.toFixed(2)} name=${m.scoreDetail.nameScore.toFixed(2)} recency=${m.scoreDetail.recencyScore.toFixed(2)} overlap=${m.scoreDetail.overlapScore.toFixed(2)}`));
2366
+ console.log();
2367
+ }
2368
+ if (result.matches.length === 0) {
2369
+ info(`No Specs found for symbol "${symbol}". Try a more specific file path or check that specs have been mined for this project.`);
2370
+ }
2371
+ }
2372
+ }
2373
+ catch (err) {
2374
+ error(`Trace failed: ${err instanceof Error ? err.message : String(err)}`);
2375
+ process.exit(1);
2376
+ }
2377
+ finally {
2378
+ try {
2379
+ await cg?.close();
2380
+ }
2381
+ catch { /* best effort */ }
2382
+ try {
2383
+ db?.close();
2384
+ }
2385
+ catch { /* best effort */ }
2386
+ }
2387
+ });
2388
+ /**
2389
+ * homegraph spec stats
2390
+ */
2391
+ specCommand
2392
+ .command('stats')
2393
+ .description('Show statistics about the spec knowledge graph')
2394
+ .option('-p, --path <path>', 'Path to the repository')
2395
+ .option('--db-path <path>', 'Path to the SQLite database file')
2396
+ .option('-j, --json', 'Output as JSON')
2397
+ .action(async (options) => {
2398
+ let db;
2399
+ try {
2400
+ const repoPath = resolveSpecProjectPath(options.path);
2401
+ const { createDatabase } = await Promise.resolve().then(() => __importStar(require('../db/sqlite-adapter')));
2402
+ const { resolveDbPath } = await Promise.resolve().then(() => __importStar(require('../spec/utils')));
2403
+ const { getSpecStats } = await Promise.resolve().then(() => __importStar(require('../spec/graph/queries')));
2404
+ const dbPath = resolveDbPath(repoPath, options.dbPath);
2405
+ if (!fs.existsSync(dbPath)) {
2406
+ error(`Database not found at ${dbPath}. Run 'homegraph spec mine' first.`);
2407
+ process.exit(1);
2408
+ }
2409
+ const created = createDatabase(dbPath);
2410
+ db = created.db;
2411
+ const stats = getSpecStats(db);
2412
+ if (options.json) {
2413
+ console.log(JSON.stringify(stats, null, 2));
2414
+ }
2415
+ else {
2416
+ console.log(chalk.bold('\nSpec Knowledge Graph Stats\n'));
2417
+ console.log(` Specs: ${chalk.cyan(String(stats.specCount))}`);
2418
+ console.log(` Active: ${chalk.green(String(stats.activeSpecCount))}`);
2419
+ console.log(` Deprecated: ${chalk.yellow(String(stats.deprecatedSpecCount))}`);
2420
+ console.log(` Commits: ${stats.commitCount}`);
2421
+ console.log(` Fragments: ${stats.fragmentCount}`);
2422
+ console.log(` Relations: ${stats.relationCount}`);
2423
+ console.log();
2424
+ if (stats.specCount === 0) {
2425
+ info("No specs yet. Run 'homegraph spec mine' to build the knowledge graph.");
2426
+ }
2427
+ }
2428
+ }
2429
+ catch (err) {
2430
+ error(`Stats failed: ${err instanceof Error ? err.message : String(err)}`);
2431
+ process.exit(1);
2432
+ }
2433
+ finally {
2434
+ try {
2435
+ db?.close();
2436
+ }
2437
+ catch { /* best effort */ }
2438
+ }
2439
+ });
1999
2440
  /**
2000
2441
  * homegraph spec evolve
2001
2442
  */
@@ -2069,8 +2510,12 @@ function main() {
2069
2510
  '# Triggers spec self-evolution after each commit. Runs in background.',
2070
2511
  '# Installed by: homegraph spec evolve install',
2071
2512
  `# Logs: ${utils_1.SPEC_DATA_DIR}/logs/evolve-hook.log`,
2072
- `"${homegraphBin}" spec evolve process --path "$(pwd)" --json \\`,
2073
- ` >> ${utils_1.SPEC_DATA_DIR}/logs/evolve-hook.log 2>&1 &`,
2513
+ '# Runtime guard: skip if homegraph is not available',
2514
+ `HOMEGRAPH_BIN="${homegraphBin}"`,
2515
+ 'if [ -x "$HOMEGRAPH_BIN" ] || command -v homegraph >/dev/null 2>&1; then',
2516
+ ' "${HOMEGRAPH_BIN:-homegraph}" spec evolve process --path "$(pwd)" --json \\',
2517
+ ` >> ${utils_1.SPEC_DATA_DIR}/logs/evolve-hook.log 2>&1 &`,
2518
+ 'fi',
2074
2519
  MARKER_END,
2075
2520
  ].join('\n');
2076
2521
  let content;
@@ -2111,15 +2556,85 @@ function main() {
2111
2556
  process.exit(1);
2112
2557
  }
2113
2558
  });
2559
+ /**
2560
+ * homegraph spec evolve uninstall
2561
+ */
2562
+ evolveCommand
2563
+ .command('uninstall')
2564
+ .description('Remove the git post-commit hook installed by spec evolve')
2565
+ .option('-p, --path <path>', 'Path to the repository')
2566
+ .action(async (options) => {
2567
+ try {
2568
+ const repoPath = resolveSpecProjectPath(options.path);
2569
+ const { isGitRepo } = await Promise.resolve().then(() => __importStar(require('../spec/mining/git-scanner')));
2570
+ if (!isGitRepo(repoPath)) {
2571
+ error(`Not a git repository: ${repoPath}`);
2572
+ process.exit(1);
2573
+ }
2574
+ const { execFileSync } = await Promise.resolve().then(() => __importStar(require('child_process')));
2575
+ const hooksDir = execFileSync('git', ['rev-parse', '--git-path', 'hooks'], {
2576
+ cwd: repoPath,
2577
+ encoding: 'utf8',
2578
+ stdio: ['ignore', 'pipe', 'ignore'],
2579
+ windowsHide: true,
2580
+ }).trim();
2581
+ const resolvedHooksDir = path.isAbsolute(hooksDir)
2582
+ ? hooksDir
2583
+ : path.resolve(repoPath, hooksDir);
2584
+ const hookPath = path.join(resolvedHooksDir, 'post-commit');
2585
+ if (!fs.existsSync(hookPath)) {
2586
+ info('No post-commit hook found — nothing to uninstall.');
2587
+ return;
2588
+ }
2589
+ const MARKER_BEGIN = '# >>> homegraph spec evolve hook >>>';
2590
+ const MARKER_END = '# <<< homegraph spec evolve hook <<<';
2591
+ const existing = fs.readFileSync(hookPath, 'utf8');
2592
+ const lines = existing.split('\n');
2593
+ const kept = [];
2594
+ let inBlock = false;
2595
+ let removedBlock = false;
2596
+ for (const line of lines) {
2597
+ const trimmed = line.trim();
2598
+ if (trimmed === MARKER_BEGIN) {
2599
+ inBlock = true;
2600
+ removedBlock = true;
2601
+ continue;
2602
+ }
2603
+ if (trimmed === MARKER_END) {
2604
+ inBlock = false;
2605
+ continue;
2606
+ }
2607
+ if (!inBlock)
2608
+ kept.push(line);
2609
+ }
2610
+ if (!removedBlock) {
2611
+ info('No homegraph spec evolve hook block found — nothing to uninstall.');
2612
+ return;
2613
+ }
2614
+ const remaining = kept.join('\n').trim();
2615
+ if (remaining.length === 0 || /^#!\/bin\/sh\s*$/.test(remaining)) {
2616
+ // Hook only contained our block (or just a shebang) — delete entirely
2617
+ fs.unlinkSync(hookPath);
2618
+ success(`Removed post-commit hook at ${hookPath}`);
2619
+ }
2620
+ else {
2621
+ fs.writeFileSync(hookPath, remaining + '\n');
2622
+ success(`Removed spec evolve hook block from ${hookPath} (user content preserved)`);
2623
+ }
2624
+ }
2625
+ catch (err) {
2626
+ error(`Hook uninstall failed: ${err instanceof Error ? err.message : String(err)}`);
2627
+ process.exit(1);
2628
+ }
2629
+ });
2114
2630
  /**
2115
2631
  * homegraph spec evolve process
2116
2632
  */
2117
2633
  evolveCommand
2118
2634
  .command('process')
2119
- .description('Process a commit through the spec self-evolve pipeline')
2635
+ .description('Process commits through the spec self-evolve pipeline since last evolve')
2120
2636
  .option('-p, --path <path>', 'Path to the repository')
2121
2637
  .option('--db-path <path>', 'Path to the SQLite database file')
2122
- .option('--commit-hash <hash>', 'Commit hash to process (default: HEAD)', 'HEAD')
2123
2638
  .option('-j, --json', 'Output as JSON')
2124
2639
  .action(async (options) => {
2125
2640
  let db;
@@ -2129,7 +2644,7 @@ function main() {
2129
2644
  const { resolveDbPath } = await Promise.resolve().then(() => __importStar(require('../spec/utils')));
2130
2645
  const { initSpecSchema } = await Promise.resolve().then(() => __importStar(require('../spec/db/schema')));
2131
2646
  const { loadSpecConfig } = await Promise.resolve().then(() => __importStar(require('../spec/config')));
2132
- const { runEvolvePipeline } = await Promise.resolve().then(() => __importStar(require('../spec/evolve/pipeline')));
2647
+ const { runBatchEvolvePipeline } = await Promise.resolve().then(() => __importStar(require('../spec/evolve/pipeline')));
2133
2648
  const { isGitRepo } = await Promise.resolve().then(() => __importStar(require('../spec/mining/git-scanner')));
2134
2649
  if (!isGitRepo(repoPath)) {
2135
2650
  error(`Not a git repository: ${repoPath}`);
@@ -2141,25 +2656,77 @@ function main() {
2141
2656
  initSpecSchema(db);
2142
2657
  const config = loadSpecConfig(repoPath);
2143
2658
  const llmConfig = config.llm;
2144
- const commitHash = options.commitHash || 'HEAD';
2145
- const result = await runEvolvePipeline(repoPath, db, commitHash, llmConfig);
2659
+ if (!llmConfig) {
2660
+ warn('LLM not configured Path B (LLM-based spec evolution) will be skipped.');
2661
+ warn('Only Path A (commit message scope → GENERATE) will be processed.');
2662
+ warn('Configure LLM in .homegraph/commit4spec/configs.json for full functionality:\n' +
2663
+ '{\n' +
2664
+ ' "llm": {\n' +
2665
+ ' "provider": "openai",\n' +
2666
+ ' "apiKeyEnv": "OPENAI_API_KEY",\n' +
2667
+ ' "model": "gpt-4o"\n' +
2668
+ ' }\n' +
2669
+ '}');
2670
+ }
2671
+ const result = await runBatchEvolvePipeline(repoPath, db, llmConfig ?? undefined);
2146
2672
  if (options.json) {
2147
2673
  console.log(JSON.stringify(result, null, 2));
2148
2674
  }
2149
2675
  else {
2150
- console.log(chalk.bold(`Processing commit ${result.commitHash.slice(0, 7)}`));
2151
- console.log(`Is logic change: ${result.isLogicChange ? chalk.green(' yes') : chalk.red('❌ no')}`);
2152
- console.log(`Reason: ${result.logicCheckReason}`);
2153
- if (result.generateSpecId) {
2154
- console.log(chalk.cyan(`Link Path A - GENERATE ${result.generateSpecId}`));
2676
+ if (result.commitsProcessed === 0) {
2677
+ console.log(chalk.green('No new commits to evolve.'));
2678
+ console.log(`Last evolved commit: ${result.fromCommit ? result.fromCommit.slice(0, 7) : 'none'}`);
2679
+ console.log(`Current HEAD: ${result.toCommit.slice(0, 7)}`);
2155
2680
  }
2156
- if (result.evolvedSpecs.length > 0) {
2157
- console.log(chalk.cyan(`Cycle Path B - Self-evolve (${result.affectedSpecCount} affected)`));
2158
- for (const evolved of result.evolvedSpecs) {
2159
- console.log(` ${evolved.specId}: ${evolved.action}`);
2681
+ else {
2682
+ console.log(chalk.bold(`Evolve complete: ${result.commitsProcessed} commit(s) processed`));
2683
+ console.log(`Range: ${result.fromCommit ? result.fromCommit.slice(0, 7) : 'none'} → ${result.toCommit.slice(0, 7)}`);
2684
+ if (result.metaUpdated) {
2685
+ console.log(chalk.green(`meta.json updated (currentCommitID = ${result.toCommit.slice(0, 7)})`));
2686
+ }
2687
+ else if (result.failures > 0) {
2688
+ console.log(chalk.yellow(`⚠ meta.json NOT updated — ${result.failures} commit(s) failed`));
2160
2689
  }
2690
+ else if (result.skippedCommits > 0) {
2691
+ console.log(chalk.yellow('⚠ meta.json NOT updated — all commits were skipped (no Path A match, no LLM)'));
2692
+ }
2693
+ else {
2694
+ console.log(chalk.yellow('⚠ meta.json NOT updated'));
2695
+ }
2696
+ // Show per-commit summary
2697
+ for (const r of result.perCommitResults) {
2698
+ let prefix;
2699
+ if (r.skipped) {
2700
+ prefix = chalk.yellow(' ⚠');
2701
+ }
2702
+ else {
2703
+ prefix = r.persisted ? chalk.green(' ✓') : chalk.red(' ✗');
2704
+ }
2705
+ console.log(`${prefix} ${r.commitHash.slice(0, 7)}`);
2706
+ if (r.skipped) {
2707
+ console.log(` skipped: ${r.skipReason}`);
2708
+ }
2709
+ if (r.isLogicChange) {
2710
+ console.log(` logic change: ${r.logicCheckReason}`);
2711
+ }
2712
+ if (r.generateSpecId) {
2713
+ console.log(` Path A - GENERATE ${r.generateSpecId}`);
2714
+ }
2715
+ for (const ev of r.evolvedSpecs) {
2716
+ console.log(` ${ev.specId}: ${ev.action}`);
2717
+ }
2718
+ }
2719
+ // Aggregate summary
2720
+ const totalFragments = result.perCommitResults.reduce((s, r) => s + r.fragmentsCount, 0);
2721
+ const totalRelations = result.perCommitResults.reduce((s, r) => s + r.relationsCreated, 0);
2722
+ if (result.skippedCommits > 0) {
2723
+ console.log(chalk.yellow(`${result.skippedCommits} commit(s) skipped — no LLM configured, did not match Path A`));
2724
+ }
2725
+ if (result.failures > 0) {
2726
+ console.log(chalk.red(`${result.failures} commit(s) failed — see details above`));
2727
+ }
2728
+ console.log(`Summary: fragments=${totalFragments}, relations=${totalRelations}`);
2161
2729
  }
2162
- console.log(`Summary: fragments=${result.fragmentsCount}, relations=${result.relationsCreated}`);
2163
2730
  }
2164
2731
  }
2165
2732
  catch (err) {
@@ -2179,92 +2746,6 @@ function main() {
2179
2746
  catch { /* best effort */ }
2180
2747
  }
2181
2748
  });
2182
- /**
2183
- * homegraph install
2184
- */
2185
- program
2186
- .command('install')
2187
- .description('Install homegraph MCP server into one or more agents (Claude Code, Cursor, Codex CLI, opencode, DevEco Code, CodeBuddy, Hermes Agent)')
2188
- .option('-t, --target <ids>', 'Target agent(s): comma-separated ids, or "auto"|"all"|"none". Default: prompt')
2189
- .option('-l, --location <where>', 'Install location: "global" or "local". Default: prompt')
2190
- .option('-y, --yes', 'Non-interactive: defaults to --location=global --target=auto, auto-allow on')
2191
- .option('--no-permissions', 'Skip writing the auto-allow permissions list (Claude Code only)')
2192
- .option('--print-config <id>', 'Print MCP config snippet for the named agent and exit (no file writes)')
2193
- .action(async (opts) => {
2194
- if (opts.printConfig) {
2195
- const { getTarget, listTargetIds } = await Promise.resolve().then(() => __importStar(require('../installer/targets/registry')));
2196
- const target = getTarget(opts.printConfig);
2197
- if (!target) {
2198
- const known = listTargetIds().join(', ');
2199
- error(`Unknown target "${opts.printConfig}". Known: ${known}.`);
2200
- process.exit(1);
2201
- }
2202
- const loc = (opts.location === 'local' ? 'local' : 'global');
2203
- process.stdout.write(target.printConfig(loc));
2204
- return;
2205
- }
2206
- const { runInstallerWithOptions } = await Promise.resolve().then(() => __importStar(require('../installer')));
2207
- if (opts.location && opts.location !== 'global' && opts.location !== 'local') {
2208
- error(`--location must be "global" or "local" (got "${opts.location}").`);
2209
- process.exit(1);
2210
- }
2211
- try {
2212
- // Commander's `--no-permissions` makes `opts.permissions === false`;
2213
- // omitting the flag leaves it `true` (the positive-form default).
2214
- // We MUST treat the default-true as "user did not override — let
2215
- // the orchestrator prompt" and only forward an explicit `false`
2216
- // (or `true` when --yes implies it). Otherwise the auto-allow
2217
- // prompt is silently skipped on every interactive run.
2218
- const explicitNoPermissions = opts.permissions === false;
2219
- const autoAllow = explicitNoPermissions
2220
- ? false
2221
- : opts.yes
2222
- ? true
2223
- : undefined;
2224
- await runInstallerWithOptions({
2225
- target: opts.target,
2226
- location: opts.location,
2227
- autoAllow,
2228
- yes: opts.yes,
2229
- });
2230
- }
2231
- catch (err) {
2232
- error(err instanceof Error ? err.message : String(err));
2233
- process.exit(1);
2234
- }
2235
- });
2236
- /**
2237
- * homegraph uninstall
2238
- *
2239
- * Inverse of `install`. Removes the homegraph MCP server entry,
2240
- * instructions block, and permissions from every agent (or a
2241
- * `--target` subset). Prompts global-vs-local when not given. Does NOT
2242
- * delete the `.homegraph/` index — that's `homegraph uninit`.
2243
- */
2244
- program
2245
- .command('uninstall')
2246
- .description('Remove homegraph from your agents (Claude Code, Cursor, Codex CLI, opencode, DevEco Code, CodeBuddy, Hermes Agent)')
2247
- .option('-t, --target <ids>', 'Target agent(s): comma-separated ids, or "all". Default: all')
2248
- .option('-l, --location <where>', 'Uninstall location: "global" or "local". Default: prompt')
2249
- .option('-y, --yes', 'Non-interactive: defaults to --location=global --target=all')
2250
- .action(async (opts) => {
2251
- const { runUninstaller } = await Promise.resolve().then(() => __importStar(require('../installer')));
2252
- if (opts.location && opts.location !== 'global' && opts.location !== 'local') {
2253
- error(`--location must be "global" or "local" (got "${opts.location}").`);
2254
- process.exit(1);
2255
- }
2256
- try {
2257
- await runUninstaller({
2258
- target: opts.target,
2259
- location: opts.location,
2260
- yes: opts.yes,
2261
- });
2262
- }
2263
- catch (err) {
2264
- error(err instanceof Error ? err.message : String(err));
2265
- process.exit(1);
2266
- }
2267
- });
2268
2749
  /**
2269
2750
  * homegraph telemetry [on|off|status]
2270
2751
  */
@@ -2304,6 +2785,63 @@ function main() {
2304
2785
  console.log(`Config: ${s.configPath}`);
2305
2786
  console.log(chalk.dim(`\nExactly what is collected (and never collected): ${telemetry_1.TELEMETRY_DOCS}\n`));
2306
2787
  });
2788
+ /**
2789
+ * homegraph index-api <input> [version]
2790
+ * Build a standalone OHOS API database from command-line-tools SDK.
2791
+ */
2792
+ program
2793
+ .command('index-api <input> [version]')
2794
+ .description('Index OpenHarmony SDK API declarations into a standalone SQLite db')
2795
+ .option('-o, --output <path>', 'Output database path')
2796
+ .option('-q, --quiet', 'Suppress progress output')
2797
+ .action(async (input, versionArg, options) => {
2798
+ try {
2799
+ const { resolveOhosSdkInput, ohosApiDbFilename, indexOhosApiDb } = await Promise.resolve().then(() => __importStar(require('../extraction/languages/arkts')));
2800
+ const resolved = resolveOhosSdkInput({
2801
+ inputPath: input,
2802
+ versionOverride: versionArg,
2803
+ });
2804
+ const outputPath = options.output ?? path.join(process.cwd(), ohosApiDbFilename(resolved.version));
2805
+ if (!options.quiet) {
2806
+ info(`SDK home: ${resolved.sdkHome}`);
2807
+ info(`API version: ${resolved.version}`);
2808
+ info(`Output: ${outputPath}`);
2809
+ }
2810
+ const result = await indexOhosApiDb({
2811
+ sdkHome: resolved.sdkHome,
2812
+ version: resolved.version,
2813
+ outputPath,
2814
+ onProgress: options.quiet
2815
+ ? undefined
2816
+ : (progress) => {
2817
+ if (progress.phase === 'arkts-batch') {
2818
+ const label = progress.subphase === 'scene' ? 'Scene' : 'Persist';
2819
+ process.stdout.write(`\r${label}: ${progress.current}/${progress.total} ${progress.currentFile ?? ''}`.padEnd(80));
2820
+ }
2821
+ },
2822
+ });
2823
+ resolved.cleanup?.();
2824
+ if (!options.quiet) {
2825
+ process.stdout.write('\n');
2826
+ if (result.success) {
2827
+ info(`Indexed ${result.filesIndexed} SDK files → ${result.nodesCreated} nodes, ${result.edgesCreated} edges (${result.durationMs}ms)`);
2828
+ }
2829
+ else {
2830
+ error('OHOS API indexing failed');
2831
+ for (const err of result.errors.filter((e) => e.severity === 'error')) {
2832
+ error(err.message);
2833
+ }
2834
+ }
2835
+ }
2836
+ if (!result.success) {
2837
+ process.exit(1);
2838
+ }
2839
+ }
2840
+ catch (err) {
2841
+ error(err instanceof Error ? err.message : String(err));
2842
+ process.exit(1);
2843
+ }
2844
+ });
2307
2845
  /**
2308
2846
  * homegraph upgrade [version]
2309
2847
  *