trail-code 0.0.0-stage → 0.28.0-semantic.3

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 (593) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +567 -2
  3. package/dist/ai/crux.d.ts +88 -0
  4. package/dist/ai/crux.d.ts.map +1 -0
  5. package/dist/ai/crux.js +219 -0
  6. package/dist/ai/crux.js.map +1 -0
  7. package/dist/ai/failure.d.ts +60 -0
  8. package/dist/ai/failure.d.ts.map +1 -0
  9. package/dist/ai/failure.js +86 -0
  10. package/dist/ai/failure.js.map +1 -0
  11. package/dist/ai/llm/anthropic.d.ts +37 -0
  12. package/dist/ai/llm/anthropic.d.ts.map +1 -0
  13. package/dist/ai/llm/anthropic.js +144 -0
  14. package/dist/ai/llm/anthropic.js.map +1 -0
  15. package/dist/ai/llm/factory.d.ts +28 -0
  16. package/dist/ai/llm/factory.d.ts.map +1 -0
  17. package/dist/ai/llm/factory.js +36 -0
  18. package/dist/ai/llm/factory.js.map +1 -0
  19. package/dist/ai/llm/litellm.d.ts +34 -0
  20. package/dist/ai/llm/litellm.d.ts.map +1 -0
  21. package/dist/ai/llm/litellm.js +45 -0
  22. package/dist/ai/llm/litellm.js.map +1 -0
  23. package/dist/ai/llm/openai.d.ts +145 -0
  24. package/dist/ai/llm/openai.d.ts.map +1 -0
  25. package/dist/ai/llm/openai.js +629 -0
  26. package/dist/ai/llm/openai.js.map +1 -0
  27. package/dist/ai/llm/orcarouter.d.ts +34 -0
  28. package/dist/ai/llm/orcarouter.d.ts.map +1 -0
  29. package/dist/ai/llm/orcarouter.js +45 -0
  30. package/dist/ai/llm/orcarouter.js.map +1 -0
  31. package/dist/ai/llm/recover-tool.d.ts +21 -0
  32. package/dist/ai/llm/recover-tool.d.ts.map +1 -0
  33. package/dist/ai/llm/recover-tool.js +109 -0
  34. package/dist/ai/llm/recover-tool.js.map +1 -0
  35. package/dist/ai/llm/types.d.ts +122 -0
  36. package/dist/ai/llm/types.d.ts.map +1 -0
  37. package/dist/ai/llm/types.js +33 -0
  38. package/dist/ai/llm/types.js.map +1 -0
  39. package/dist/ai/providers.d.ts +59 -0
  40. package/dist/ai/providers.d.ts.map +1 -0
  41. package/dist/ai/providers.js +54 -0
  42. package/dist/ai/providers.js.map +1 -0
  43. package/dist/ai/summarize.d.ts +25 -0
  44. package/dist/ai/summarize.d.ts.map +1 -0
  45. package/dist/ai/summarize.js +34 -0
  46. package/dist/ai/summarize.js.map +1 -0
  47. package/dist/ai/synthesize.d.ts +40 -0
  48. package/dist/ai/synthesize.d.ts.map +1 -0
  49. package/dist/ai/synthesize.js +230 -0
  50. package/dist/ai/synthesize.js.map +1 -0
  51. package/dist/ask/ask.d.ts +199 -0
  52. package/dist/ask/ask.d.ts.map +1 -0
  53. package/dist/ask/ask.js +1291 -0
  54. package/dist/ask/ask.js.map +1 -0
  55. package/dist/ask/file-rank.d.ts +72 -0
  56. package/dist/ask/file-rank.d.ts.map +1 -0
  57. package/dist/ask/file-rank.js +160 -0
  58. package/dist/ask/file-rank.js.map +1 -0
  59. package/dist/ask/file-selection.d.ts +22 -0
  60. package/dist/ask/file-selection.d.ts.map +1 -0
  61. package/dist/ask/file-selection.js +66 -0
  62. package/dist/ask/file-selection.js.map +1 -0
  63. package/dist/ask/fuse.d.ts +195 -0
  64. package/dist/ask/fuse.d.ts.map +1 -0
  65. package/dist/ask/fuse.js +344 -0
  66. package/dist/ask/fuse.js.map +1 -0
  67. package/dist/ask/graphrank.d.ts +61 -0
  68. package/dist/ask/graphrank.d.ts.map +1 -0
  69. package/dist/ask/graphrank.js +130 -0
  70. package/dist/ask/graphrank.js.map +1 -0
  71. package/dist/ask/index-file.d.ts +44 -0
  72. package/dist/ask/index-file.d.ts.map +1 -0
  73. package/dist/ask/index-file.js +128 -0
  74. package/dist/ask/index-file.js.map +1 -0
  75. package/dist/ask/semantic.d.ts +42 -0
  76. package/dist/ask/semantic.d.ts.map +1 -0
  77. package/dist/ask/semantic.js +293 -0
  78. package/dist/ask/semantic.js.map +1 -0
  79. package/dist/bin/graft.d.ts +3 -0
  80. package/dist/bin/graft.d.ts.map +1 -0
  81. package/dist/bin/graft.js +14 -0
  82. package/dist/bin/graft.js.map +1 -0
  83. package/dist/bin/trail.d.ts +3 -0
  84. package/dist/bin/trail.d.ts.map +1 -0
  85. package/dist/bin/trail.js +13 -0
  86. package/dist/bin/trail.js.map +1 -0
  87. package/dist/blast/blast-cli.d.ts +35 -0
  88. package/dist/blast/blast-cli.d.ts.map +1 -0
  89. package/dist/blast/blast-cli.js +155 -0
  90. package/dist/blast/blast-cli.js.map +1 -0
  91. package/dist/blast/blast.d.ts +128 -0
  92. package/dist/blast/blast.d.ts.map +1 -0
  93. package/dist/blast/blast.js +383 -0
  94. package/dist/blast/blast.js.map +1 -0
  95. package/dist/blast/diff.d.ts +53 -0
  96. package/dist/blast/diff.d.ts.map +1 -0
  97. package/dist/blast/diff.js +193 -0
  98. package/dist/blast/diff.js.map +1 -0
  99. package/dist/blast/evidence.d.ts +51 -0
  100. package/dist/blast/evidence.d.ts.map +1 -0
  101. package/dist/blast/evidence.js +162 -0
  102. package/dist/blast/evidence.js.map +1 -0
  103. package/dist/blast/modules.d.ts +33 -0
  104. package/dist/blast/modules.d.ts.map +1 -0
  105. package/dist/blast/modules.js +83 -0
  106. package/dist/blast/modules.js.map +1 -0
  107. package/dist/blast/name.d.ts +74 -0
  108. package/dist/blast/name.d.ts.map +1 -0
  109. package/dist/blast/name.js +269 -0
  110. package/dist/blast/name.js.map +1 -0
  111. package/dist/blast/owners.d.ts +85 -0
  112. package/dist/blast/owners.d.ts.map +1 -0
  113. package/dist/blast/owners.js +272 -0
  114. package/dist/blast/owners.js.map +1 -0
  115. package/dist/blast/render.d.ts +46 -0
  116. package/dist/blast/render.d.ts.map +1 -0
  117. package/dist/blast/render.js +422 -0
  118. package/dist/blast/render.js.map +1 -0
  119. package/dist/blast/viz.d.ts +37 -0
  120. package/dist/blast/viz.d.ts.map +1 -0
  121. package/dist/blast/viz.js +180 -0
  122. package/dist/blast/viz.js.map +1 -0
  123. package/dist/brand.d.ts +111 -0
  124. package/dist/brand.d.ts.map +1 -0
  125. package/dist/brand.js +282 -0
  126. package/dist/brand.js.map +1 -0
  127. package/dist/claude/build-progress.d.ts +30 -0
  128. package/dist/claude/build-progress.d.ts.map +1 -0
  129. package/dist/claude/build-progress.js +81 -0
  130. package/dist/claude/build-progress.js.map +1 -0
  131. package/dist/claude/hermes-skill-template.d.ts +16 -0
  132. package/dist/claude/hermes-skill-template.d.ts.map +1 -0
  133. package/dist/claude/hermes-skill-template.js +154 -0
  134. package/dist/claude/hermes-skill-template.js.map +1 -0
  135. package/dist/claude/init.d.ts +65 -0
  136. package/dist/claude/init.d.ts.map +1 -0
  137. package/dist/claude/init.js +258 -0
  138. package/dist/claude/init.js.map +1 -0
  139. package/dist/claude/paths.d.ts +9 -0
  140. package/dist/claude/paths.d.ts.map +1 -0
  141. package/dist/claude/paths.js +22 -0
  142. package/dist/claude/paths.js.map +1 -0
  143. package/dist/claude/session-metrics.d.ts +73 -0
  144. package/dist/claude/session-metrics.d.ts.map +1 -0
  145. package/dist/claude/session-metrics.js +209 -0
  146. package/dist/claude/session-metrics.js.map +1 -0
  147. package/dist/claude/settings-merge.d.ts +76 -0
  148. package/dist/claude/settings-merge.d.ts.map +1 -0
  149. package/dist/claude/settings-merge.js +283 -0
  150. package/dist/claude/settings-merge.js.map +1 -0
  151. package/dist/claude/skill-template.d.ts +13 -0
  152. package/dist/claude/skill-template.d.ts.map +1 -0
  153. package/dist/claude/skill-template.js +290 -0
  154. package/dist/claude/skill-template.js.map +1 -0
  155. package/dist/claude/state.d.ts +85 -0
  156. package/dist/claude/state.d.ts.map +1 -0
  157. package/dist/claude/state.js +88 -0
  158. package/dist/claude/state.js.map +1 -0
  159. package/dist/claude/stats.d.ts +5 -0
  160. package/dist/claude/stats.d.ts.map +1 -0
  161. package/dist/claude/stats.js +24 -0
  162. package/dist/claude/stats.js.map +1 -0
  163. package/dist/cli-epilogue.d.ts +12 -0
  164. package/dist/cli-epilogue.d.ts.map +1 -0
  165. package/dist/cli-epilogue.js +72 -0
  166. package/dist/cli-epilogue.js.map +1 -0
  167. package/dist/cli-meta.d.ts +82 -0
  168. package/dist/cli-meta.d.ts.map +1 -0
  169. package/dist/cli-meta.js +238 -0
  170. package/dist/cli-meta.js.map +1 -0
  171. package/dist/cli-picker.d.ts +139 -0
  172. package/dist/cli-picker.d.ts.map +1 -0
  173. package/dist/cli-picker.js +439 -0
  174. package/dist/cli-picker.js.map +1 -0
  175. package/dist/cli.d.ts +3 -0
  176. package/dist/cli.d.ts.map +1 -0
  177. package/dist/cli.js +1638 -0
  178. package/dist/cli.js.map +1 -0
  179. package/dist/context/batches.d.ts +29 -0
  180. package/dist/context/batches.d.ts.map +1 -0
  181. package/dist/context/batches.js +130 -0
  182. package/dist/context/batches.js.map +1 -0
  183. package/dist/context/build.d.ts +57 -0
  184. package/dist/context/build.d.ts.map +1 -0
  185. package/dist/context/build.js +396 -0
  186. package/dist/context/build.js.map +1 -0
  187. package/dist/context/check.d.ts +42 -0
  188. package/dist/context/check.d.ts.map +1 -0
  189. package/dist/context/check.js +162 -0
  190. package/dist/context/check.js.map +1 -0
  191. package/dist/context/map-dir.d.ts +13 -0
  192. package/dist/context/map-dir.d.ts.map +1 -0
  193. package/dist/context/map-dir.js +81 -0
  194. package/dist/context/map-dir.js.map +1 -0
  195. package/dist/context/node-file.d.ts +109 -0
  196. package/dist/context/node-file.d.ts.map +1 -0
  197. package/dist/context/node-file.js +390 -0
  198. package/dist/context/node-file.js.map +1 -0
  199. package/dist/context/price.d.ts +49 -0
  200. package/dist/context/price.d.ts.map +1 -0
  201. package/dist/context/price.js +83 -0
  202. package/dist/context/price.js.map +1 -0
  203. package/dist/context/savings.d.ts +67 -0
  204. package/dist/context/savings.d.ts.map +1 -0
  205. package/dist/context/savings.js +125 -0
  206. package/dist/context/savings.js.map +1 -0
  207. package/dist/engine.d.ts +85 -0
  208. package/dist/engine.d.ts.map +1 -0
  209. package/dist/engine.js +122 -0
  210. package/dist/engine.js.map +1 -0
  211. package/dist/graph/bindings.d.ts +64 -0
  212. package/dist/graph/bindings.d.ts.map +1 -0
  213. package/dist/graph/bindings.js +713 -0
  214. package/dist/graph/bindings.js.map +1 -0
  215. package/dist/graph/build-run.d.ts +25 -0
  216. package/dist/graph/build-run.d.ts.map +1 -0
  217. package/dist/graph/build-run.js +121 -0
  218. package/dist/graph/build-run.js.map +1 -0
  219. package/dist/graph/build.d.ts +88 -0
  220. package/dist/graph/build.d.ts.map +1 -0
  221. package/dist/graph/build.js +386 -0
  222. package/dist/graph/build.js.map +1 -0
  223. package/dist/graph/cards.d.ts +46 -0
  224. package/dist/graph/cards.d.ts.map +1 -0
  225. package/dist/graph/cards.js +280 -0
  226. package/dist/graph/cards.js.map +1 -0
  227. package/dist/graph/check.d.ts +25 -0
  228. package/dist/graph/check.d.ts.map +1 -0
  229. package/dist/graph/check.js +192 -0
  230. package/dist/graph/check.js.map +1 -0
  231. package/dist/graph/container.d.ts +63 -0
  232. package/dist/graph/container.d.ts.map +1 -0
  233. package/dist/graph/container.js +191 -0
  234. package/dist/graph/container.js.map +1 -0
  235. package/dist/graph/depth-parser.d.ts +61 -0
  236. package/dist/graph/depth-parser.d.ts.map +1 -0
  237. package/dist/graph/depth-parser.js +153 -0
  238. package/dist/graph/depth-parser.js.map +1 -0
  239. package/dist/graph/enrich.d.ts +62 -0
  240. package/dist/graph/enrich.d.ts.map +1 -0
  241. package/dist/graph/enrich.js +258 -0
  242. package/dist/graph/enrich.js.map +1 -0
  243. package/dist/graph/extract-cache.d.ts +101 -0
  244. package/dist/graph/extract-cache.d.ts.map +1 -0
  245. package/dist/graph/extract-cache.js +199 -0
  246. package/dist/graph/extract-cache.js.map +1 -0
  247. package/dist/graph/extract.d.ts +85 -0
  248. package/dist/graph/extract.d.ts.map +1 -0
  249. package/dist/graph/extract.js +2377 -0
  250. package/dist/graph/extract.js.map +1 -0
  251. package/dist/graph/fingerprint.d.ts +76 -0
  252. package/dist/graph/fingerprint.d.ts.map +1 -0
  253. package/dist/graph/fingerprint.js +160 -0
  254. package/dist/graph/fingerprint.js.map +1 -0
  255. package/dist/graph/generic.d.ts +74 -0
  256. package/dist/graph/generic.d.ts.map +1 -0
  257. package/dist/graph/generic.js +773 -0
  258. package/dist/graph/generic.js.map +1 -0
  259. package/dist/graph/invariants.d.ts +33 -0
  260. package/dist/graph/invariants.d.ts.map +1 -0
  261. package/dist/graph/invariants.js +52 -0
  262. package/dist/graph/invariants.js.map +1 -0
  263. package/dist/graph/load.d.ts +29 -0
  264. package/dist/graph/load.d.ts.map +1 -0
  265. package/dist/graph/load.js +97 -0
  266. package/dist/graph/load.js.map +1 -0
  267. package/dist/graph/lsp/client.d.ts +61 -0
  268. package/dist/graph/lsp/client.d.ts.map +1 -0
  269. package/dist/graph/lsp/client.js +217 -0
  270. package/dist/graph/lsp/client.js.map +1 -0
  271. package/dist/graph/lsp/enrich.d.ts +11 -0
  272. package/dist/graph/lsp/enrich.d.ts.map +1 -0
  273. package/dist/graph/lsp/enrich.js +163 -0
  274. package/dist/graph/lsp/enrich.js.map +1 -0
  275. package/dist/graph/lsp/registry.d.ts +22 -0
  276. package/dist/graph/lsp/registry.d.ts.map +1 -0
  277. package/dist/graph/lsp/registry.js +71 -0
  278. package/dist/graph/lsp/registry.js.map +1 -0
  279. package/dist/graph/map.d.ts +107 -0
  280. package/dist/graph/map.d.ts.map +1 -0
  281. package/dist/graph/map.js +239 -0
  282. package/dist/graph/map.js.map +1 -0
  283. package/dist/graph/markdown.d.ts +22 -0
  284. package/dist/graph/markdown.d.ts.map +1 -0
  285. package/dist/graph/markdown.js +143 -0
  286. package/dist/graph/markdown.js.map +1 -0
  287. package/dist/graph/native.d.ts +46 -0
  288. package/dist/graph/native.d.ts.map +1 -0
  289. package/dist/graph/native.js +169 -0
  290. package/dist/graph/native.js.map +1 -0
  291. package/dist/graph/queries/c.scm +13 -0
  292. package/dist/graph/queries/c_sharp.scm +19 -0
  293. package/dist/graph/queries/clojure.scm +58 -0
  294. package/dist/graph/queries/cpp.scm +23 -0
  295. package/dist/graph/queries/dart.scm +42 -0
  296. package/dist/graph/queries/elixir.scm +54 -0
  297. package/dist/graph/queries/gleam.scm +51 -0
  298. package/dist/graph/queries/java.scm +20 -0
  299. package/dist/graph/queries/kotlin.scm +43 -0
  300. package/dist/graph/queries/lua.scm +36 -0
  301. package/dist/graph/queries/nix.scm +32 -0
  302. package/dist/graph/queries/php.scm +40 -0
  303. package/dist/graph/queries/ruby.scm +65 -0
  304. package/dist/graph/queries/rust.scm +41 -0
  305. package/dist/graph/queries/scala.scm +66 -0
  306. package/dist/graph/queries/solidity.scm +43 -0
  307. package/dist/graph/queries/swift.scm +51 -0
  308. package/dist/graph/refresh.d.ts +106 -0
  309. package/dist/graph/refresh.d.ts.map +1 -0
  310. package/dist/graph/refresh.js +430 -0
  311. package/dist/graph/refresh.js.map +1 -0
  312. package/dist/graph/relations.d.ts +14 -0
  313. package/dist/graph/relations.d.ts.map +1 -0
  314. package/dist/graph/relations.js +8 -0
  315. package/dist/graph/relations.js.map +1 -0
  316. package/dist/graph/resolve.d.ts +17 -0
  317. package/dist/graph/resolve.d.ts.map +1 -0
  318. package/dist/graph/resolve.js +716 -0
  319. package/dist/graph/resolve.js.map +1 -0
  320. package/dist/graph/root.d.ts +11 -0
  321. package/dist/graph/root.d.ts.map +1 -0
  322. package/dist/graph/root.js +40 -0
  323. package/dist/graph/root.js.map +1 -0
  324. package/dist/graph/scopes.d.ts +40 -0
  325. package/dist/graph/scopes.d.ts.map +1 -0
  326. package/dist/graph/scopes.js +383 -0
  327. package/dist/graph/scopes.js.map +1 -0
  328. package/dist/graph/seed.d.ts +38 -0
  329. package/dist/graph/seed.d.ts.map +1 -0
  330. package/dist/graph/seed.js +229 -0
  331. package/dist/graph/seed.js.map +1 -0
  332. package/dist/graph/source-files.d.ts +46 -0
  333. package/dist/graph/source-files.d.ts.map +1 -0
  334. package/dist/graph/source-files.js +118 -0
  335. package/dist/graph/source-files.js.map +1 -0
  336. package/dist/graph/traverse-cli.d.ts +55 -0
  337. package/dist/graph/traverse-cli.d.ts.map +1 -0
  338. package/dist/graph/traverse-cli.js +189 -0
  339. package/dist/graph/traverse-cli.js.map +1 -0
  340. package/dist/graph/traverse.d.ts +112 -0
  341. package/dist/graph/traverse.d.ts.map +1 -0
  342. package/dist/graph/traverse.js +192 -0
  343. package/dist/graph/traverse.js.map +1 -0
  344. package/dist/graph/types.d.ts +78 -0
  345. package/dist/graph/types.d.ts.map +1 -0
  346. package/dist/graph/types.js +14 -0
  347. package/dist/graph/types.js.map +1 -0
  348. package/dist/graph/wasm-runtime.d.ts +19 -0
  349. package/dist/graph/wasm-runtime.d.ts.map +1 -0
  350. package/dist/graph/wasm-runtime.js +38 -0
  351. package/dist/graph/wasm-runtime.js.map +1 -0
  352. package/dist/graph/workspace-cli.d.ts +56 -0
  353. package/dist/graph/workspace-cli.d.ts.map +1 -0
  354. package/dist/graph/workspace-cli.js +111 -0
  355. package/dist/graph/workspace-cli.js.map +1 -0
  356. package/dist/graph/workspace.d.ts +172 -0
  357. package/dist/graph/workspace.d.ts.map +1 -0
  358. package/dist/graph/workspace.js +714 -0
  359. package/dist/graph/workspace.js.map +1 -0
  360. package/dist/graph/write.d.ts +13 -0
  361. package/dist/graph/write.d.ts.map +1 -0
  362. package/dist/graph/write.js +86 -0
  363. package/dist/graph/write.js.map +1 -0
  364. package/dist/hosts/antigravity.d.ts +14 -0
  365. package/dist/hosts/antigravity.d.ts.map +1 -0
  366. package/dist/hosts/antigravity.js +59 -0
  367. package/dist/hosts/antigravity.js.map +1 -0
  368. package/dist/hosts/auto.d.ts +37 -0
  369. package/dist/hosts/auto.d.ts.map +1 -0
  370. package/dist/hosts/auto.js +141 -0
  371. package/dist/hosts/auto.js.map +1 -0
  372. package/dist/hosts/claude-global.d.ts +72 -0
  373. package/dist/hosts/claude-global.d.ts.map +1 -0
  374. package/dist/hosts/claude-global.js +230 -0
  375. package/dist/hosts/claude-global.js.map +1 -0
  376. package/dist/hosts/config-write.d.ts +36 -0
  377. package/dist/hosts/config-write.d.ts.map +1 -0
  378. package/dist/hosts/config-write.js +64 -0
  379. package/dist/hosts/config-write.js.map +1 -0
  380. package/dist/hosts/init.d.ts +28 -0
  381. package/dist/hosts/init.d.ts.map +1 -0
  382. package/dist/hosts/init.js +89 -0
  383. package/dist/hosts/init.js.map +1 -0
  384. package/dist/hosts/instructions.d.ts +14 -0
  385. package/dist/hosts/instructions.d.ts.map +1 -0
  386. package/dist/hosts/instructions.js +95 -0
  387. package/dist/hosts/instructions.js.map +1 -0
  388. package/dist/hosts/mcp-config.d.ts +78 -0
  389. package/dist/hosts/mcp-config.d.ts.map +1 -0
  390. package/dist/hosts/mcp-config.js +251 -0
  391. package/dist/hosts/mcp-config.js.map +1 -0
  392. package/dist/hosts/plan.d.ts +32 -0
  393. package/dist/hosts/plan.d.ts.map +1 -0
  394. package/dist/hosts/plan.js +73 -0
  395. package/dist/hosts/plan.js.map +1 -0
  396. package/dist/hosts/registry.d.ts +41 -0
  397. package/dist/hosts/registry.d.ts.map +1 -0
  398. package/dist/hosts/registry.js +164 -0
  399. package/dist/hosts/registry.js.map +1 -0
  400. package/dist/hosts/retract.d.ts +47 -0
  401. package/dist/hosts/retract.d.ts.map +1 -0
  402. package/dist/hosts/retract.js +520 -0
  403. package/dist/hosts/retract.js.map +1 -0
  404. package/dist/hosts/sections.d.ts +47 -0
  405. package/dist/hosts/sections.d.ts.map +1 -0
  406. package/dist/hosts/sections.js +127 -0
  407. package/dist/hosts/sections.js.map +1 -0
  408. package/dist/index.d.ts +41 -0
  409. package/dist/index.d.ts.map +1 -0
  410. package/dist/index.js +32 -0
  411. package/dist/index.js.map +1 -0
  412. package/dist/ingest/fs.d.ts +79 -0
  413. package/dist/ingest/fs.d.ts.map +1 -0
  414. package/dist/ingest/fs.js +306 -0
  415. package/dist/ingest/fs.js.map +1 -0
  416. package/dist/init-guard.d.ts +16 -0
  417. package/dist/init-guard.d.ts.map +1 -0
  418. package/dist/init-guard.js +27 -0
  419. package/dist/init-guard.js.map +1 -0
  420. package/dist/mcp/instructions.d.ts +5 -0
  421. package/dist/mcp/instructions.d.ts.map +1 -0
  422. package/dist/mcp/instructions.js +64 -0
  423. package/dist/mcp/instructions.js.map +1 -0
  424. package/dist/mcp/kept-listing.d.ts +27 -0
  425. package/dist/mcp/kept-listing.d.ts.map +1 -0
  426. package/dist/mcp/kept-listing.js +63 -0
  427. package/dist/mcp/kept-listing.js.map +1 -0
  428. package/dist/mcp/server.d.ts +4 -0
  429. package/dist/mcp/server.d.ts.map +1 -0
  430. package/dist/mcp/server.js +147 -0
  431. package/dist/mcp/server.js.map +1 -0
  432. package/dist/mcp/tool-names.d.ts +27 -0
  433. package/dist/mcp/tool-names.d.ts.map +1 -0
  434. package/dist/mcp/tool-names.js +47 -0
  435. package/dist/mcp/tool-names.js.map +1 -0
  436. package/dist/mcp/tools.d.ts +22 -0
  437. package/dist/mcp/tools.d.ts.map +1 -0
  438. package/dist/mcp/tools.js +440 -0
  439. package/dist/mcp/tools.js.map +1 -0
  440. package/dist/node-version.d.ts +15 -0
  441. package/dist/node-version.d.ts.map +1 -0
  442. package/dist/node-version.js +23 -0
  443. package/dist/node-version.js.map +1 -0
  444. package/dist/search/grep-cli.d.ts +35 -0
  445. package/dist/search/grep-cli.d.ts.map +1 -0
  446. package/dist/search/grep-cli.js +115 -0
  447. package/dist/search/grep-cli.js.map +1 -0
  448. package/dist/search/grep.d.ts +60 -0
  449. package/dist/search/grep.d.ts.map +1 -0
  450. package/dist/search/grep.js +143 -0
  451. package/dist/search/grep.js.map +1 -0
  452. package/dist/search/ignored-code.d.ts +9 -0
  453. package/dist/search/ignored-code.d.ts.map +1 -0
  454. package/dist/search/ignored-code.js +73 -0
  455. package/dist/search/ignored-code.js.map +1 -0
  456. package/dist/search/user-regexp.d.ts +26 -0
  457. package/dist/search/user-regexp.d.ts.map +1 -0
  458. package/dist/search/user-regexp.js +150 -0
  459. package/dist/search/user-regexp.js.map +1 -0
  460. package/dist/telemetry/contract.d.ts +117 -0
  461. package/dist/telemetry/contract.d.ts.map +1 -0
  462. package/dist/telemetry/contract.js +341 -0
  463. package/dist/telemetry/contract.js.map +1 -0
  464. package/dist/telemetry/flush.d.ts +21 -0
  465. package/dist/telemetry/flush.d.ts.map +1 -0
  466. package/dist/telemetry/flush.js +85 -0
  467. package/dist/telemetry/flush.js.map +1 -0
  468. package/dist/telemetry/gate.d.ts +30 -0
  469. package/dist/telemetry/gate.d.ts.map +1 -0
  470. package/dist/telemetry/gate.js +81 -0
  471. package/dist/telemetry/gate.js.map +1 -0
  472. package/dist/telemetry/identity.d.ts +40 -0
  473. package/dist/telemetry/identity.d.ts.map +1 -0
  474. package/dist/telemetry/identity.js +80 -0
  475. package/dist/telemetry/identity.js.map +1 -0
  476. package/dist/telemetry/index.d.ts +18 -0
  477. package/dist/telemetry/index.d.ts.map +1 -0
  478. package/dist/telemetry/index.js +16 -0
  479. package/dist/telemetry/index.js.map +1 -0
  480. package/dist/telemetry/key.d.ts +21 -0
  481. package/dist/telemetry/key.d.ts.map +1 -0
  482. package/dist/telemetry/key.js +49 -0
  483. package/dist/telemetry/key.js.map +1 -0
  484. package/dist/telemetry/notice.d.ts +19 -0
  485. package/dist/telemetry/notice.d.ts.map +1 -0
  486. package/dist/telemetry/notice.js +93 -0
  487. package/dist/telemetry/notice.js.map +1 -0
  488. package/dist/telemetry/queue.d.ts +35 -0
  489. package/dist/telemetry/queue.d.ts.map +1 -0
  490. package/dist/telemetry/queue.js +162 -0
  491. package/dist/telemetry/queue.js.map +1 -0
  492. package/dist/telemetry/send.d.ts +19 -0
  493. package/dist/telemetry/send.d.ts.map +1 -0
  494. package/dist/telemetry/send.js +84 -0
  495. package/dist/telemetry/send.js.map +1 -0
  496. package/dist/telemetry/sessions.d.ts +25 -0
  497. package/dist/telemetry/sessions.d.ts.map +1 -0
  498. package/dist/telemetry/sessions.js +104 -0
  499. package/dist/telemetry/sessions.js.map +1 -0
  500. package/dist/telemetry/track.d.ts +62 -0
  501. package/dist/telemetry/track.d.ts.map +1 -0
  502. package/dist/telemetry/track.js +143 -0
  503. package/dist/telemetry/track.js.map +1 -0
  504. package/dist/upkeep-run.d.ts +22 -0
  505. package/dist/upkeep-run.d.ts.map +1 -0
  506. package/dist/upkeep-run.js +85 -0
  507. package/dist/upkeep-run.js.map +1 -0
  508. package/dist/upkeep.d.ts +127 -0
  509. package/dist/upkeep.d.ts.map +1 -0
  510. package/dist/upkeep.js +252 -0
  511. package/dist/upkeep.js.map +1 -0
  512. package/dist/util/actions-folder.d.ts +2 -0
  513. package/dist/util/actions-folder.d.ts.map +1 -0
  514. package/dist/util/actions-folder.js +9 -0
  515. package/dist/util/actions-folder.js.map +1 -0
  516. package/dist/util/browser.d.ts +3 -0
  517. package/dist/util/browser.d.ts.map +1 -0
  518. package/dist/util/browser.js +14 -0
  519. package/dist/util/browser.js.map +1 -0
  520. package/dist/util/child-process.d.ts +15 -0
  521. package/dist/util/child-process.d.ts.map +1 -0
  522. package/dist/util/child-process.js +15 -0
  523. package/dist/util/child-process.js.map +1 -0
  524. package/dist/util/front-matter.d.ts +46 -0
  525. package/dist/util/front-matter.d.ts.map +1 -0
  526. package/dist/util/front-matter.js +113 -0
  527. package/dist/util/front-matter.js.map +1 -0
  528. package/dist/util/id.d.ts +7 -0
  529. package/dist/util/id.d.ts.map +1 -0
  530. package/dist/util/id.js +14 -0
  531. package/dist/util/id.js.map +1 -0
  532. package/dist/util/paths.d.ts +29 -0
  533. package/dist/util/paths.d.ts.map +1 -0
  534. package/dist/util/paths.js +71 -0
  535. package/dist/util/paths.js.map +1 -0
  536. package/dist/util/private-file.d.ts +14 -0
  537. package/dist/util/private-file.d.ts.map +1 -0
  538. package/dist/util/private-file.js +82 -0
  539. package/dist/util/private-file.js.map +1 -0
  540. package/dist/util/source.d.ts +4 -0
  541. package/dist/util/source.d.ts.map +1 -0
  542. package/dist/util/source.js +12 -0
  543. package/dist/util/source.js.map +1 -0
  544. package/dist/util/spinner.d.ts +15 -0
  545. package/dist/util/spinner.d.ts.map +1 -0
  546. package/dist/util/spinner.js +52 -0
  547. package/dist/util/spinner.js.map +1 -0
  548. package/dist/util/state.d.ts +120 -0
  549. package/dist/util/state.d.ts.map +1 -0
  550. package/dist/util/state.js +264 -0
  551. package/dist/util/state.js.map +1 -0
  552. package/dist/vendor/grammars/tree-sitter-go.LICENSE +21 -0
  553. package/dist/vendor/grammars/tree-sitter-go.wasm +0 -0
  554. package/dist/vendor/grammars/tree-sitter-java.LICENSE +21 -0
  555. package/dist/vendor/grammars/tree-sitter-java.wasm +0 -0
  556. package/dist/vendor/grammars/tree-sitter-kotlin.LICENSE +9 -0
  557. package/dist/vendor/grammars/tree-sitter-kotlin.wasm +0 -0
  558. package/dist/vendor/grammars/tree-sitter-php.LICENSE +22 -0
  559. package/dist/vendor/grammars/tree-sitter-php.wasm +0 -0
  560. package/dist/vendor/grammars/tree-sitter-python.LICENSE +21 -0
  561. package/dist/vendor/grammars/tree-sitter-python.wasm +0 -0
  562. package/dist/vendor/grammars/tree-sitter-r.LICENSE +9 -0
  563. package/dist/vendor/grammars/tree-sitter-r.wasm +0 -0
  564. package/dist/vendor/grammars/tree-sitter-swift.LICENSE +21 -0
  565. package/dist/vendor/grammars/tree-sitter-swift.wasm +0 -0
  566. package/dist/vendor/grammars/tree-sitter-tsx.wasm +0 -0
  567. package/dist/vendor/grammars/tree-sitter-typescript.LICENSE +21 -0
  568. package/dist/vendor/grammars/tree-sitter-typescript.wasm +0 -0
  569. package/dist/viewer/app.js +2 -0
  570. package/dist/viewer/index.html +49 -0
  571. package/dist/viewer/style.css +154 -0
  572. package/dist/viz/assemble.d.ts +83 -0
  573. package/dist/viz/assemble.d.ts.map +1 -0
  574. package/dist/viz/assemble.js +101 -0
  575. package/dist/viz/assemble.js.map +1 -0
  576. package/dist/viz/export.d.ts +39 -0
  577. package/dist/viz/export.d.ts.map +1 -0
  578. package/dist/viz/export.js +98 -0
  579. package/dist/viz/export.js.map +1 -0
  580. package/dist/viz/serve.d.ts +13 -0
  581. package/dist/viz/serve.d.ts.map +1 -0
  582. package/dist/viz/serve.js +130 -0
  583. package/dist/viz/serve.js.map +1 -0
  584. package/package.json +115 -4
  585. package/scripts/build-grammar-wasm.mjs +112 -0
  586. package/scripts/build-viewer.mjs +44 -0
  587. package/scripts/graph-quality.mjs +117 -0
  588. package/scripts/pack-trail.mjs +53 -0
  589. package/scripts/postinstall.mjs +65 -0
  590. package/scripts/run-tests.mjs +72 -0
  591. package/scripts/stamp-telemetry-key.mjs +56 -0
  592. package/scripts/tally-audit.mjs +140 -0
  593. package/scripts/vendor-grammars.mjs +53 -0
package/dist/cli.js ADDED
@@ -0,0 +1,1638 @@
1
+ #!/usr/bin/env node
2
+ import "dotenv/config";
3
+ import { Command } from "commander";
4
+ import { existsSync, readFileSync, statSync } from "node:fs";
5
+ import { join, relative, resolve } from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+ import { Graft } from "./engine.js";
8
+ import { resolveConfig } from "./ai/providers.js";
9
+ import { formatCheckReport } from "./context/check.js";
10
+ import { formatGraphCheckReport } from "./graph/check.js";
11
+ import { buildGraphIfMissing, runInit } from "./claude/init.js";
12
+ import { statuslineWanted } from "./claude/settings-merge.js";
13
+ import { runHostsInit } from "./hosts/init.js";
14
+ import { hostIds } from "./hosts/registry.js";
15
+ import { agentList, autoSelection } from "./hosts/auto.js";
16
+ import { asksForTrail, brand, cmd, graftNotice, setBrand, strandedRepoNotice, tag } from "./brand.js";
17
+ import { contextDirFor } from "./context/node-file.js";
18
+ import { loadGraphCached } from "./graph/load.js";
19
+ import { ensureFreshChildren, ensureFreshGraph, refreshNote } from "./graph/refresh.js";
20
+ import { isWorkspaceBuildRoot, loadWorkspaceGraphs, readWorkspace } from "./graph/workspace.js";
21
+ import { homeFolderRefusal } from "./init-guard.js";
22
+ import { nearestGraftRoot } from "./graph/root.js";
23
+ import { unsupportedExtensions, supportedExtensions } from "./graph/source-files.js";
24
+ import { discoverWorkspaceChildren } from "./graph/scopes.js";
25
+ import { runWorkspaceAsk, runWorkspaceBuild, runWorkspaceCallers, runWorkspaceCheck, runWorkspaceGrep, runWorkspaceMap, } from "./graph/workspace-cli.js";
26
+ import { planInit, selectedWrites } from "./hosts/plan.js";
27
+ import { planRetract, runRetract, changed } from "./hosts/retract.js";
28
+ import { formatNonInteractiveHelp, formatPlan, runPicker } from "./cli-picker.js";
29
+ import { homedir } from "node:os";
30
+ import { formatUpgradeReport, formatVersionReport, getNpmViewVersion, readCurrentVersion, readPackageName, runUpgrade } from "./cli-meta.js";
31
+ import { nativeFallbackNotice } from "./graph/native.js";
32
+ import { patchBuildConfig } from "./util/state.js";
33
+ import { normalizePathPrefix } from "./util/paths.js";
34
+ import { latestSession, formatSessionStats, sessionInputRate } from "./claude/session-metrics.js";
35
+ import { listSessionIds, readSession, sessionDir } from "./claude/state.js";
36
+ import { setInputRate } from "./context/savings.js";
37
+ import { formatUpdateNudge, maybeRefreshInBackground, readUpdateCache, refreshUpdateCache, writeStamp } from "./upkeep.js";
38
+ import { errorCode, filesBucket, durationBucket, formatDebug, formatStatus, firstRunNotice, isTrackedCommand, langsValue, offReason, maybeFlushInBackground, patchState, runFlush, track, trackFirstRunIfNew, } from "./telemetry/index.js";
39
+ const program = new Command();
40
+ const currentVersion = readCurrentVersion(import.meta.url);
41
+ // When Commander prints an error (unknown option, missing required option, etc.)
42
+ // append a hint so users know how to discover the available flags.
43
+ program.configureOutput({
44
+ writeErr: (str) => {
45
+ process.stderr.write(str);
46
+ },
47
+ outputError: (str, write) => {
48
+ write(str);
49
+ // Append a --help hint when Commander exits with a parse error, so users
50
+ // know how to discover available flags instead of having to guess.
51
+ if (str.startsWith('error:')) {
52
+ const sub = process.argv[2];
53
+ const hint = sub && !sub.startsWith('-')
54
+ ? `Run 'trail ${sub} --help' to see available options.\n`
55
+ : `Run 'trail --help' to see available commands.\n`;
56
+ process.stderr.write(hint);
57
+ }
58
+ },
59
+ });
60
+ /**
61
+ * Started as `trail` rather than `graft`. Decided once, before any command is
62
+ * registered, because it changes which commands exist: under trail the old
63
+ * `graft trail …` group moves to the top level, `check` hands its name to the
64
+ * team check (the freshness check is `build --check`), and `stats` and
65
+ * `trail status` become one `status`. Under graft nothing moves. Trail's setup
66
+ * prompt runs as trail whichever name started it (`asksForTrail`).
67
+ */
68
+ if (asksForTrail(process.argv.slice(2)))
69
+ setBrand("trail");
70
+ const TRAIL = brand() === "trail";
71
+ /** Help-screen groups under trail; graft keeps its one flat list. */
72
+ const GROUP = {
73
+ find: "Find code · your agent runs these:",
74
+ setup: "Setup:",
75
+ ci: "CI:",
76
+ };
77
+ /** Put a command in a help group, under trail only. */
78
+ function grouped(c, heading) {
79
+ if (TRAIL)
80
+ c.helpGroup(heading);
81
+ return c;
82
+ }
83
+ /**
84
+ * What the `query` telemetry event will say, filled in by the command as it runs
85
+ * and emitted once from the `postAction` hook below.
86
+ *
87
+ * A module-level slot rather than a threaded parameter because the repo root is
88
+ * resolved deep inside each action (`queryRoot`) while the event is emitted
89
+ * centrally — and because a command that calls `process.exit` should simply
90
+ * report nothing, which falls out of never emitting until postAction.
91
+ */
92
+ let queryNote = {};
93
+ /** Record the repo a query ran against, and pass it straight through so call
94
+ * sites stay one line. */
95
+ function noteQuery(dir) {
96
+ queryNote.repo = dir;
97
+ // Every retrieval command funnels through here, which makes it the one place
98
+ // that can price this session's tokens before a formatter needs the number.
99
+ setInputRate(sessionInputRate(dir));
100
+ return dir;
101
+ }
102
+ /** Whether a query found anything. Only the commands that have a result count in
103
+ * hand call this; the property is simply absent for the others. */
104
+ function noteHit(found) {
105
+ queryNote.hit = found ? "yes" : "no";
106
+ }
107
+ program
108
+ .name(brand())
109
+ .description(TRAIL
110
+ ? "A code map for your coding agents: exact file:line spans, a call graph,\n" +
111
+ "and a semantic index, kept in sync with the working tree as you query it."
112
+ : "Build a repo's context graph as linked markdown, and keep it in sync with the code.")
113
+ .version(currentVersion, "-v, --version")
114
+ .option("--dir <path>", "context graph directory (default: <repo>/graft)")
115
+ .option("--provider <name>", "LLM wire format: openai | anthropic | litellm | orcarouter (env GRAFT_PROVIDER)")
116
+ .option("--model <id>", "model id for the LLM pass (env GRAFT_MODEL)")
117
+ .option("--api-key <key>", "provider API key (env GRAFT_API_KEY)")
118
+ .option("--base-url <url>", "OpenAI-compatible endpoint URL (env GRAFT_BASE_URL)");
119
+ /** Config drawn from the global CLI flags (env + defaults fill the rest). */
120
+ function cliConfig() {
121
+ const o = program.opts();
122
+ return {
123
+ contextDir: o.dir,
124
+ provider: o.provider,
125
+ model: o.model,
126
+ apiKey: o.apiKey,
127
+ baseUrl: o.baseUrl,
128
+ };
129
+ }
130
+ const engineFrom = () => new Graft(cliConfig());
131
+ /**
132
+ * Warn (never fail) when a user's `-e` extension has no parser, so it is never a silent
133
+ * no-op — `graft build -e ".vue"` used to accept it, index nothing, and exit 0. The
134
+ * supported set is listed so `-e` also answers "what is actually supported".
135
+ */
136
+ function warnUnsupportedExtensions(exts) {
137
+ if (!exts?.length)
138
+ return;
139
+ const bad = unsupportedExtensions(exts);
140
+ if (bad.length === 0)
141
+ return;
142
+ for (const e of bad) {
143
+ const shown = e.trim().startsWith(".") ? e.trim() : `.${e.trim()}`;
144
+ console.error(`⚠ -e "${shown}": no parser registered for this extension, so it is ignored.`);
145
+ }
146
+ console.error(` supported: ${supportedExtensions().join(" ")}`);
147
+ }
148
+ /** Text for the omitted-`[dir]` case, shared by every query command's help. */
149
+ const DIR_ARG = ["[dir]", "repository root (default: nearest ancestor with a graft/ index)"];
150
+ /**
151
+ * The root a query runs against: the dir the user named, else the nearest
152
+ * ancestor holding a graft index (`graph/root.ts`) so a shell or agent session
153
+ * in a subdirectory still finds the graph. The walk is announced on stderr —
154
+ * answering from an ancestor's graph must never be silent.
155
+ */
156
+ function queryRoot(dir) {
157
+ if (dir !== undefined)
158
+ return resolve(dir);
159
+ const { root, levels } = nearestGraftRoot(process.cwd(), program.opts().dir);
160
+ if (levels > 0)
161
+ console.error(`${tag()} no code map here: answering from ${contextDirFor(root, program.opts().dir)}`);
162
+ return root;
163
+ }
164
+ /**
165
+ * Bring the graph up to date with the working tree before a query answers from it
166
+ * — the same gate the MCP tools run (see `graph/refresh.ts`). Cheap when nothing
167
+ * moved; a structural, $0 rebuild when it did. The note goes to stderr so `--json`
168
+ * stdout stays machine-readable.
169
+ */
170
+ async function refreshBefore(dir, opts) {
171
+ const globalDir = program.opts().dir;
172
+ const root = resolve(dir);
173
+ const disabled = opts.refresh === false;
174
+ const ws = readWorkspace(root, globalDir);
175
+ const r = ws
176
+ ? await ensureFreshChildren(root, ws.children, { contextDir: globalDir, disabled })
177
+ : await ensureFreshGraph(root, { contextDir: globalDir, disabled });
178
+ const note = refreshNote(r);
179
+ if (note)
180
+ console.error(note);
181
+ }
182
+ /** Attached to every query command: `--no-refresh` answers from the graph exactly
183
+ * as it is on disk, no rebuild. */
184
+ const NO_REFRESH_FLAG = ["--no-refresh", "skip the freshness check: answer from the graph as-is"];
185
+ const VIZ_TABS = ["context", "code", "outline"];
186
+ /**
187
+ * `--tabs context,code` → the tabs to write into an exported page.
188
+ *
189
+ * An unknown name is a caller mistake worth failing on rather than silently
190
+ * dropping: a page exported with a typo'd tab list would be missing a tab and
191
+ * nothing would say why. Undefined means "all of them", which is the default the
192
+ * exporter already applies.
193
+ */
194
+ function parseTabs(raw) {
195
+ if (raw === undefined)
196
+ return undefined;
197
+ const want = raw.split(",").map((t) => t.trim()).filter(Boolean);
198
+ const bad = want.filter((t) => !VIZ_TABS.includes(t));
199
+ if (bad.length > 0 || want.length === 0) {
200
+ console.error(`✗ --tabs takes a comma-separated subset of ${VIZ_TABS.join(", ")}${bad.length ? `, not "${bad.join('", "')}"` : ""}`);
201
+ process.exit(1);
202
+ }
203
+ return want;
204
+ }
205
+ /**
206
+ * Commands that own the upgrade story themselves (`version`, `upgrade`) or must
207
+ * not editorialize on stderr at startup (`mcp` runs its own upkeep at boot, and
208
+ * `_update-check` IS the fetch).
209
+ */
210
+ const UPKEEP_SKIP = new Set(["version", "upgrade", "_update-check", "mcp"]);
211
+ /**
212
+ * Every other command: top up the cached registry answer in the background and,
213
+ * if a newer graft is out, say so once on stderr. This is what makes the CLI the
214
+ * cache filler for the hooks, which are not allowed to touch the network.
215
+ */
216
+ program.hook("preAction", (_parent, action) => {
217
+ if (UPKEEP_SKIP.has(action.name()))
218
+ return;
219
+ // Someone typing graft by hand learns its new name, once a day. Agents,
220
+ // hooks, CI and pipes never see it (see graftNotice).
221
+ const renamed = graftNotice({ ran: spelling(action), tty: Boolean(process.stderr.isTTY) });
222
+ if (renamed)
223
+ console.error(renamed);
224
+ // Someone typing trail in a repo whose graft-era hooks can't reach it learns
225
+ // the one command that moves it (init itself is that command).
226
+ const stranded = action.name() === "init" ? null : strandedRepoNotice({ cwd: process.cwd(), tty: Boolean(process.stderr.isTTY) });
227
+ if (stranded)
228
+ console.error(stranded);
229
+ maybeRefreshInBackground();
230
+ const nudge = formatUpdateNudge(currentVersion, readUpdateCache()?.latest);
231
+ if (nudge)
232
+ console.error(nudge);
233
+ // Telemetry, in the order a user should experience it: disclose first, then
234
+ // record, then (at most once a day, detached) send. Every step is a no-op in a
235
+ // fork, in CI, under DO_NOT_TRACK, or after `graft telemetry disable`.
236
+ const notice = firstRunNotice();
237
+ if (notice)
238
+ console.error(notice);
239
+ trackFirstRunIfNew();
240
+ maybeFlushInBackground();
241
+ });
242
+ /**
243
+ * The `query` event, emitted after the command rather than before it, so it can
244
+ * carry what the query actually did. A command that exits early via
245
+ * `process.exit` never reaches here and is simply not counted — under-reporting
246
+ * is the right failure mode for a metric.
247
+ */
248
+ program.hook("postAction", (_parent, action) => {
249
+ // `trail build --check` is graft's `check`, and counts as it always has.
250
+ const name = queryNote.command ?? action.name();
251
+ if (!isTrackedCommand(name))
252
+ return;
253
+ track("query", { command: name, surface: "cli", hit: queryNote.hit, notes: queryNote.notes }, { repo: queryNote.repo });
254
+ });
255
+ /** `graft ask`, `graft build`: a command as graft spells it, whatever ran it. */
256
+ function spelling(action) {
257
+ const words = [];
258
+ for (let c = action; c && c.parent; c = c.parent)
259
+ words.unshift(c.name());
260
+ return ["graft", ...words].join(" ");
261
+ }
262
+ program
263
+ .command("_update-check", { hidden: true })
264
+ .description("internal: refresh the cached latest-version answer")
265
+ .action(() => {
266
+ refreshUpdateCache();
267
+ });
268
+ // Hidden for the same reason as _update-check: only ever spawned detached, by
269
+ // maybeFlushInBackground. Running it by hand is harmless — it drains the queue.
270
+ program
271
+ .command("_telemetry-flush", { hidden: true })
272
+ .description("internal: POST the queued anonymous usage events")
273
+ .action(async () => {
274
+ await runFlush();
275
+ });
276
+ grouped(program.command("telemetry"), GROUP.setup)
277
+ .description("Show, inspect, or turn off the anonymous usage stats (see TELEMETRY.md)")
278
+ .argument("[action]", "status (default) | enable | disable | debug", "status")
279
+ .action((action) => {
280
+ switch (action) {
281
+ case "status":
282
+ console.log(formatStatus());
283
+ return;
284
+ case "enable":
285
+ patchState({ enabled: true });
286
+ console.log(`telemetry: on, anonymous and aggregate-only. \`${cmd("graft telemetry status")}\` for details.`);
287
+ return;
288
+ case "disable":
289
+ // Also stamp the notice as shown: someone who has just opted out should
290
+ // not be told about telemetry again the next time they run a command.
291
+ patchState({ enabled: false, noticeShownAt: new Date().toISOString() });
292
+ console.log("telemetry: off. Nothing further will be recorded or sent.");
293
+ return;
294
+ case "debug":
295
+ console.log(formatDebug());
296
+ return;
297
+ default:
298
+ console.error(`✗ unknown action "${action}": expected status, enable, disable, or debug`);
299
+ process.exit(1);
300
+ }
301
+ });
302
+ // Hidden under trail, where -v says the same; listed under graft as it was.
303
+ program
304
+ .command("version", { hidden: TRAIL })
305
+ .description("Print the installed version and the latest published on npm")
306
+ .action(() => {
307
+ const latest = getNpmViewVersion();
308
+ console.log(formatVersionReport(currentVersion, latest));
309
+ });
310
+ grouped(program.command("upgrade"), GROUP.setup)
311
+ .description(`Upgrade the globally installed ${brand()} to the latest version on npm`)
312
+ .action(() => {
313
+ const result = runUpgrade(import.meta.url);
314
+ console.log(formatUpgradeReport(result));
315
+ if (result.ran && !result.ok)
316
+ process.exit(1);
317
+ });
318
+ grouped(program.command("config", { hidden: !TRAIL }), GROUP.setup)
319
+ .description("Read or update workspace or global trail configuration");
320
+ grouped(program.command("build"), GROUP.setup)
321
+ .description(TRAIL
322
+ ? "Rebuild the code map in .trail/cache/ ($0, no key). --check fails if it's stale; --deep adds LLM summaries."
323
+ : "Build graft/ from your code: wiring graph + per-file cards ($0, no key). " +
324
+ "Add --deep for the LLM concept map + per-symbol summaries/crux.")
325
+ .argument("[dir]", "repository root", ".")
326
+ .option("--check", "build nothing: fail if graft/ is stale relative to the code (for CI)")
327
+ .option("--json", "with --check: output the drift as JSON")
328
+ .option("--deep", "run the LLM pass: concept nodes (graft/*.md) + per-symbol summary/crux")
329
+ .option("-e, --extensions <exts...>", 'code extensions to include (e.g. ".ts" ".py"); an extension with no parser is ignored with a warning that lists the supported set')
330
+ .option("-j, --concurrency <n>", "files summarized in parallel during --deep (default 5)")
331
+ .option("--synth-concurrency <n>", "concept synthesis batches in flight at once during --deep (default 4). Separate from -j: " +
332
+ "each synthesis call carries a whole batch of summaries, not one file")
333
+ .option("--no-reuse", "re-parse every file instead of replaying unchanged ones from the extraction cache")
334
+ .option("--lsp", "add compiler-grade call edges via a language server if one is installed (opt-in, slower; e.g. rust-analyzer, clangd)")
335
+ .option("--allow-partial", "with --deep: exit 0 even when some files' summaries failed (default: a degraded meaning tier exits 1)")
336
+ .option("--follow-submodules", "include initialized Git submodules recursively; persisted for later builds and automatic refreshes")
337
+ .option("--no-follow-submodules", "exclude Git submodules; persisted for later builds and automatic refreshes (default)")
338
+ .option("--follow-nested-repos", "include nested Git clones the index does not track (a multi-repo manifest checkout, or any repo cloned into the tree) " +
339
+ "as ONE graph, so imports across them resolve; persisted for later builds and automatic refreshes")
340
+ .option("--no-follow-nested-repos", "exclude untracked nested Git clones; persisted for later builds and automatic refreshes (default)")
341
+ .option("--include-dir <name>", "override SKIP_DIRS for this repo's walks, repeatable (e.g. --include-dir build --include-dir tools); " +
342
+ "persisted, so a later build (and the automatic refresh before a query) include it without the flag; dot-dirs are never overridable", (val, prev) => [...prev, val], [])
343
+ .option("--only-dir <path>", "only index files under this repo-relative path, repeatable; the wiring walk and the --deep " +
344
+ "concept pass both honor it. Recorded in the graph fingerprint so a later build " +
345
+ "(and the automatic refresh before a query) walks the same set; everything outside the list is skipped", (val, prev) => [...prev, val], [])
346
+ .option("--no-gitignore", "skip writing graft/ into .gitignore (same as GRAFT_NO_GITIGNORE=1)")
347
+ .option("--no-ignore", "skip writing .ignore for ripgrep re-admit (same as GRAFT_NO_IGNORE=1)")
348
+ .option("--no-semantic", "skip embedding nodes for semantic search after building the graph")
349
+ .option("--include-md", "index .md files as graph nodes (headings → symbols); persisted for later builds and refreshes")
350
+ .option("--no-include-md", "stop indexing .md files (revert a previous --include-md); persisted")
351
+ .option("--md-only-dir <path>", "limit .md indexing to this repo-relative dir, repeatable (e.g. --md-only-dir specs); persisted", (val, prev) => [...prev, val], [])
352
+ .option("--include-openspec-archive", "also index openspec/changes/archive/ .md files (excluded by default); persisted")
353
+ .action(async (dir, opts, command) => {
354
+ if (opts.check) {
355
+ // The freshness check, which was `graft check`. Counted as `check` so the
356
+ // metric carries on across the rename; the dir defaults like a query's.
357
+ queryNote.command = "check";
358
+ await runCheckCommand(command.args[0], { extensions: opts.extensions, json: opts.json }, cmd("graft check"));
359
+ return;
360
+ }
361
+ const buildStartedAt = Date.now();
362
+ if (opts.gitignore === false)
363
+ process.env.GRAFT_NO_GITIGNORE = "1";
364
+ if (opts.ignore === false)
365
+ process.env.GRAFT_NO_IGNORE = "1";
366
+ const concurrency = opts.concurrency ? Math.max(1, Number(opts.concurrency)) : undefined;
367
+ if (opts.concurrency && !Number.isFinite(concurrency)) {
368
+ console.error(`✗ --concurrency must be a number, got "${opts.concurrency}"`);
369
+ process.exit(1);
370
+ }
371
+ const synthConcurrency = opts.synthConcurrency ? Math.max(1, Number(opts.synthConcurrency)) : undefined;
372
+ if (opts.synthConcurrency && !Number.isFinite(synthConcurrency)) {
373
+ console.error(`✗ --synth-concurrency must be a number, got "${opts.synthConcurrency}"`);
374
+ process.exit(1);
375
+ }
376
+ warnUnsupportedExtensions(opts.extensions);
377
+ // Persisted BEFORE the build itself runs, so this invocation's walks (and
378
+ // every later no-flag build / hooks refresh) see it identically — the
379
+ // walkDir call sites read it from state, not from a threaded option.
380
+ const buildConfigPatch = {};
381
+ if (opts.includeDir && opts.includeDir.length > 0) {
382
+ // --include-dir takes bare SKIP_DIRS-style directory NAMES (shouldSkipDir
383
+ // compares a single path segment), never paths, and dot-dirs are never
384
+ // overridable at all (see the option's own help text) — reject anything
385
+ // else up front instead of silently persisting a value that can never
386
+ // match a real directory name.
387
+ for (const name of opts.includeDir) {
388
+ if (name.startsWith(".")) {
389
+ console.error(`✗ --include-dir "${name}": dot-directories are never overridable`);
390
+ process.exit(1);
391
+ }
392
+ if (name.includes("/") || name.includes("\\")) {
393
+ console.error(`✗ --include-dir "${name}": expected a bare directory name, not a path`);
394
+ process.exit(1);
395
+ }
396
+ }
397
+ buildConfigPatch.includeDirs = opts.includeDir;
398
+ }
399
+ // The whitelist is NOT persisted to `.graft/config.json`: it belongs with the
400
+ // graph (the fingerprint records it at build time), never in the source repo,
401
+ // so a `--only-dir` build leaves no trace under the repo being indexed.
402
+ let onlyDirs;
403
+ if (opts.onlyDir && opts.onlyDir.length > 0) {
404
+ // --only-dir takes a repo-relative path prefix, normalized to the same
405
+ // posix, no-`./`, no-trailing-slash form `--in` uses, so the prefix match
406
+ // is exact. A prefix that normalizes to "" (a bare "/" or ".") is rejected:
407
+ // it would mean "match nothing" or "match everything", neither of which is
408
+ // a deliberate whitelist.
409
+ const normalized = opts.onlyDir.map((p) => normalizePathPrefix(p)).filter((p) => p !== "");
410
+ if (normalized.length === 0) {
411
+ console.error("✗ --only-dir: expected a non-empty repo-relative path");
412
+ process.exit(1);
413
+ }
414
+ onlyDirs = normalized;
415
+ }
416
+ const followSubmodulesWasExplicit = command.getOptionValueSource("followSubmodules") === "cli";
417
+ if (followSubmodulesWasExplicit && typeof opts.followSubmodules === "boolean") {
418
+ buildConfigPatch.followSubmodules = opts.followSubmodules;
419
+ }
420
+ const followNestedReposWasExplicit = command.getOptionValueSource("followNestedRepos") === "cli";
421
+ if (followNestedReposWasExplicit && typeof opts.followNestedRepos === "boolean") {
422
+ buildConfigPatch.followNestedRepos = opts.followNestedRepos;
423
+ }
424
+ const includeMdWasExplicit = command.getOptionValueSource("includeMd") === "cli";
425
+ if (includeMdWasExplicit && typeof opts.includeMd === "boolean") {
426
+ buildConfigPatch.includeMd = opts.includeMd;
427
+ }
428
+ if (opts.mdOnlyDir && opts.mdOnlyDir.length > 0) {
429
+ buildConfigPatch.mdOnlyDirs = opts.mdOnlyDir;
430
+ }
431
+ const includeOpenSpecArchiveWasExplicit = command.getOptionValueSource("includeOpenSpecArchive") === "cli";
432
+ if (includeOpenSpecArchiveWasExplicit && opts.includeOpenSpecArchive !== undefined) {
433
+ buildConfigPatch.includeOpenSpecArchive = opts.includeOpenSpecArchive;
434
+ }
435
+ if (Object.keys(buildConfigPatch).length > 0) {
436
+ patchBuildConfig(resolve(dir), buildConfigPatch);
437
+ }
438
+ const engine = engineFrom();
439
+ const fmt = (o) => Object.entries(o)
440
+ .sort((a, b) => b[1] - a[1])
441
+ .map(([k, n]) => `${n} ${k}`)
442
+ .join(", ");
443
+ // --deep needs a key; without one, degrade to the $0 structural build.
444
+ let deep = opts.deep;
445
+ const resolved = resolveConfig(cliConfig());
446
+ if (deep && !resolved.apiKey) {
447
+ deep = false;
448
+ console.error("⚠ no API key set, so falling back to the structural build (no LLM summaries).\n" +
449
+ " Set GRAFT_API_KEY (and GRAFT_PROVIDER / GRAFT_BASE_URL / GRAFT_MODEL for your\n" +
450
+ ` provider) and re-run \`${cmd("graft build --deep")}\` to add concept nodes and summaries.`);
451
+ }
452
+ if (deep && resolved.usedLegacyEnv) {
453
+ console.error("⚠ using OPENROUTER_API_KEY (deprecated): prefer GRAFT_API_KEY + GRAFT_BASE_URL.");
454
+ }
455
+ // Workspace parent: build each child into its OWN graft/ + a workspace index.
456
+ const buildRoot = resolve(dir);
457
+ const buildGlobalDir = program.opts().dir;
458
+ if (isWorkspaceBuildRoot(buildRoot, buildGlobalDir)) {
459
+ await runWorkspaceBuild(buildRoot, {
460
+ deep: !!deep,
461
+ extensions: opts.extensions,
462
+ concurrency,
463
+ synthConcurrency,
464
+ childConfig: cliConfig(),
465
+ override: buildGlobalDir,
466
+ includeDirs: opts.includeDir,
467
+ followSubmodules: followSubmodulesWasExplicit ? opts.followSubmodules : undefined,
468
+ followNestedRepos: followNestedReposWasExplicit ? opts.followNestedRepos : undefined,
469
+ includeMd: includeMdWasExplicit ? opts.includeMd : undefined,
470
+ mdOnlyDirs: opts.mdOnlyDir && opts.mdOnlyDir.length > 0 ? opts.mdOnlyDir : undefined,
471
+ includeOpenSpecArchive: includeOpenSpecArchiveWasExplicit ? opts.includeOpenSpecArchive : undefined,
472
+ packageName: readPackageName(import.meta.url),
473
+ });
474
+ // --semantic: embed each child repo after workspace build.
475
+ if (opts.semantic) {
476
+ const { embedNodes: doEmbedWs } = await import('./ask/semantic.js');
477
+ const { join: pathJoinWs } = await import('node:path');
478
+ const { contextDirFor: ctxDirForWs } = await import('./context/node-file.js');
479
+ const { loadGraphCached: loadGraphWs } = await import('./graph/load.js');
480
+ const ws = readWorkspace(buildRoot, buildGlobalDir);
481
+ if (ws) {
482
+ for (const child of ws.children) {
483
+ const childRoot = pathJoinWs(buildRoot, child);
484
+ const childCtx = ctxDirForWs(childRoot);
485
+ const loaded = loadGraphWs(childCtx);
486
+ if (loaded) {
487
+ const dbPath = pathJoinWs(childCtx, '.cache', 'embeddings.db');
488
+ await doEmbedWs(loaded.nodes, dbPath);
489
+ }
490
+ }
491
+ process.stderr.write('[semantic] Workspace embedding complete\n');
492
+ }
493
+ }
494
+ return;
495
+ }
496
+ // --deep: concept nodes first, then the wiring graph links cards up to them.
497
+ let conceptErrors = [];
498
+ let conceptFatal;
499
+ if (deep) {
500
+ const c = await engine.init(dir, {
501
+ extensions: opts.extensions,
502
+ onlyDirs,
503
+ synthConcurrency,
504
+ onProgress: ({ phase, index, total, file }) => process.stderr.write(`\r${phase === "summarize" ? "reading" : "writing"} concepts ${index + 1}/${total}: ${file.slice(0, 40).padEnd(40)}`),
505
+ }).catch((err) => {
506
+ // Only the stage and a code enum; the message stays on this machine.
507
+ track("build_failed", { stage: "summarize", code: errorCode(err) }, { repo: buildRoot });
508
+ throw err;
509
+ });
510
+ process.stderr.write("\n");
511
+ const conceptLine = `concepts: ${c.nodes} nodes, ${c.links} links from ${c.files} files (${c.summarized} read, ${c.cached} cached)`;
512
+ if (c.nodes === 0)
513
+ console.error(`⚠ ${conceptLine}`);
514
+ else
515
+ console.log(`✓ ${conceptLine}`);
516
+ for (const e of c.errors)
517
+ console.error(`✗ ${e}`);
518
+ conceptErrors = c.errors;
519
+ conceptFatal = c.fatal;
520
+ }
521
+ // Wiring graph — always; LLM meaning only with --deep.
522
+ const g = await engine.graph(dir, {
523
+ llm: deep,
524
+ concurrency,
525
+ reuse: opts.reuse,
526
+ lsp: opts.lsp,
527
+ onlyDirs,
528
+ onProgress: ({ phase, index, total, file }) => process.stderr.write(`\r${phase === "enrich" ? "summarizing" : "parsing"} ${index + 1}/${total}: ${file.slice(0, 50).padEnd(50)}`),
529
+ }).catch((err) => {
530
+ track("build_failed", { stage: "graph", code: errorCode(err) }, { repo: buildRoot });
531
+ throw err;
532
+ });
533
+ process.stderr.write("\n");
534
+ console.log(`✓ wiring: ${g.nodes} nodes (${fmt(g.byKind)}), ${g.edges} edges, ${g.cards} cards [${g.languages.join(", ")}]`);
535
+ console.log(` parsed: ${g.parsed} of ${g.files} files (${g.reused} replayed from cache)`);
536
+ // Worth one line: this build started from a graph the user never built *here*.
537
+ if (g.seededFrom)
538
+ console.log(` seeded: copied a starting graph from ${g.seededFrom} (git worktree)`);
539
+ if (deep) {
540
+ const m = g.meaning;
541
+ console.log(` meaning: ${m.computed} computed, ${m.cached} cached, ${m.stale} stale, ${m.pending} pending`);
542
+ }
543
+ console.log(` → ${g.contextDir}`);
544
+ // The activation event. Everything here is a bucket or a fixed label: repo
545
+ // scale rather than a file count, a language set rather than file names.
546
+ track("build_completed", {
547
+ files_bucket: filesBucket(g.files),
548
+ langs: langsValue(g.languages),
549
+ mode: deep ? "deep" : "fast",
550
+ duration_bucket: durationBucket(Date.now() - buildStartedAt),
551
+ incremental: String(g.reused > 0),
552
+ }, { repo: buildRoot });
553
+ for (const e of g.errors)
554
+ console.error(`✗ ${e}`);
555
+ // A language whose depth grammar didn't load was parsed by its breadth stand-in:
556
+ // say which, and the install that fixes it. Nothing when everything loaded.
557
+ const fallbackNotice = nativeFallbackNotice(g.nativeFallback, readPackageName(import.meta.url));
558
+ if (fallbackNotice)
559
+ console.error(fallbackNotice);
560
+ const rel = relative(process.cwd(), g.contextDir) || "graft";
561
+ if (process.env.GRAFT_NO_GITIGNORE) {
562
+ console.log(` ${rel}/ is a local cache: add it to your gitignore if you want it untracked.`);
563
+ }
564
+ else {
565
+ console.log(` ${rel}/ is git-ignored (added automatically), a local cache; teammates run \`${cmd("graft build")}\` to get their own.`);
566
+ }
567
+ // --semantic: embed all nodes for semantic search after a successful build.
568
+ if (opts.semantic) {
569
+ const { embedNodes: doEmbed } = await import('./ask/semantic.js');
570
+ const { join: pathJoin } = await import('node:path');
571
+ const { contextDirFor } = await import('./context/node-file.js');
572
+ const { loadGraphCached } = await import('./graph/load.js');
573
+ const contextDir = contextDirFor(buildRoot, program.opts().dir);
574
+ const loaded = loadGraphCached(contextDir);
575
+ if (loaded) {
576
+ const dbPath = pathJoin(contextDir, '.cache', 'embeddings.db');
577
+ await doEmbed(loaded.nodes, dbPath);
578
+ process.stderr.write('[semantic] Embedding complete\n');
579
+ }
580
+ }
581
+ // #127: a --deep run whose LLM calls failed used to print the same success
582
+ // footer and exit 0, so a quota-exhausted build looked identical to a clean
583
+ // one and `graft check` still said "in sync" (it only ever checked Tier-1).
584
+ // The structural graph IS still written and every successful summary is
585
+ // cached, so this is a loud warning about a degraded tier, not a rollback.
586
+ if (deep) {
587
+ const m = g.meaning;
588
+ const failed = m.failedFiles > 0 || m.fatal !== undefined || conceptErrors.length > 0 || conceptFatal !== undefined;
589
+ if (failed) {
590
+ const ready = m.computed + m.cached;
591
+ const total = ready + m.stale + m.pending;
592
+ const pct = total > 0 ? Math.round((ready / total) * 100) : 0;
593
+ console.error("");
594
+ console.error(`✗ the deep pass did not complete, so the meaning tier is incomplete.`);
595
+ if (conceptFatal)
596
+ console.error(` concepts: ${conceptFatal}`);
597
+ if (m.fatal)
598
+ console.error(` summaries: ${m.fatal}`);
599
+ if (m.failedFiles > 0) {
600
+ const skipped = m.skippedFiles > 0 ? `, ${m.skippedFiles} never attempted` : "";
601
+ console.error(` ${m.failedFiles} file(s) failed to summarize${skipped}.`);
602
+ }
603
+ if (conceptErrors.length > 0)
604
+ console.error(` ${conceptErrors.length} concept-pass error(s).`);
605
+ console.error(` meaning coverage: ${ready}/${total} symbols (${pct}%).`);
606
+ console.error(` Nothing computed was lost: re-run \`${cmd("graft build --deep")}\` to resume from what is cached.\n` +
607
+ " Pass --allow-partial to accept a degraded meaning tier and exit 0.");
608
+ if (!opts.allowPartial)
609
+ process.exitCode = 1;
610
+ }
611
+ }
612
+ });
613
+ /**
614
+ * After `ask`, under trail: which shown learnings are a teammate's, and the
615
+ * lookup recorded (the learnings shown are a use, which is what the team page
616
+ * counts as reuse; the tokens saved ride along). Then the background upkeep
617
+ * that keeps branches and the workspace current. Spawns no git itself. Teammates' branches come up after an edit
618
+ * instead, once there's a function to compare (notes/edit-overlap.ts).
619
+ */
620
+ function afterAsk(r, repo) {
621
+ // Note: learnings tracking removed in trail-solo-grafo.
622
+ void r;
623
+ void repo;
624
+ }
625
+ grouped(program.command("ask"), GROUP.find)
626
+ .description("Query the graft/ graph: returns ranked nodes + exact file:line, routed to prose or wiring ($0, no key)")
627
+ .argument("<query>", "what you want to understand, in plain words (- reads it from stdin)")
628
+ .argument(...DIR_ARG)
629
+ .option("-n, --limit <n>", "max results", "8")
630
+ .option("--source", "inline the source at each file:line hit (retriever mode: the pack IS the answer, no need to re-open files)")
631
+ .option("--full", "with --source: inline whole definition spans instead of the default ≤8-line crux excerpts")
632
+ .option("--in <path>", "narrow to nodes under this path prefix, filtered before scoring (segment-aware, like scopeOf)")
633
+ .option("--json", "output the result as JSON")
634
+ .option("--no-graph-rank", "rank by lexical relevance only, without the graph-connectivity re-rank (ablation/eval)")
635
+ .option("--semantic", "use semantic (embedding) search instead of lexical — finds conceptually related code even without exact keywords; fallback when MCP trail_search_semantic is not available")
636
+ .option(...NO_REFRESH_FLAG)
637
+ .action(async (query, dirArg, opts) => {
638
+ // The prompt hook passes what the person typed on stdin, out of sight of `ps`.
639
+ if (query === "-")
640
+ query = readFileSync(0, "utf8").trim();
641
+ const dir = noteQuery(queryRoot(dirArg));
642
+ await refreshBefore(dir, opts);
643
+ const askGlobalDir = program.opts().dir;
644
+ // --semantic: vector search fallback (equivalent to MCP trail_search_semantic).
645
+ if (opts.semantic) {
646
+ const limit = Number(opts.limit);
647
+ const { searchSemantic } = await import('./ask/semantic.js');
648
+ const { contextDirFor } = await import('./context/node-file.js');
649
+ const ws = readWorkspace(dir, askGlobalDir);
650
+ const results = [];
651
+ const children = ws?.children ?? [];
652
+ if (children.length > 0) {
653
+ // Workspace: search each child's DB.
654
+ const { join: pjoin } = await import('node:path');
655
+ for (const child of children) {
656
+ const childCtx = contextDirFor(join(dir, child));
657
+ const db = pjoin(childCtx, '.cache', 'embeddings.db');
658
+ if (!existsSync(db))
659
+ continue;
660
+ try {
661
+ const hits = await searchSemantic(query, db, limit);
662
+ hits.forEach(h => results.push({ ...h, file_path: h.file_path.startsWith(child) ? h.file_path : `${child}/${h.file_path}` }));
663
+ }
664
+ catch { /* DB not ready */ }
665
+ }
666
+ }
667
+ else {
668
+ // Single repo.
669
+ const ctx = contextDirFor(dir, askGlobalDir);
670
+ const db = join(ctx, '.cache', 'embeddings.db');
671
+ if (existsSync(db)) {
672
+ try {
673
+ (await searchSemantic(query, db, limit)).forEach(h => results.push(h));
674
+ }
675
+ catch { /* DB not ready */ }
676
+ }
677
+ }
678
+ if (results.length === 0) {
679
+ console.error('no semantic matches found — run `trail build` to build the semantic index');
680
+ process.exitCode = 1;
681
+ return;
682
+ }
683
+ results.sort((a, b) => b.score - a.score);
684
+ results.slice(0, limit).filter(h => h.score > 0.05).forEach((h, i) => {
685
+ console.log(`${i + 1}. ${h.file_path} · ${h.symbol} (score: ${h.score.toFixed(3)})\n ${h.snippet}`);
686
+ });
687
+ return;
688
+ }
689
+ if (readWorkspace(dir, askGlobalDir)) {
690
+ runWorkspaceAsk(dir, askGlobalDir, query, {
691
+ limit: Number(opts.limit), source: opts.source, full: opts.full, in: opts.in, json: opts.json,
692
+ });
693
+ return;
694
+ }
695
+ const engine = engineFrom();
696
+ let r;
697
+ try {
698
+ r = engine.ask(dir, query, { limit: Number(opts.limit), source: opts.source, full: opts.full, in: opts.in, graphRank: opts.graphRank });
699
+ }
700
+ catch (err) {
701
+ console.error(`✗ ${err instanceof Error ? err.message : String(err)}`);
702
+ process.exit(1);
703
+ return;
704
+ }
705
+ noteHit(r.hits.length > 0);
706
+ // Teammates' branches on the same files, and the learnings shown, counted as a use.
707
+ if (TRAIL)
708
+ afterAsk(r, dir);
709
+ if (opts.json) {
710
+ console.log(JSON.stringify(r, null, 2));
711
+ }
712
+ else {
713
+ const { formatAsk } = await import("./ask/ask.js");
714
+ process.stdout.write(formatAsk(r));
715
+ }
716
+ });
717
+ grouped(program.command("skeleton"), GROUP.find)
718
+ .description("Signatures-only view of one file from the wiring graph: the cheapest way to see a file's API surface")
719
+ .argument("<file>", "repo-relative path (or unique basename) of the file")
720
+ .argument(...DIR_ARG)
721
+ .option("--json", "output the result as JSON")
722
+ .option(...NO_REFRESH_FLAG)
723
+ .action(async (file, dirArg, opts) => {
724
+ const dir = noteQuery(queryRoot(dirArg));
725
+ await refreshBefore(dir, opts);
726
+ const { skeleton, formatSkeleton } = await import("./ask/ask.js");
727
+ const globalOpts = program.opts();
728
+ const r = skeleton(dir, file, { contextDir: globalOpts.dir });
729
+ if (opts.json)
730
+ console.log(JSON.stringify(r, null, 2));
731
+ else
732
+ process.stdout.write(formatSkeleton(r));
733
+ });
734
+ if (!TRAIL) {
735
+ program
736
+ .command("check")
737
+ .description("Fail if graft/ is stale relative to the code (for CI)")
738
+ .argument(...DIR_ARG)
739
+ .option("-e, --extensions <exts...>", "code extensions to include")
740
+ .option("--json", "output the drift as JSON")
741
+ .action(async (dirArg, opts) => {
742
+ await runCheckCommand(dirArg, opts, "graft check");
743
+ });
744
+ }
745
+ // Under trail, `check` is the team check (teamCheckCommand, with the cloud
746
+ // commands below); the freshness check is `trail build --check`.
747
+ /**
748
+ * The freshness check: `graft check`, and `trail build --check`. `label` is how
749
+ * the report names the command, so each name prints its own spelling.
750
+ */
751
+ async function runCheckCommand(dirArg, opts, label) {
752
+ warnUnsupportedExtensions(opts.extensions);
753
+ const dir = noteQuery(queryRoot(dirArg));
754
+ const checkGlobalDir = program.opts().dir;
755
+ if (readWorkspace(dir, checkGlobalDir)) {
756
+ await runWorkspaceCheck(dir, checkGlobalDir);
757
+ return;
758
+ }
759
+ const engine = engineFrom();
760
+ const r = engine.check(dir, { extensions: opts.extensions });
761
+ const g = await engine.checkGraph(dir); // graph.json is only judged when it exists
762
+ // A layer that IS present must be in sync; a never-built layer (keyless
763
+ // build skips the markdown layer) is informational, not a failure.
764
+ const bothMissing = r.missing && g.missing;
765
+ const markdownFail = !r.missing && !r.ok;
766
+ const wiringFail = !g.missing && !g.ok;
767
+ if (opts.json) {
768
+ console.log(JSON.stringify({ context: r, graph: g.missing ? null : g }, null, 2));
769
+ }
770
+ else if (bothMissing) {
771
+ console.log(`${label}: NO GRAPH\n\nNo code map found. Run \`${cmd("graft build")}\` first.`);
772
+ }
773
+ else {
774
+ if (r.missing) {
775
+ console.log(`deep layer: not built (run \`${cmd("graft build --deep")}\` for concept nodes); the wiring graph is the source of truth`);
776
+ }
777
+ else {
778
+ console.log(formatCheckReport(r, label));
779
+ }
780
+ if (!g.missing)
781
+ console.log("\n" + formatGraphCheckReport(g));
782
+ }
783
+ if (bothMissing || markdownFail || wiringFail)
784
+ process.exit(1);
785
+ }
786
+ // Under trail this is part of `status`; the old name still runs, unlisted.
787
+ program
788
+ .command("stats", { hidden: TRAIL })
789
+ .description(`Show this agent session's ${brand()}-vs-source usage mix and tokens saved`)
790
+ .argument(...DIR_ARG)
791
+ .option("--json", "output the session stats as JSON")
792
+ .action((dirArg, opts) => {
793
+ // Reads local session JSON only — no graph, no network. This is how a Cursor
794
+ // user (no statusline) sees the numbers the Claude Code bar would show, and it
795
+ // works under DO_NOT_TRACK because it never touches telemetry.
796
+ const dir = queryRoot(dirArg);
797
+ const s = latestSession(dir);
798
+ if (opts.json) {
799
+ console.log(JSON.stringify(s, null, 2));
800
+ return;
801
+ }
802
+ console.log(formatSessionStats(s));
803
+ });
804
+ program
805
+ .command("viz", { hidden: TRAIL })
806
+ .description("Serve an interactive visualization of the context graph (and graph.json when present)")
807
+ .argument(...DIR_ARG)
808
+ .option("-p, --port <port>", "port to serve on", "4400")
809
+ .option("--no-open", "don't open the browser")
810
+ .option("--repos <list...>", "workspace: show only these child repos (space or comma separated, e.g. frontend backend)")
811
+ .option("--skip <list...>", "workspace: exclude these child repos (space or comma separated)")
812
+ .option("--include-tests", "include test files in the graph (excluded by default)")
813
+ .option("--export <dir>", "write one self-contained index.html instead of serving (for CI, GitHub Pages, or a build artifact)")
814
+ .option("--title <text>", "subtitle shown beside the repo name in an exported page (e.g. \"PR #151\")")
815
+ .option("--tabs <list>", "tabs the exported page offers, comma separated: context,code,outline (default: all three)")
816
+ .action(async (dirArg, opts) => {
817
+ // Flags are checked before the repository is: a typo'd `--tabs` is a mistake
818
+ // in the command the caller just typed, and telling them to go build an index
819
+ // first sends them off to fix the wrong thing.
820
+ const tabs = parseTabs(opts.tabs);
821
+ const dir = noteQuery(queryRoot(dirArg));
822
+ const { existsSync } = await import("node:fs");
823
+ const { resolve, basename } = await import("node:path");
824
+ const { spawn } = await import("node:child_process");
825
+ const { fileURLToPath } = await import("node:url");
826
+ const { contextDirFor } = await import("./context/node-file.js");
827
+ const { startVizServer } = await import("./viz/serve.js");
828
+ const root = resolve(dir);
829
+ const globalOpts = program.opts();
830
+ let contextDir = contextDirFor(root, globalOpts.dir);
831
+ let federatedWiringPath = null;
832
+ // Workspace: federate all children's wiring.json into a single in-memory file for viz.
833
+ const { readWorkspace: readWs } = await import('./graph/workspace.js');
834
+ const ws = readWs(root);
835
+ if (ws && ws.children.length > 0) {
836
+ const { join: pj } = await import('node:path');
837
+ const { writeFileSync, mkdirSync, readFileSync: rfs } = await import('node:fs');
838
+ // Apply --only / --exclude filters.
839
+ const onlySet = opts.repos?.length ? new Set(opts.repos.flatMap((s) => s.split(/[,\s]+/)).map((s) => s.trim()).filter(Boolean)) : null;
840
+ const excludeSet = opts.skip?.length ? new Set(opts.skip.flatMap((s) => s.split(/[,\s]+/)).map((s) => s.trim()).filter(Boolean)) : null;
841
+ // [DEBUG removed]
842
+ const children = ws.children.filter(c => {
843
+ const name = c.replace(/\//g, '/'); // keep as-is for matching
844
+ if (onlySet && !onlySet.has(c) && !onlySet.has(c.split('/').pop()))
845
+ return false;
846
+ if (excludeSet && (excludeSet.has(c) || excludeSet.has(c.split('/').pop())))
847
+ return false;
848
+ return true;
849
+ });
850
+ if (onlySet || excludeSet)
851
+ console.error(`· workspace viz: showing ${children.length}/${ws.children.length} repos: ${children.join(', ')}`);
852
+ const allNodes = [];
853
+ const allEdges = [];
854
+ const allLangs = new Set();
855
+ let childrenWithGraph = 0;
856
+ for (const child of children) {
857
+ const childCtx = contextDirFor(pj(root, child));
858
+ const wiringPath = pj(childCtx, '.graph', 'wiring.json');
859
+ if (!existsSync(wiringPath))
860
+ continue;
861
+ try {
862
+ const w = JSON.parse(rfs(wiringPath, 'utf8'));
863
+ if (w?.meta?.version !== 1)
864
+ continue;
865
+ const isTestPath = (p) => /\.(test|spec)\.[jt]sx?$|[\\/](__tests__|tests?|specs?)[\\/]|\.Tests\.cs$/i.test(p);
866
+ const filterNode = (n) => opts.includeTests || !isTestPath(n.path ?? n.id ?? '');
867
+ const filteredNodes = (w.nodes ?? []).filter(filterNode);
868
+ const filteredIds = new Set(filteredNodes.map((n) => `${child}/${n.id}`));
869
+ // Prefix node ids and paths with child name to avoid collisions.
870
+ for (const n of filteredNodes)
871
+ allNodes.push({ ...n, id: `${child}/${n.id}`, path: n.path ? `${child}/${n.path}` : n.path });
872
+ for (const e of (w.edges ?? [])) {
873
+ const src = `${child}/${e.source}`, tgt = `${child}/${e.target}`;
874
+ if (filteredIds.has(src) && filteredIds.has(tgt))
875
+ allEdges.push({ ...e, source: src, target: tgt });
876
+ }
877
+ for (const l of (w.meta?.languages ?? []))
878
+ allLangs.add(l);
879
+ childrenWithGraph++;
880
+ }
881
+ catch { /* skip broken child */ }
882
+ }
883
+ if (childrenWithGraph > 0) {
884
+ const federated = { meta: { version: 1, nodeCount: allNodes.length, edgeCount: allEdges.length, languages: [...allLangs] }, nodes: allNodes, edges: allEdges };
885
+ const tmpDir = pj(contextDir, '.graph');
886
+ mkdirSync(tmpDir, { recursive: true });
887
+ // Write as wiring.json so startVizServer finds it at the expected path (Code tab).
888
+ const tmpPath = pj(tmpDir, 'wiring.json');
889
+ writeFileSync(tmpPath, JSON.stringify(federated));
890
+ federatedWiringPath = tmpPath;
891
+ // Federation of context cards (.md) for the Context tab.
892
+ const { readdirSync, copyFileSync } = await import('node:fs');
893
+ for (const child of children) {
894
+ const childCtx = contextDirFor(pj(root, child));
895
+ if (!existsSync(childCtx))
896
+ continue;
897
+ try {
898
+ for (const f of readdirSync(childCtx)) {
899
+ if (!f.endsWith('.md'))
900
+ continue;
901
+ copyFileSync(pj(childCtx, f), pj(contextDir, `${child.replace(/\//g, '__')}__${f}`));
902
+ }
903
+ }
904
+ catch { /* skip */ }
905
+ }
906
+ console.error(`· workspace viz: federating ${childrenWithGraph} repos (${allNodes.length} nodes, ${allEdges.length} edges)`);
907
+ }
908
+ }
909
+ if (!existsSync(contextDir)) {
910
+ console.error(`✗ no context graph at ${contextDir}: run \`${cmd("graft build --deep")}\` first`);
911
+ process.exit(1);
912
+ }
913
+ const viewerDir = fileURLToPath(new URL("./viewer/", import.meta.url)); // prebuilt
914
+ // Filter tests from wiring.json unless --include-tests (applies to single repo and workspace).
915
+ const isTestPath = (p) => /\.(test|spec)\.[jt]sx?$|[\\/](__tests__|tests?|specs?)[\\/]|\.Tests\.cs$/i.test(p ?? '');
916
+ let wiringOverride;
917
+ if (!opts.includeTests) {
918
+ const { readFileSync: rfs2, writeFileSync: wfs2, mkdirSync: mks2 } = await import('node:fs');
919
+ const { join: pj2 } = await import('node:path');
920
+ const srcWiring = pj2(contextDir, '.graph', 'wiring.json');
921
+ if (existsSync(srcWiring)) {
922
+ try {
923
+ const w = JSON.parse(rfs2(srcWiring, 'utf8'));
924
+ if (w?.meta?.version === 1) {
925
+ const filteredNodes = (w.nodes ?? []).filter((n) => !isTestPath(n.path ?? n.id ?? ''));
926
+ const filteredIds = new Set(filteredNodes.map((n) => n.id));
927
+ const filteredEdges = (w.edges ?? []).filter((e) => filteredIds.has(e.source) && filteredIds.has(e.target));
928
+ const filtered = { ...w, meta: { ...w.meta, nodeCount: filteredNodes.length, edgeCount: filteredEdges.length }, nodes: filteredNodes, edges: filteredEdges };
929
+ const tmpPath = pj2(contextDir, '.graph', 'wiring-viz.json');
930
+ mks2(pj2(contextDir, '.graph'), { recursive: true });
931
+ wfs2(tmpPath, JSON.stringify(filtered));
932
+ wiringOverride = tmpPath;
933
+ const removed = (w.nodes ?? []).length - filteredNodes.length;
934
+ if (removed > 0)
935
+ console.error(`· viz: ${removed} test nodes hidden (use --include-tests to show)`);
936
+ }
937
+ }
938
+ catch { /* skip — serve original */ }
939
+ }
940
+ }
941
+ if (opts.export) {
942
+ const { exportViz } = await import("./viz/export.js");
943
+ const out = exportViz({
944
+ contextDir,
945
+ viewerDir,
946
+ outDir: resolve(opts.export),
947
+ repoName: basename(root),
948
+ subtitle: opts.title,
949
+ tabs,
950
+ });
951
+ const kb = Math.round(out.bytes / 1024);
952
+ console.log(`${cmd("graft viz")} → ${out.file} (${kb} kB, ${out.contextNodes} concept nodes, ${out.codeNodes} code nodes)`);
953
+ return;
954
+ }
955
+ const srv = await startVizServer({
956
+ contextDir,
957
+ viewerDir,
958
+ port: Number(opts.port),
959
+ repoName: basename(root),
960
+ wiringOverride,
961
+ });
962
+ console.log(`${cmd("graft viz")} → ${srv.url} (ctrl-c to stop)`);
963
+ if (opts.open) {
964
+ const opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
965
+ spawn(opener, [srv.url], { stdio: "ignore", detached: true, shell: process.platform === "win32" }).unref();
966
+ }
967
+ });
968
+ program
969
+ .command("mcp", { hidden: TRAIL })
970
+ .description("Serve the graph over MCP (stdio): exposes graft_find_code, graft_trace_calls, graft_find_all, graft_file_api, graft_repo_map, graft_check_freshness and graft_search_semantic as tools")
971
+ .argument(...DIR_ARG)
972
+ .option("--brains", "serve only the paired Trail workspace's tools and skills (what Claude Code gets after init --connect)")
973
+ .action(async (dirArg, opts) => {
974
+ const { startMcpServer } = await import("./mcp/server.js");
975
+ if (opts.brains) {
976
+ startMcpServer(process.cwd(), undefined, currentVersion, { brainsOnly: true });
977
+ return;
978
+ }
979
+ const dir = noteQuery(queryRoot(dirArg));
980
+ const globalOpts = program.opts();
981
+ startMcpServer(dir, globalOpts.dir, currentVersion);
982
+ });
983
+ grouped(program.command("callers"), GROUP.find)
984
+ .description("Who calls/references a symbol ($0, no LLM). --direction out gives callees (what it calls); --depth N (or all) walks transitively for full blast radius")
985
+ .argument("<symbol>", "bare name, qualified (Class.method), or package-qualified (pkg.Fn)")
986
+ .argument(...DIR_ARG)
987
+ .option("--direction <in|out>", 'edge direction: "in" = callers (default), "out" = callees')
988
+ .option("-d, --depth <n>", 'walk transitively up to N hops for blast radius, or "all" for the full connected closure (default 1)')
989
+ .option("--in <path>", "narrow matches to nodes at or under this path prefix")
990
+ .option("--json", "output as JSON")
991
+ .option(...NO_REFRESH_FLAG)
992
+ .action(async (symbol, dirArg, opts) => {
993
+ const dir = noteQuery(queryRoot(dirArg));
994
+ await refreshBefore(dir, opts);
995
+ const globalOpts = program.opts();
996
+ if (!opts.json && readWorkspace(dir, globalOpts.dir)) {
997
+ runWorkspaceCallers(dir, globalOpts.dir, symbol, {
998
+ direction: opts.direction === "out" ? "out" : "in",
999
+ depth: opts.depth
1000
+ ? (/^(all|full|max)$/i.test(opts.depth) ? Number.POSITIVE_INFINITY : Number(opts.depth))
1001
+ : undefined,
1002
+ in: opts.in,
1003
+ });
1004
+ return;
1005
+ }
1006
+ const { runCallersCommand } = await import("./graph/traverse-cli.js");
1007
+ runCallersCommand(symbol, dir, {
1008
+ direction: opts.direction,
1009
+ depth: opts.depth,
1010
+ in: opts.in,
1011
+ json: opts.json,
1012
+ globalDir: globalOpts.dir,
1013
+ });
1014
+ });
1015
+ grouped(program.command("blast"), GROUP.ci)
1016
+ .description("Blast radius of a diff: what depends on the lines this change touched ($0, no LLM). " +
1017
+ "Built for CI: `--format markdown` is a PR comment with a Mermaid diagram.")
1018
+ .argument(...DIR_ARG)
1019
+ .option("--base <ref>", "diff against this ref's merge base with HEAD (e.g. origin/main); default: the working tree vs HEAD")
1020
+ .option("-d, --depth <n>", 'hops to walk over incoming edges, or "all" for the full closure (default 2)')
1021
+ .option("--format <fmt>", "text (default) | markdown | mermaid | json")
1022
+ .option("--name", "name the affected areas with one cached LLM call (needs GRAFT_API_KEY); without it, areas are named after their hub symbol")
1023
+ .option("--export-viz <dir>", "also write the interactive page for this radius (one self-contained index.html, for CI, GitHub Pages, or an artifact)")
1024
+ .option("--title <text>", "subtitle beside the repo name on the exported page (e.g. \"PR #171\")")
1025
+ .option("--no-owners", "do not suggest who to tag (by default, git history names the people behind each affected area)")
1026
+ .option("--pr-author <who...>", "GitHub login, git name or email of the PR author, so they are left out of their own suggestions")
1027
+ .option(...NO_REFRESH_FLAG)
1028
+ .action(async (dirArg, opts) => {
1029
+ const dir = noteQuery(queryRoot(dirArg));
1030
+ await refreshBefore(dir, opts);
1031
+ const { runBlastCommand } = await import("./blast/blast-cli.js");
1032
+ await runBlastCommand(dir, {
1033
+ base: opts.base,
1034
+ depth: opts.depth,
1035
+ format: opts.format,
1036
+ name: opts.name,
1037
+ exportViz: opts.exportViz,
1038
+ title: opts.title,
1039
+ owners: opts.owners,
1040
+ prAuthor: opts.prAuthor,
1041
+ globalDir: program.opts().dir,
1042
+ });
1043
+ });
1044
+ grouped(program.command("grep"), GROUP.find)
1045
+ .description("Regex search over indexed files, hits grouped by enclosing symbol and ranked by coupling ($0, no LLM)")
1046
+ .argument("<pattern>", "regex pattern (or literal string with --fixed)")
1047
+ .argument(...DIR_ARG)
1048
+ .option("-i, --ignore-case", "case-insensitive match")
1049
+ .option("--fixed", "treat pattern as a literal string, not a regex")
1050
+ .option("--in <path>", "narrow to files at or under this path prefix")
1051
+ .option("--json", "output as JSON")
1052
+ .option(...NO_REFRESH_FLAG)
1053
+ .action(async (pattern, dirArg, opts) => {
1054
+ const dir = noteQuery(queryRoot(dirArg));
1055
+ await refreshBefore(dir, opts);
1056
+ const globalOpts = program.opts();
1057
+ if (readWorkspace(dir, globalOpts.dir)) {
1058
+ runWorkspaceGrep(dir, globalOpts.dir, pattern, {
1059
+ ignoreCase: opts.ignoreCase, fixed: opts.fixed, json: opts.json,
1060
+ });
1061
+ return;
1062
+ }
1063
+ const { runGrepCommand } = await import("./search/grep-cli.js");
1064
+ runGrepCommand(pattern, dir, {
1065
+ ignoreCase: opts.ignoreCase,
1066
+ fixed: opts.fixed,
1067
+ in: opts.in,
1068
+ json: opts.json,
1069
+ globalDir: globalOpts.dir,
1070
+ });
1071
+ });
1072
+ grouped(program.command("map"), GROUP.find)
1073
+ .description("Token-budgeted repo orientation: directory clusters, per-directory hubs, and global hotspots from the wiring graph ($0, no LLM)")
1074
+ .argument(...DIR_ARG)
1075
+ .option("--max-dirs <n>", "max directory entries shown, rest counted into dropped (default 16)")
1076
+ .option("--json", "output as JSON")
1077
+ .option(...NO_REFRESH_FLAG)
1078
+ .action(async (dirArg, opts) => {
1079
+ const dir = noteQuery(queryRoot(dirArg));
1080
+ const root = resolve(dir);
1081
+ const globalOpts = program.opts();
1082
+ let maxDirsW;
1083
+ if (opts.maxDirs !== undefined) {
1084
+ const n = parseInt(opts.maxDirs, 10);
1085
+ if (!Number.isFinite(n) || n <= 0) {
1086
+ console.error(`✗ --max-dirs must be a positive integer, got "${opts.maxDirs}"`);
1087
+ process.exit(1);
1088
+ return;
1089
+ }
1090
+ maxDirsW = n;
1091
+ }
1092
+ await refreshBefore(dir, opts); // after arg validation: a bad flag shouldn't cost a rebuild
1093
+ if (!opts.json && readWorkspace(root, globalOpts.dir)) {
1094
+ runWorkspaceMap(root, globalOpts.dir, { maxDirs: maxDirsW });
1095
+ return;
1096
+ }
1097
+ const { buildRepoMap, formatRepoMap } = await import("./graph/map.js");
1098
+ const contextDir = contextDirFor(root, globalOpts.dir);
1099
+ const graph = loadGraphCached(contextDir);
1100
+ if (!graph) {
1101
+ console.error(`✗ no graph: run ${cmd("graft build")} first`);
1102
+ process.exit(1);
1103
+ return;
1104
+ }
1105
+ const map = buildRepoMap(graph, { maxDirs: maxDirsW });
1106
+ if (opts.json) {
1107
+ console.log(JSON.stringify(map, null, 2));
1108
+ return;
1109
+ }
1110
+ process.stdout.write(formatRepoMap(map));
1111
+ });
1112
+ // `trail note`: what a session worked out, saved for the next person. Listed
1113
+ // under trail; under graft it still runs, unlisted, for a teammate on graft in
1114
+ // a repo where someone else set up trail.
1115
+ /** Lines that close a command's output, with a blank line before them. */
1116
+ function printLines(lines) {
1117
+ if (lines.length)
1118
+ console.log(`\n${lines.join("\n")}`);
1119
+ }
1120
+ /** Repo-relative with forward slashes, for printing. */
1121
+ function shown(repo, path) {
1122
+ return relative(repo, path).split("\\").join("/");
1123
+ }
1124
+ /** Agents init knows by another id: both read AGENTS.md. */
1125
+ const AGENT_ALIASES = { codex: "agents", opencode: "agents" };
1126
+ /** `graft init`. */
1127
+ async function runInitCommand(dir, opts, progress) {
1128
+ if (opts.listAgents) {
1129
+ for (const id of [...hostIds(), "claude"])
1130
+ console.log(id);
1131
+ return null;
1132
+ }
1133
+ const repo = resolve(dir);
1134
+ // Codex and OpenCode read AGENTS.md: `--agents codex` means that wiring.
1135
+ // The name as given is kept for Trail's onboarding page.
1136
+ const asked = Array.isArray(opts.agents) ? opts.agents : undefined;
1137
+ // `--agents auto`: the website's one setup prompt, run by an agent with no one to ask.
1138
+ const auto = asked?.includes("auto") === true;
1139
+ if (auto && asked.length > 1) {
1140
+ console.error("✗ --agents auto picks the agents itself: give it on its own, or name the agents instead");
1141
+ process.exit(1);
1142
+ }
1143
+ const explicit = asked && !auto ? [...new Set(asked.map((id) => AGENT_ALIASES[id] ?? id))] : undefined;
1144
+ // Checked before anything is written: run from the home folder, init wired
1145
+ // it, mapped every code file under it, then said to commit.
1146
+ const refusal = homeFolderRefusal(repo);
1147
+ if (refusal) {
1148
+ console.error(refusal);
1149
+ process.exitCode = 1;
1150
+ return null;
1151
+ }
1152
+ // Trail's page shares a repo with the team: removed in trail-solo-grafo.
1153
+ if (explicit) {
1154
+ const validIds = [...hostIds(), "claude"];
1155
+ const unknown = explicit.filter((id) => !validIds.includes(id));
1156
+ if (unknown.length) {
1157
+ console.error(`✗ unknown agent id(s): ${unknown.join(", ")}; valid: ${validIds.join(", ")}`);
1158
+ process.exit(1);
1159
+ }
1160
+ }
1161
+ // Which agents to wire, decided before anything is written. Explicit flags
1162
+ // win; otherwise prompt on a TTY, and on a pipe write nothing rather than
1163
+ // guessing (pre-0.8 this silently wired every agent the machine had ever
1164
+ // installed — see --yes to get that back).
1165
+ const home = homedir();
1166
+ const plan = planInit(repo, { home });
1167
+ const detectedIds = plan.filter((p) => p.detected).map((p) => p.id);
1168
+ const noAgents = opts.agents === false;
1169
+ // auto: the main agents' repo files plus the laptop's; out-of-repo writes only for `laptop`, the agents installed here and the caller.
1170
+ const autoSel = auto ? autoSelection({ home, repo }) : null;
1171
+ const laptop = autoSel?.laptop;
1172
+ let ids;
1173
+ // The consent answer from the picker. Undefined everywhere else — a scripted
1174
+ // or flag-driven init never asked, so it must not silently answer.
1175
+ let consent;
1176
+ if (autoSel)
1177
+ ids = autoSel.repo;
1178
+ else if (explicit)
1179
+ ids = explicit;
1180
+ else if (opts.allAgents)
1181
+ ids = plan.map((p) => p.id);
1182
+ else if (noAgents)
1183
+ ids = ["claude"];
1184
+ else if (opts.yes || opts.dryRun)
1185
+ ids = detectedIds;
1186
+ else if (process.stdin.isTTY && process.stderr.isTTY) {
1187
+ // Only offer the row when telemetry could actually run. `disabled` still
1188
+ // counts: someone who turned it off should be able to turn it back on here.
1189
+ const reason = offReason();
1190
+ const picked = await runPicker(plan, repo, home, {
1191
+ offerTelemetry: reason === null || reason === "disabled",
1192
+ });
1193
+ if (picked === null) {
1194
+ console.error("· cancelled, nothing written");
1195
+ return null;
1196
+ }
1197
+ ids = picked.hosts;
1198
+ consent = picked.telemetry;
1199
+ }
1200
+ else {
1201
+ console.error(formatNonInteractiveHelp(detectedIds));
1202
+ return null;
1203
+ }
1204
+ // The picker's answer, recorded before anything is wired: a user who
1205
+ // unchecked the row must not have this run's init_completed sent.
1206
+ if (consent !== undefined) {
1207
+ patchState({ enabled: consent, noticeShownAt: new Date().toISOString() });
1208
+ }
1209
+ // Knowledge repo prompt removed in trail-solo-grafo.
1210
+ // Workspace parent: every child repo gets its OWN wiring too.
1211
+ // opens at a repo root, not at the parent, and reads `.claude/` from there —
1212
+ // wiring only the parent leaves each child with no skill, hooks, or MCP.
1213
+ // The parent's own wiring stays (queries there federate across children).
1214
+ // Knowledge repo cloning removed in trail-solo-grafo.
1215
+ const children = isWorkspaceBuildRoot(repo, program.opts().dir)
1216
+ ? discoverWorkspaceChildren(repo)
1217
+ : [];
1218
+ // Parent FIRST: its build is the workspace build, which builds every child's
1219
+ // graph, so each child's own `buildGraphIfMissing` then finds one and no-ops.
1220
+ const targets = [repo, ...children.map((c) => join(repo, c))];
1221
+ if (opts.dryRun) {
1222
+ if (autoSel)
1223
+ console.error(`· --agents auto: repo files for Claude Code, Cursor, Codex and OpenCode; laptop settings for ${autoSel.agents.length ? agentList(autoSel.agents) : "none (no agent found here)"}\n`);
1224
+ console.error(formatPlan(laptopOnly(plan, laptop), ids, repo, home));
1225
+ for (const child of children)
1226
+ console.error(`\n· ${child}/ (workspace child)\n` + formatPlan(laptopOnly(planInit(join(repo, child), { home }), laptop), ids, join(repo, child), home));
1227
+ return null;
1228
+ }
1229
+ if (ids.length === 0) {
1230
+ console.error("· no agents selected, nothing written");
1231
+ return null;
1232
+ }
1233
+ const wantClaude = ids.includes("claude");
1234
+ const cliPath = fileURLToPath(import.meta.url);
1235
+ // Say it out loud: wiring a workspace parent writes into every child repo, and
1236
+ // silence here reads as "only the parent was touched".
1237
+ if (children.length > 0)
1238
+ console.error(`· workspace: wiring ${repo} and ${children.length} child repo(s): ${children.join(", ")}`);
1239
+ // Wire each target repo.
1240
+ for (const target of targets) {
1241
+ const targetPlan = target === repo ? plan : planInit(target, { home });
1242
+ const wr = wireTarget(target, ids, {
1243
+ home,
1244
+ cliPath,
1245
+ plan: targetPlan,
1246
+ wantClaude,
1247
+ opts: { build: opts.build, mcp: opts.mcp, global: opts.global },
1248
+ quiet: targets.length > 1,
1249
+ laptop,
1250
+ });
1251
+ for (const w of wr.warnings)
1252
+ console.error(w);
1253
+ }
1254
+ // Build graph (builds all targets in workspace mode).
1255
+ if (opts.build !== false) {
1256
+ const { buildGraphWithProgress: bgp } = await import('./claude/build-progress.js');
1257
+ await bgp(repo, { build: opts.build, cliPath, childCount: children.length });
1258
+ }
1259
+ // teamSaved/LEARNINGS_DIR removed in trail-solo-grafo.
1260
+ // Sorted so `claude,cursor` and `cursor,claude` aggregate as one value.
1261
+ track("init_completed", { agents: [...ids].sort().join(","), consent: consent === undefined ? "unasked" : String(consent) }, { repo });
1262
+ return { ids, laptop: laptop ?? ids };
1263
+ }
1264
+ /** A plan with the out-of-repo writes only for `laptop`'s hosts; the plan itself when every host may write there. */
1265
+ function laptopOnly(plan, laptop) {
1266
+ if (!laptop)
1267
+ return plan;
1268
+ return plan.map((p) => (laptop.includes(p.id) ? p : { ...p, writes: p.writes.filter((w) => w.scope === "repo") }));
1269
+ }
1270
+ /** One repo's worth of `init` writes — the parent, then each workspace child. */
1271
+ function wireTarget(repo, ids, ctx) {
1272
+ const { home, cliPath, plan, wantClaude } = ctx;
1273
+ const quiet = ctx.quiet === true;
1274
+ const opts = quiet ? { ...ctx.opts, build: false } : ctx.opts;
1275
+ const report = { retracted: [], warnings: [] };
1276
+ // Every per-file line goes through here; in quiet mode only ⚠ lines survive.
1277
+ const say = (line) => {
1278
+ if (!quiet)
1279
+ console.error(line);
1280
+ else if (line.startsWith("⚠"))
1281
+ report.warnings.push(line);
1282
+ };
1283
+ const wantStatusline = statuslineWanted();
1284
+ // Converge, don't just add. init writes the selected hosts; without this it
1285
+ // never touches the rest, so a repo wired by an older version (or by the same
1286
+ // version with different --agents) keeps that run's files forever — and
1287
+ // `reconcileWiring` then keeps them *up to date*, which is worse than stale.
1288
+ // Retract every host NOT being written now; `exclude` spares the ones about to
1289
+ // be rewritten, and the graph cache is kept (init is one step from using it).
1290
+ const retracted = changed(runRetract(repo, { home, apply: true, global: opts.global, cache: false, exclude: ids })).filter((r) => r.action !== "skipped-unparseable");
1291
+ for (const r of retracted)
1292
+ say(`- removed ${r.path} (${r.what}): agent not selected`);
1293
+ report.retracted = [...new Set(retracted.map((r) => r.hostId))];
1294
+ if (wantClaude) {
1295
+ // `global`/`home` are threaded through alongside `statusline`: the claude layer
1296
+ // writes under `~/.claude` now (hosts/claude-global.ts), so --no-global has to
1297
+ // reach it or the flag would silently mean "no out-of-repo writes, except three".
1298
+ const claudeGlobal = opts.global !== false && (!ctx.laptop || ctx.laptop.includes("claude"));
1299
+ const res = runInit(repo, { build: opts.build, cliPath, statusline: wantStatusline, global: claudeGlobal, home, brand: brand() });
1300
+ if (res.settingsPath)
1301
+ say(`✓ wrote ${res.settingsPath}`);
1302
+ for (const s of res.shims)
1303
+ say(`✓ wrote ${s}`);
1304
+ if (res.skill)
1305
+ say(`✓ wrote ${res.skill}`);
1306
+ if (!res.mcp) {
1307
+ // trail registers no server for Claude Code: the hooks and the `trail` command cover it.
1308
+ }
1309
+ else if (res.mcp.action === "skipped-unparseable")
1310
+ say(`⚠ .mcp.json: ${res.mcp.path} left unchanged (not valid JSON); add the graft server manually`);
1311
+ else if (res.mcp.action === "unchanged")
1312
+ say(`· mcp claude: ${res.mcp.path} (already registered)`);
1313
+ else
1314
+ say(`✓ mcp claude: ${res.mcp.path} (${res.mcp.action}); restart Claude Code to load the graft MCP server`);
1315
+ say(res.built ? `✓ built the graph (${cmd("graft build")})` : "· skipped graph build");
1316
+ if (!wantStatusline)
1317
+ say("· skipped Claude Code statusLine (GRAFT_NO_STATUSLINE)");
1318
+ for (const w of res.warnings)
1319
+ say(`⚠ ${w}`);
1320
+ }
1321
+ // `ids` is already resolved, so hosts init is always driven by an explicit
1322
+ // list — never by its own detection fallback.
1323
+ // trail-solo-grafo: only 'hermes' is permitted as a non-claude agent;
1324
+ // gemini, antigravity, cursor, codex etc. are excluded (capa eliminada).
1325
+ const others = ids.filter((id) => id === "hermes");
1326
+ if (others.length > 0) {
1327
+ const r = runHostsInit(repo, {
1328
+ agents: others,
1329
+ home,
1330
+ mcp: opts.mcp,
1331
+ global: opts.global,
1332
+ laptop: ctx.laptop,
1333
+ brand: brand(),
1334
+ });
1335
+ for (const w of r.written)
1336
+ say(`✓ ${w.id}: ${w.path} (${w.action})`);
1337
+ for (const m of r.mcp)
1338
+ say(`✓ mcp ${m.id}: ${m.path} (${m.action})`);
1339
+ for (const h of r.hooks)
1340
+ say(`✓ hook ${h.id}: ${h.path} (${h.action})`);
1341
+ // Only worth saying when there was actually something out-of-repo to skip.
1342
+ if (opts.global === false && selectedWrites(plan, ids).some((w) => w.scope === "global"))
1343
+ say("· skipped out-of-repo writes (--no-global)");
1344
+ }
1345
+ // Record WHICH graft wrote this repo's agent files, and under which flags.
1346
+ // Every entry point compares this against the running binary and re-writes
1347
+ // them on a mismatch, so an `npm i -g` upgrade reaches the hooks/skill/rules
1348
+ // too — not just the binary. The flags ride along so a refresh replays the
1349
+ // user's choices (notably --no-global) instead of overriding them.
1350
+ writeStamp(repo, currentVersion, ids, {
1351
+ global: opts.global !== false,
1352
+ mcp: opts.mcp !== false,
1353
+ hooks: false,
1354
+ statusline: wantStatusline,
1355
+ ...(ctx.laptop ? { laptop: ctx.laptop } : {}),
1356
+ });
1357
+ // Every host's wiring points at graft/, so the graph is built whatever was
1358
+ // selected — not only when Claude Code is in the list (runInit does its own).
1359
+ if (!wantClaude) {
1360
+ if (!quiet)
1361
+ say(buildGraphIfMissing(repo, { build: opts.build, cliPath })
1362
+ ? `✓ built the graph (${cmd("graft build")})`
1363
+ : "· skipped graph build");
1364
+ }
1365
+ return report;
1366
+ }
1367
+ /** Group a retraction report by host, so the output reads as "what leaves each agent". */
1368
+ function formatRetractions(rs, apply) {
1369
+ const hit = changed(rs);
1370
+ if (hit.length === 0)
1371
+ return "· nothing to remove: no graft wiring found here";
1372
+ const verb = apply ? "removed" : "would remove";
1373
+ const lines = [];
1374
+ const byHost = new Map();
1375
+ for (const r of hit) {
1376
+ const k = byHost.get(r.hostId) ?? [];
1377
+ k.push(r);
1378
+ byHost.set(r.hostId, k);
1379
+ }
1380
+ for (const [host, items] of byHost) {
1381
+ lines.push(`\n${host}:`);
1382
+ for (const r of items) {
1383
+ const mark = r.action === "skipped-unparseable"
1384
+ ? "⚠"
1385
+ : r.action === "deleted"
1386
+ ? "-"
1387
+ : "~";
1388
+ const note = r.action === "skipped-unparseable"
1389
+ ? ": not valid JSON, left untouched (remove the graft entry by hand)"
1390
+ : r.action === "deleted"
1391
+ ? ` (${r.what}, deleted)`
1392
+ : ` (${r.what})`;
1393
+ const scope = r.scope === "global" ? " [machine-wide]" : "";
1394
+ lines.push(` ${mark} ${verb}: ${r.path}${scope}${note}`);
1395
+ }
1396
+ }
1397
+ return lines.join("\n").replace(/^\n/, "");
1398
+ }
1399
+ grouped(program.command("init"), GROUP.setup)
1400
+ .description(TRAIL
1401
+ ? "Wire trail into your agents: AGENTS.md, .trail/ directory and Hermes skill"
1402
+ : "Wire Graft into the AI coding agents used with this repo")
1403
+ .argument("[dir]", "target repo directory", ".")
1404
+ .option("--agents <ids...>", "agent ids to wire (claude, cursor, codex, opencode, gemini, antigravity, agents)")
1405
+ .option("--all-agents", "wire every supported agent")
1406
+ .option("--list-agents", "print the supported agent ids and exit")
1407
+ .option("--no-build", "skip the graph build (wire only)")
1408
+ .option("--no-mcp", "skip MCP server registration")
1409
+ .option("--yes", "accept all defaults without prompting")
1410
+ .option("--dry-run", "print what would be written and exit")
1411
+ .option("--no-global", "skip out-of-repo writes (~/.claude, etc.)")
1412
+ .option("--verbose", "print the graph-build report when done")
1413
+ .action(async (dir, opts) => {
1414
+ const result = await runInitCommand(dir, opts);
1415
+ if (!result)
1416
+ return;
1417
+ if (opts.dryRun)
1418
+ return;
1419
+ console.error(`· restart your agents so a new session picks up ${brand()}`);
1420
+ if (TRAIL)
1421
+ console.error("· run `trail build` from this folder to update the code graph and semantic index");
1422
+ });
1423
+ grouped(program.command("uninstall"), GROUP.setup)
1424
+ .description(`Remove every file and config entry ${TRAIL ? "trail or graft" : "graft"} has written to this repo (the inverse of init)`)
1425
+ .argument("[dir]", "target repo directory", ".")
1426
+ .option("-y, --yes", "actually remove (without this, prints what it would remove and exits)")
1427
+ .option("--keep-cache", "keep graft/ and the .gitignore entries: wiring only")
1428
+ .option("--no-global", "leave out-of-repo files alone (~/.codex, ~/.gemini)")
1429
+ .action((dir, opts) => {
1430
+ const repo = resolve(dir);
1431
+ const home = homedir();
1432
+ const common = { home, global: opts.global, cache: opts.keepCache ? false : true };
1433
+ if (!opts.yes) {
1434
+ console.error(formatRetractions(planRetract(repo, common), false));
1435
+ console.error("\nDry run: nothing was touched. Re-run with -y to remove.");
1436
+ if (opts.global !== false)
1437
+ console.error("Entries marked [machine-wide] affect every project; --no-global skips them.");
1438
+ return;
1439
+ }
1440
+ const done = runRetract(repo, { ...common, apply: true });
1441
+ console.error(formatRetractions(done, true));
1442
+ const bad = changed(done).filter((r) => r.action === "skipped-unparseable");
1443
+ console.error(bad.length
1444
+ ? `\n⚠ ${bad.length} file(s) could not be parsed and were left as-is: see above.`
1445
+ : `\n✓ ${brand()} fully removed. \`${cmd("graft init")}\` re-wires from scratch.`);
1446
+ });
1447
+ /* -------------------------------------------------------------------------- */
1448
+ /* trail login · logout · workspace */
1449
+ /* -------------------------------------------------------------------------- */
1450
+ /**
1451
+ * `trail login`: sign this laptop in to a Trail workspace through the
1452
+ * browser. No code to copy: the link carries a fresh state, the person signs
1453
+ * in and allows it there, and this laptop claims its own token. In a
1454
+ * terminal it opens the browser and waits. Run by an agent, it prints the
1455
+ * link for the agent's reply and leaves a background wait to finish it, so
1456
+ * the agent never sits blocked. `--workspace` (or the repo's
1457
+ * `.trail/workspace.json`) picks the workspace on that page. A laptop
1458
+ * already signed in links another repo the same way, to the same
1459
+ * workspace or another one: each repo goes to one workspace.
1460
+ *
1461
+ * `--code` skips the browser: Trail's web app made the one-time code for the
1462
+ * person already signed in there, so the laptop trades it for its token
1463
+ * straight away.
1464
+ */
1465
+ function savedThisWeek(repo, now = Date.now()) {
1466
+ const since = now - 7 * 24 * 60 * 60 * 1000;
1467
+ let total = 0;
1468
+ for (const id of listSessionIds(repo)) {
1469
+ try {
1470
+ if (statSync(join(sessionDir(repo), `${id}.json`)).mtimeMs < since)
1471
+ continue;
1472
+ }
1473
+ catch {
1474
+ continue;
1475
+ }
1476
+ total += readSession(repo, id).savedTokens ?? 0;
1477
+ }
1478
+ return total;
1479
+ }
1480
+ /** 212345 → `212k`, 1_250_000 → `1.3M`. */
1481
+ function shortCount(n) {
1482
+ if (n >= 1_000_000)
1483
+ return `${(n / 1_000_000).toFixed(1)}M`;
1484
+ if (n >= 1_000)
1485
+ return `${Math.round(n / 1_000)}k`;
1486
+ return String(n);
1487
+ }
1488
+ /**
1489
+ * `trail status`: what was `graft stats`, on one
1490
+ * screen. The code map and whether it matches the code, the semantic index, the
1491
+ * workspace, and what the agents saved this week.
1492
+ */
1493
+ function statusCommand(name = "status") {
1494
+ return new Command(name)
1495
+ .description("Code map, freshness and tokens saved, on one screen")
1496
+ .argument(...DIR_ARG)
1497
+ .option("--json", "machine-readable output")
1498
+ .action(async (dirArg, opts) => {
1499
+ const repo = queryRoot(dirArg);
1500
+ const globalDir = program.opts().dir;
1501
+ const ws = readWorkspace(repo, globalDir);
1502
+ // ── Code-map: workspace aggregates across children, single-repo reads directly ──
1503
+ let totalNodes = 0;
1504
+ let builtRepos = 0;
1505
+ let totalRepos = 0;
1506
+ let missingRepos = [];
1507
+ let fresh = null;
1508
+ if (ws) {
1509
+ const wg = loadWorkspaceGraphs(repo, globalDir);
1510
+ builtRepos = wg.loaded.length;
1511
+ missingRepos = wg.missing;
1512
+ totalRepos = builtRepos + missingRepos.length;
1513
+ totalNodes = wg.loaded.reduce((s, l) => s + l.graph.meta.nodeCount, 0);
1514
+ // No per-child staleness check in status — treat built children as ok.
1515
+ fresh = builtRepos > 0 ? true : null;
1516
+ }
1517
+ else {
1518
+ const graph = loadGraphCached(contextDirFor(repo, globalDir));
1519
+ const g = graph ? await engineFrom().checkGraph(repo) : null;
1520
+ fresh = g && !g.missing ? g.ok : null;
1521
+ totalRepos = 1;
1522
+ if (graph) {
1523
+ totalNodes = graph.meta.nodeCount;
1524
+ builtRepos = 1;
1525
+ }
1526
+ }
1527
+ // Learnings and cloud sections removed in trail-solo-grafo.
1528
+ const saved = savedThisWeek(repo);
1529
+ // ── Semantic index: iterate children in workspace mode ─────────────────
1530
+ let semanticSymbols = 0;
1531
+ let semanticRepos = 0;
1532
+ {
1533
+ const { existsSync: fsExists } = await import('node:fs');
1534
+ const { join: pj } = await import('node:path');
1535
+ const { contextDirFor: ctxFor } = await import('./context/node-file.js');
1536
+ const { createRequire } = await import('node:module');
1537
+ const req = createRequire(import.meta.url);
1538
+ let Database = null;
1539
+ try {
1540
+ Database = req('better-sqlite3');
1541
+ }
1542
+ catch { /* sqlite not available */ }
1543
+ if (Database) {
1544
+ const ctxDirs = ws
1545
+ ? ws.children.map((c) => ctxFor(join(repo, c)))
1546
+ : [ctxFor(repo, globalDir)];
1547
+ for (const ctxDir of ctxDirs) {
1548
+ const dbPath = pj(ctxDir, '.cache', 'embeddings.db');
1549
+ if (!fsExists(dbPath))
1550
+ continue;
1551
+ try {
1552
+ const db = Database(dbPath, { readonly: true });
1553
+ const { c } = db.prepare('SELECT COUNT(*) AS c FROM embed_nodes').get();
1554
+ db.close();
1555
+ semanticSymbols += c;
1556
+ semanticRepos++;
1557
+ }
1558
+ catch { /* DB not readable — ignore */ }
1559
+ }
1560
+ }
1561
+ }
1562
+ // ── Output ─────────────────────────────────────────────────────────────
1563
+ if (opts.json) {
1564
+ console.log(JSON.stringify({
1565
+ codeMap: builtRepos > 0 ? { nodes: totalNodes, repos: ws ? { built: builtRepos, total: totalRepos } : undefined, inSync: fresh } : null,
1566
+ week: { savedTokens: saved },
1567
+ semantic: semanticRepos > 0 ? { symbols: semanticSymbols, repos: semanticRepos, total: totalRepos } : null,
1568
+ }, null, 2));
1569
+ return;
1570
+ }
1571
+ const fmt = (x) => x.toLocaleString("en-US");
1572
+ const row = (k, v) => console.log(`${k.padEnd(11)} ${v}`);
1573
+ // Code-map row
1574
+ if (builtRepos === 0) {
1575
+ row("code map", `✗ not built · run ${cmd("trail build")}`);
1576
+ }
1577
+ else if (ws) {
1578
+ const repoLabel = `${builtRepos} repo${builtRepos === 1 ? "" : "s"}`;
1579
+ if (missingRepos.length > 0) {
1580
+ row("code map", `⚠ ${repoLabel} of ${totalRepos} built · ${fmt(totalNodes)} nodes · ${cmd("trail build")} to cover ${missingRepos.join(", ")}`);
1581
+ }
1582
+ else {
1583
+ row("code map", `✓ ${repoLabel} · ${fmt(totalNodes)} nodes`);
1584
+ }
1585
+ }
1586
+ else if (fresh === false) {
1587
+ row("code map", `⚠ ${fmt(totalNodes)} nodes · behind the code, refreshed on the next query`);
1588
+ }
1589
+ else {
1590
+ row("code map", `✓ ${fmt(totalNodes)} nodes · in sync with the code`);
1591
+ }
1592
+ row("this week", saved > 0 ? `~${shortCount(saved)} tokens saved` : "nothing saved yet");
1593
+ // Semantic row
1594
+ if (semanticRepos === 0) {
1595
+ row("semantic", `✗ not built · run \`trail build\` to enable vector search`);
1596
+ }
1597
+ else if (ws) {
1598
+ row("semantic", `✓ ${fmt(semanticSymbols)} symbols · ${semanticRepos} of ${totalRepos} repos indexed`);
1599
+ }
1600
+ else {
1601
+ row("semantic", `✓ ${fmt(semanticSymbols)} symbols indexed · run \`trail build\` to refresh`);
1602
+ }
1603
+ });
1604
+ }
1605
+ if (TRAIL) {
1606
+ program.addCommand(statusCommand().helpGroup(GROUP.setup));
1607
+ // Help lists the groups in this order, and the commands within each.
1608
+ const order = ["ask", "grep", "skeleton", "callers", "map", "note", "check", "team", "skills", "init", "build", "status", "upgrade", "uninstall", "telemetry", "workspace", "login", "logout", "blast"];
1609
+ const rank = (c) => {
1610
+ const i = order.indexOf(c.name());
1611
+ return i === -1 ? order.length : i;
1612
+ };
1613
+ program.commands.sort((a, b) => rank(a) - rank(b));
1614
+ // One line each in the list; `trail <command> --help` keeps the long text.
1615
+ const summaries = {
1616
+ ask: "ranked answer, with the code inlined at each file:line",
1617
+ grep: "every occurrence, grouped by enclosing symbol",
1618
+ skeleton: "a file's whole API in ~200 tokens",
1619
+ callers: "who calls it, or what it calls with --direction out",
1620
+ map: "directory clusters, hubs and hotspots",
1621
+ init: "wire trail into your agents",
1622
+ build: "rebuild the code map · --check fails if it's stale",
1623
+ status: "code map and semantic index",
1624
+ upgrade: "install the latest trail",
1625
+ uninstall: "remove everything trail or graft wrote to this repo",
1626
+ telemetry: "show or turn off the anonymous usage stats",
1627
+ blast: "what depends on the lines this diff touched",
1628
+ };
1629
+ for (const c of program.commands)
1630
+ if (summaries[c.name()])
1631
+ c.summary(summaries[c.name()]);
1632
+ program.addHelpText("after", "\ntrail <command> --help for flags · the same command set runs as `graft`");
1633
+ }
1634
+ program.parseAsync(process.argv).catch(async (err) => {
1635
+ console.error(err instanceof Error ? err.message : err);
1636
+ process.exit(1);
1637
+ });
1638
+ //# sourceMappingURL=cli.js.map