@cstart/coldstart 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (360) hide show
  1. package/README.md +236 -0
  2. package/dist/cache/disk-cache.d.ts +14 -0
  3. package/dist/cache/disk-cache.d.ts.map +1 -0
  4. package/dist/cache/disk-cache.js +371 -0
  5. package/dist/cache/disk-cache.js.map +1 -0
  6. package/dist/cli.d.ts +18 -0
  7. package/dist/cli.d.ts.map +1 -0
  8. package/dist/cli.js +206 -0
  9. package/dist/cli.js.map +1 -0
  10. package/dist/constants.d.ts +8 -0
  11. package/dist/constants.d.ts.map +1 -0
  12. package/dist/constants.js +111 -0
  13. package/dist/constants.js.map +1 -0
  14. package/dist/daemon-lock.d.ts +63 -0
  15. package/dist/daemon-lock.d.ts.map +1 -0
  16. package/dist/daemon-lock.js +218 -0
  17. package/dist/daemon-lock.js.map +1 -0
  18. package/dist/daemon-log.d.ts +26 -0
  19. package/dist/daemon-log.d.ts.map +1 -0
  20. package/dist/daemon-log.js +110 -0
  21. package/dist/daemon-log.js.map +1 -0
  22. package/dist/index-manager.d.ts +103 -0
  23. package/dist/index-manager.d.ts.map +1 -0
  24. package/dist/index-manager.js +361 -0
  25. package/dist/index-manager.js.map +1 -0
  26. package/dist/index.d.ts +4 -0
  27. package/dist/index.d.ts.map +1 -0
  28. package/dist/index.js +724 -0
  29. package/dist/index.js.map +1 -0
  30. package/dist/indexer/content-tokens.d.ts +74 -0
  31. package/dist/indexer/content-tokens.d.ts.map +1 -0
  32. package/dist/indexer/content-tokens.js +406 -0
  33. package/dist/indexer/content-tokens.js.map +1 -0
  34. package/dist/indexer/cpp-include-roots.d.ts +7 -0
  35. package/dist/indexer/cpp-include-roots.d.ts.map +1 -0
  36. package/dist/indexer/cpp-include-roots.js +166 -0
  37. package/dist/indexer/cpp-include-roots.js.map +1 -0
  38. package/dist/indexer/csharp-synthetic.d.ts +9 -0
  39. package/dist/indexer/csharp-synthetic.d.ts.map +1 -0
  40. package/dist/indexer/csharp-synthetic.js +51 -0
  41. package/dist/indexer/csharp-synthetic.js.map +1 -0
  42. package/dist/indexer/django-synthetic.d.ts +12 -0
  43. package/dist/indexer/django-synthetic.d.ts.map +1 -0
  44. package/dist/indexer/django-synthetic.js +49 -0
  45. package/dist/indexer/django-synthetic.js.map +1 -0
  46. package/dist/indexer/extractors/angularjs.d.ts +16 -0
  47. package/dist/indexer/extractors/angularjs.d.ts.map +1 -0
  48. package/dist/indexer/extractors/angularjs.js +49 -0
  49. package/dist/indexer/extractors/angularjs.js.map +1 -0
  50. package/dist/indexer/extractors/cpp.d.ts +9 -0
  51. package/dist/indexer/extractors/cpp.d.ts.map +1 -0
  52. package/dist/indexer/extractors/cpp.js +254 -0
  53. package/dist/indexer/extractors/cpp.js.map +1 -0
  54. package/dist/indexer/extractors/csharp.d.ts +19 -0
  55. package/dist/indexer/extractors/csharp.d.ts.map +1 -0
  56. package/dist/indexer/extractors/csharp.js +327 -0
  57. package/dist/indexer/extractors/csharp.js.map +1 -0
  58. package/dist/indexer/extractors/env.d.ts +9 -0
  59. package/dist/indexer/extractors/env.d.ts.map +1 -0
  60. package/dist/indexer/extractors/env.js +43 -0
  61. package/dist/indexer/extractors/env.js.map +1 -0
  62. package/dist/indexer/extractors/go.d.ts +9 -0
  63. package/dist/indexer/extractors/go.d.ts.map +1 -0
  64. package/dist/indexer/extractors/go.js +227 -0
  65. package/dist/indexer/extractors/go.js.map +1 -0
  66. package/dist/indexer/extractors/graphql.d.ts +22 -0
  67. package/dist/indexer/extractors/graphql.d.ts.map +1 -0
  68. package/dist/indexer/extractors/graphql.js +88 -0
  69. package/dist/indexer/extractors/graphql.js.map +1 -0
  70. package/dist/indexer/extractors/groovy.d.ts +9 -0
  71. package/dist/indexer/extractors/groovy.d.ts.map +1 -0
  72. package/dist/indexer/extractors/groovy.js +356 -0
  73. package/dist/indexer/extractors/groovy.js.map +1 -0
  74. package/dist/indexer/extractors/java.d.ts +10 -0
  75. package/dist/indexer/extractors/java.d.ts.map +1 -0
  76. package/dist/indexer/extractors/java.js +586 -0
  77. package/dist/indexer/extractors/java.js.map +1 -0
  78. package/dist/indexer/extractors/kotlin.d.ts +10 -0
  79. package/dist/indexer/extractors/kotlin.d.ts.map +1 -0
  80. package/dist/indexer/extractors/kotlin.js +398 -0
  81. package/dist/indexer/extractors/kotlin.js.map +1 -0
  82. package/dist/indexer/extractors/node-helpers.d.ts +13 -0
  83. package/dist/indexer/extractors/node-helpers.d.ts.map +1 -0
  84. package/dist/indexer/extractors/node-helpers.js +17 -0
  85. package/dist/indexer/extractors/node-helpers.js.map +1 -0
  86. package/dist/indexer/extractors/parser-factory.d.ts +4 -0
  87. package/dist/indexer/extractors/parser-factory.d.ts.map +1 -0
  88. package/dist/indexer/extractors/parser-factory.js +28 -0
  89. package/dist/indexer/extractors/parser-factory.js.map +1 -0
  90. package/dist/indexer/extractors/php.d.ts +17 -0
  91. package/dist/indexer/extractors/php.d.ts.map +1 -0
  92. package/dist/indexer/extractors/php.js +363 -0
  93. package/dist/indexer/extractors/php.js.map +1 -0
  94. package/dist/indexer/extractors/python.d.ts +14 -0
  95. package/dist/indexer/extractors/python.d.ts.map +1 -0
  96. package/dist/indexer/extractors/python.js +449 -0
  97. package/dist/indexer/extractors/python.js.map +1 -0
  98. package/dist/indexer/extractors/ruby.d.ts +10 -0
  99. package/dist/indexer/extractors/ruby.d.ts.map +1 -0
  100. package/dist/indexer/extractors/ruby.js +996 -0
  101. package/dist/indexer/extractors/ruby.js.map +1 -0
  102. package/dist/indexer/extractors/rust.d.ts +9 -0
  103. package/dist/indexer/extractors/rust.d.ts.map +1 -0
  104. package/dist/indexer/extractors/rust.js +322 -0
  105. package/dist/indexer/extractors/rust.js.map +1 -0
  106. package/dist/indexer/extractors/toml.d.ts +9 -0
  107. package/dist/indexer/extractors/toml.d.ts.map +1 -0
  108. package/dist/indexer/extractors/toml.js +84 -0
  109. package/dist/indexer/extractors/toml.js.map +1 -0
  110. package/dist/indexer/extractors/xml.d.ts +9 -0
  111. package/dist/indexer/extractors/xml.d.ts.map +1 -0
  112. package/dist/indexer/extractors/xml.js +163 -0
  113. package/dist/indexer/extractors/xml.js.map +1 -0
  114. package/dist/indexer/extractors/yaml.d.ts +9 -0
  115. package/dist/indexer/extractors/yaml.d.ts.map +1 -0
  116. package/dist/indexer/extractors/yaml.js +117 -0
  117. package/dist/indexer/extractors/yaml.js.map +1 -0
  118. package/dist/indexer/git.d.ts +19 -0
  119. package/dist/indexer/git.d.ts.map +1 -0
  120. package/dist/indexer/git.js +55 -0
  121. package/dist/indexer/git.js.map +1 -0
  122. package/dist/indexer/graph.d.ts +7 -0
  123. package/dist/indexer/graph.d.ts.map +1 -0
  124. package/dist/indexer/graph.js +20 -0
  125. package/dist/indexer/graph.js.map +1 -0
  126. package/dist/indexer/indexed-file.d.ts +15 -0
  127. package/dist/indexer/indexed-file.d.ts.map +1 -0
  128. package/dist/indexer/indexed-file.js +50 -0
  129. package/dist/indexer/indexed-file.js.map +1 -0
  130. package/dist/indexer/invariants.d.ts +15 -0
  131. package/dist/indexer/invariants.d.ts.map +1 -0
  132. package/dist/indexer/invariants.js +96 -0
  133. package/dist/indexer/invariants.js.map +1 -0
  134. package/dist/indexer/laravel-synthetic.d.ts +10 -0
  135. package/dist/indexer/laravel-synthetic.d.ts.map +1 -0
  136. package/dist/indexer/laravel-synthetic.js +78 -0
  137. package/dist/indexer/laravel-synthetic.js.map +1 -0
  138. package/dist/indexer/parser.d.ts +4 -0
  139. package/dist/indexer/parser.d.ts.map +1 -0
  140. package/dist/indexer/parser.js +499 -0
  141. package/dist/indexer/parser.js.map +1 -0
  142. package/dist/indexer/patch.d.ts +3 -0
  143. package/dist/indexer/patch.d.ts.map +1 -0
  144. package/dist/indexer/patch.js +291 -0
  145. package/dist/indexer/patch.js.map +1 -0
  146. package/dist/indexer/rails-synthetic.d.ts +11 -0
  147. package/dist/indexer/rails-synthetic.d.ts.map +1 -0
  148. package/dist/indexer/rails-synthetic.js +80 -0
  149. package/dist/indexer/rails-synthetic.js.map +1 -0
  150. package/dist/indexer/reconcile.d.ts +9 -0
  151. package/dist/indexer/reconcile.d.ts.map +1 -0
  152. package/dist/indexer/reconcile.js +96 -0
  153. package/dist/indexer/reconcile.js.map +1 -0
  154. package/dist/indexer/resolvers/cpp.d.ts +15 -0
  155. package/dist/indexer/resolvers/cpp.d.ts.map +1 -0
  156. package/dist/indexer/resolvers/cpp.js +35 -0
  157. package/dist/indexer/resolvers/cpp.js.map +1 -0
  158. package/dist/indexer/resolvers/csharp.d.ts +2 -0
  159. package/dist/indexer/resolvers/csharp.d.ts.map +1 -0
  160. package/dist/indexer/resolvers/csharp.js +89 -0
  161. package/dist/indexer/resolvers/csharp.js.map +1 -0
  162. package/dist/indexer/resolvers/generic.d.ts +10 -0
  163. package/dist/indexer/resolvers/generic.d.ts.map +1 -0
  164. package/dist/indexer/resolvers/generic.js +35 -0
  165. package/dist/indexer/resolvers/generic.js.map +1 -0
  166. package/dist/indexer/resolvers/go.d.ts +2 -0
  167. package/dist/indexer/resolvers/go.d.ts.map +1 -0
  168. package/dist/indexer/resolvers/go.js +171 -0
  169. package/dist/indexer/resolvers/go.js.map +1 -0
  170. package/dist/indexer/resolvers/index.d.ts +26 -0
  171. package/dist/indexer/resolvers/index.d.ts.map +1 -0
  172. package/dist/indexer/resolvers/index.js +301 -0
  173. package/dist/indexer/resolvers/index.js.map +1 -0
  174. package/dist/indexer/resolvers/java.d.ts +17 -0
  175. package/dist/indexer/resolvers/java.d.ts.map +1 -0
  176. package/dist/indexer/resolvers/java.js +104 -0
  177. package/dist/indexer/resolvers/java.js.map +1 -0
  178. package/dist/indexer/resolvers/php.d.ts +2 -0
  179. package/dist/indexer/resolvers/php.d.ts.map +1 -0
  180. package/dist/indexer/resolvers/php.js +108 -0
  181. package/dist/indexer/resolvers/php.js.map +1 -0
  182. package/dist/indexer/resolvers/python.d.ts +17 -0
  183. package/dist/indexer/resolvers/python.d.ts.map +1 -0
  184. package/dist/indexer/resolvers/python.js +63 -0
  185. package/dist/indexer/resolvers/python.js.map +1 -0
  186. package/dist/indexer/resolvers/ruby.d.ts +13 -0
  187. package/dist/indexer/resolvers/ruby.d.ts.map +1 -0
  188. package/dist/indexer/resolvers/ruby.js +199 -0
  189. package/dist/indexer/resolvers/ruby.js.map +1 -0
  190. package/dist/indexer/resolvers/rust.d.ts +12 -0
  191. package/dist/indexer/resolvers/rust.d.ts.map +1 -0
  192. package/dist/indexer/resolvers/rust.js +61 -0
  193. package/dist/indexer/resolvers/rust.js.map +1 -0
  194. package/dist/indexer/resolvers/shared.d.ts +18 -0
  195. package/dist/indexer/resolvers/shared.d.ts.map +1 -0
  196. package/dist/indexer/resolvers/shared.js +71 -0
  197. package/dist/indexer/resolvers/shared.js.map +1 -0
  198. package/dist/indexer/rust-workspace.d.ts +25 -0
  199. package/dist/indexer/rust-workspace.d.ts.map +1 -0
  200. package/dist/indexer/rust-workspace.js +155 -0
  201. package/dist/indexer/rust-workspace.js.map +1 -0
  202. package/dist/indexer/symbol-edges.d.ts +19 -0
  203. package/dist/indexer/symbol-edges.d.ts.map +1 -0
  204. package/dist/indexer/symbol-edges.js +117 -0
  205. package/dist/indexer/symbol-edges.js.map +1 -0
  206. package/dist/indexer/tokenize.d.ts +26 -0
  207. package/dist/indexer/tokenize.d.ts.map +1 -0
  208. package/dist/indexer/tokenize.js +200 -0
  209. package/dist/indexer/tokenize.js.map +1 -0
  210. package/dist/indexer/ts-parser.d.ts +10 -0
  211. package/dist/indexer/ts-parser.d.ts.map +1 -0
  212. package/dist/indexer/ts-parser.js +605 -0
  213. package/dist/indexer/ts-parser.js.map +1 -0
  214. package/dist/indexer/walker.d.ts +9 -0
  215. package/dist/indexer/walker.d.ts.map +1 -0
  216. package/dist/indexer/walker.js +91 -0
  217. package/dist/indexer/walker.js.map +1 -0
  218. package/dist/init.d.ts +111 -0
  219. package/dist/init.d.ts.map +1 -0
  220. package/dist/init.js +787 -0
  221. package/dist/init.js.map +1 -0
  222. package/dist/kb/cli.d.ts +2 -0
  223. package/dist/kb/cli.d.ts.map +1 -0
  224. package/dist/kb/cli.js +377 -0
  225. package/dist/kb/cli.js.map +1 -0
  226. package/dist/kb/commit.d.ts +6 -0
  227. package/dist/kb/commit.d.ts.map +1 -0
  228. package/dist/kb/commit.js +84 -0
  229. package/dist/kb/commit.js.map +1 -0
  230. package/dist/kb/fold.d.ts +42 -0
  231. package/dist/kb/fold.d.ts.map +1 -0
  232. package/dist/kb/fold.js +285 -0
  233. package/dist/kb/fold.js.map +1 -0
  234. package/dist/kb/freshness.d.ts +9 -0
  235. package/dist/kb/freshness.d.ts.map +1 -0
  236. package/dist/kb/freshness.js +52 -0
  237. package/dist/kb/freshness.js.map +1 -0
  238. package/dist/kb/git.d.ts +4 -0
  239. package/dist/kb/git.d.ts.map +1 -0
  240. package/dist/kb/git.js +28 -0
  241. package/dist/kb/git.js.map +1 -0
  242. package/dist/kb/ids.d.ts +8 -0
  243. package/dist/kb/ids.d.ts.map +1 -0
  244. package/dist/kb/ids.js +42 -0
  245. package/dist/kb/ids.js.map +1 -0
  246. package/dist/kb/lint.d.ts +23 -0
  247. package/dist/kb/lint.d.ts.map +1 -0
  248. package/dist/kb/lint.js +102 -0
  249. package/dist/kb/lint.js.map +1 -0
  250. package/dist/kb/lookup.d.ts +55 -0
  251. package/dist/kb/lookup.d.ts.map +1 -0
  252. package/dist/kb/lookup.js +92 -0
  253. package/dist/kb/lookup.js.map +1 -0
  254. package/dist/kb/notes-index.d.ts +31 -0
  255. package/dist/kb/notes-index.d.ts.map +1 -0
  256. package/dist/kb/notes-index.js +0 -0
  257. package/dist/kb/notes-index.js.map +1 -0
  258. package/dist/kb/raw-log.d.ts +34 -0
  259. package/dist/kb/raw-log.d.ts.map +1 -0
  260. package/dist/kb/raw-log.js +106 -0
  261. package/dist/kb/raw-log.js.map +1 -0
  262. package/dist/kb/render.d.ts +17 -0
  263. package/dist/kb/render.d.ts.map +1 -0
  264. package/dist/kb/render.js +101 -0
  265. package/dist/kb/render.js.map +1 -0
  266. package/dist/kb/search.d.ts +110 -0
  267. package/dist/kb/search.d.ts.map +1 -0
  268. package/dist/kb/search.js +453 -0
  269. package/dist/kb/search.js.map +1 -0
  270. package/dist/kb/store.d.ts +35 -0
  271. package/dist/kb/store.d.ts.map +1 -0
  272. package/dist/kb/store.js +160 -0
  273. package/dist/kb/store.js.map +1 -0
  274. package/dist/kb/types.d.ts +142 -0
  275. package/dist/kb/types.d.ts.map +1 -0
  276. package/dist/kb/types.js +13 -0
  277. package/dist/kb/types.js.map +1 -0
  278. package/dist/kb/view-template.d.ts +8 -0
  279. package/dist/kb/view-template.d.ts.map +1 -0
  280. package/dist/kb/view-template.js +8 -0
  281. package/dist/kb/view-template.js.map +1 -0
  282. package/dist/kb/view.d.ts +42 -0
  283. package/dist/kb/view.d.ts.map +1 -0
  284. package/dist/kb/view.js +118 -0
  285. package/dist/kb/view.js.map +1 -0
  286. package/dist/kb/write.d.ts +72 -0
  287. package/dist/kb/write.d.ts.map +1 -0
  288. package/dist/kb/write.js +345 -0
  289. package/dist/kb/write.js.map +1 -0
  290. package/dist/keeper-state.d.ts +29 -0
  291. package/dist/keeper-state.d.ts.map +1 -0
  292. package/dist/keeper-state.js +89 -0
  293. package/dist/keeper-state.js.map +1 -0
  294. package/dist/keeper.d.ts +23 -0
  295. package/dist/keeper.d.ts.map +1 -0
  296. package/dist/keeper.js +105 -0
  297. package/dist/keeper.js.map +1 -0
  298. package/dist/migrate.d.ts +9 -0
  299. package/dist/migrate.d.ts.map +1 -0
  300. package/dist/migrate.js +102 -0
  301. package/dist/migrate.js.map +1 -0
  302. package/dist/restart.d.ts +15 -0
  303. package/dist/restart.d.ts.map +1 -0
  304. package/dist/restart.js +48 -0
  305. package/dist/restart.js.map +1 -0
  306. package/dist/server/find.d.ts +39 -0
  307. package/dist/server/find.d.ts.map +1 -0
  308. package/dist/server/find.js +979 -0
  309. package/dist/server/find.js.map +1 -0
  310. package/dist/server/mcp.d.ts +117 -0
  311. package/dist/server/mcp.d.ts.map +1 -0
  312. package/dist/server/mcp.js +301 -0
  313. package/dist/server/mcp.js.map +1 -0
  314. package/dist/server/searcher.d.ts +14 -0
  315. package/dist/server/searcher.d.ts.map +1 -0
  316. package/dist/server/searcher.js +146 -0
  317. package/dist/server/searcher.js.map +1 -0
  318. package/dist/server/tools.d.ts +17 -0
  319. package/dist/server/tools.d.ts.map +1 -0
  320. package/dist/server/tools.js +875 -0
  321. package/dist/server/tools.js.map +1 -0
  322. package/dist/status.d.ts +12 -0
  323. package/dist/status.d.ts.map +1 -0
  324. package/dist/status.js +157 -0
  325. package/dist/status.js.map +1 -0
  326. package/dist/types.d.ts +138 -0
  327. package/dist/types.d.ts.map +1 -0
  328. package/dist/types.js +3 -0
  329. package/dist/types.js.map +1 -0
  330. package/dist/watcher.d.ts +11 -0
  331. package/dist/watcher.d.ts.map +1 -0
  332. package/dist/watcher.js +67 -0
  333. package/dist/watcher.js.map +1 -0
  334. package/hooks/canonical-find-key.mjs +68 -0
  335. package/hooks/codex-find-nudge.mjs +15 -0
  336. package/hooks/codex-find-preguard.mjs +14 -0
  337. package/hooks/codex-kb-elicit.mjs +329 -0
  338. package/hooks/codex-kb-recall.mjs +151 -0
  339. package/hooks/codex-nudge-handler.mjs +370 -0
  340. package/hooks/codex-preguard-handler.mjs +75 -0
  341. package/hooks/codex-run-hook.mjs +132 -0
  342. package/hooks/coldstart-call.mjs +58 -0
  343. package/hooks/cursor-find-nudge.mjs +15 -0
  344. package/hooks/cursor-find-preguard.mjs +14 -0
  345. package/hooks/cursor-input.mjs +72 -0
  346. package/hooks/cursor-kb-elicit.mjs +335 -0
  347. package/hooks/cursor-kb-recall.mjs +138 -0
  348. package/hooks/cursor-nudge-handler.mjs +33 -0
  349. package/hooks/cursor-preguard-handler.mjs +39 -0
  350. package/hooks/cursor-run-hook.mjs +129 -0
  351. package/hooks/find-nudge.mjs +15 -0
  352. package/hooks/find-preguard.mjs +14 -0
  353. package/hooks/kb-elicit.mjs +334 -0
  354. package/hooks/kb-recall.mjs +151 -0
  355. package/hooks/nudge-handler.mjs +370 -0
  356. package/hooks/preguard-handler.mjs +75 -0
  357. package/hooks/run-hook.mjs +132 -0
  358. package/package.json +69 -0
  359. package/templates/coldstart.cli.md +60 -0
  360. package/templates/coldstart.mcp.md +59 -0
package/dist/index.js ADDED
@@ -0,0 +1,724 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * coldstart — fast static codebase navigation for AI agents (CLI + MCP).
4
+ *
5
+ * Default invocation = stdio MCP reader over the keeper's cache:
6
+ * coldstart --root /path/to/project
7
+ * coldstart --root . --exclude vendor --quiet
8
+ * coldstart --root . --no-cache
9
+ * coldstart --root . --no-daemon # self-contained stdio MCP (no keeper)
10
+ *
11
+ * Subcommands:
12
+ * coldstart init # wire coldstart.md into the project
13
+ * coldstart find / gs # CLI readers (load the cache, print, exit)
14
+ * coldstart status # list every keeper + its index freshness
15
+ * coldstart restart [--all] # stop the keeper(s); respawn on next read
16
+ *
17
+ * Internal (spawned automatically by readers):
18
+ * coldstart --root . --daemon # background keeper (keeps cache fresh)
19
+ */
20
+ import { resolve, join } from 'node:path';
21
+ import { fileURLToPath } from 'node:url';
22
+ import { statSync } from 'node:fs';
23
+ import { walkDirectory } from './indexer/walker.js';
24
+ import { parseFile, buildFileId } from './indexer/parser.js';
25
+ import { resolveImports } from './indexer/resolvers/index.js';
26
+ import { addRailsSyntheticEdges } from './indexer/rails-synthetic.js';
27
+ import { addLaravelSyntheticEdges } from './indexer/laravel-synthetic.js';
28
+ import { addCSharpSyntheticEdges } from './indexer/csharp-synthetic.js';
29
+ import { addDjangoSyntheticEdges } from './indexer/django-synthetic.js';
30
+ import { buildGraph } from './indexer/graph.js';
31
+ import { buildFileDomains, isTestPath } from './indexer/tokenize.js';
32
+ import { baseIndexedFile } from './indexer/indexed-file.js';
33
+ import { buildContentTokenPostings } from './indexer/content-tokens.js';
34
+ import { buildSymbolEdges } from './indexer/symbol-edges.js';
35
+ import { getGitHead } from './indexer/git.js';
36
+ import { reconcileChanges } from './indexer/reconcile.js';
37
+ import { patchIndex } from './indexer/patch.js';
38
+ import { lintIndexInvariants } from './indexer/invariants.js';
39
+ import { updateKeeperState, appendRepairLog } from './keeper-state.js';
40
+ import { patchThreshold } from './constants.js';
41
+ import { loadCachedIndex, saveCachedIndex, getCacheDir, updateCachedGitHead } from './cache/disk-cache.js';
42
+ import { startMCPServer } from './server/mcp.js';
43
+ import { IndexManager } from './index-manager.js';
44
+ import { readLock, writeLock, deleteLock, isDaemonAlive, getCurrentVersion, watchOwnLockfile } from './daemon-lock.js';
45
+ import { ensureKeeper, waitForKeeperCache } from './keeper.js';
46
+ import { attachDaemonLogger } from './daemon-log.js';
47
+ import { migrateLegacyMcpConfig } from './migrate.js';
48
+ // ---------------------------------------------------------------------------
49
+ // Argument parsing
50
+ // ---------------------------------------------------------------------------
51
+ function parseArgs(argv) {
52
+ const args = argv.slice(2);
53
+ let root = '.';
54
+ let rootExplicit = false;
55
+ const excludes = [];
56
+ const includes = [];
57
+ let cacheDir;
58
+ let quiet = false;
59
+ let noCache = false;
60
+ let daemon = false;
61
+ let noDaemon = false;
62
+ let probe = false;
63
+ for (let i = 0; i < args.length; i++) {
64
+ const arg = args[i];
65
+ switch (arg) {
66
+ case '--root':
67
+ root = args[++i] ?? root;
68
+ rootExplicit = true;
69
+ break;
70
+ case '--exclude':
71
+ excludes.push(args[++i] ?? '');
72
+ break;
73
+ case '--include':
74
+ includes.push(args[++i] ?? '');
75
+ break;
76
+ case '--cache-dir':
77
+ cacheDir = args[++i];
78
+ break;
79
+ case '--quiet':
80
+ quiet = true;
81
+ break;
82
+ case '--no-cache':
83
+ noCache = true;
84
+ break;
85
+ case '--daemon':
86
+ daemon = true;
87
+ break;
88
+ case '--no-daemon':
89
+ noDaemon = true;
90
+ break;
91
+ case '--probe':
92
+ probe = true;
93
+ break;
94
+ }
95
+ }
96
+ return { root, rootExplicit, excludes, includes, cacheDir, quiet, noCache, daemon, noDaemon, probe };
97
+ }
98
+ // ---------------------------------------------------------------------------
99
+ // Logging
100
+ // ---------------------------------------------------------------------------
101
+ function log(quiet, ...args) {
102
+ if (!quiet)
103
+ process.stderr.write(args.join(' ') + '\n');
104
+ }
105
+ // MCP clients advertise their workspace as a `file://` URI (or sometimes a bare
106
+ // path). Resolve the first advertised root to an absolute filesystem path,
107
+ // falling back to the CLI-provided root when the client advertises none.
108
+ function resolveClientRoot(clientRoots, fallback) {
109
+ if (clientRoots.length === 0)
110
+ return fallback;
111
+ const uri = clientRoots[0];
112
+ return uri.startsWith('file://') ? fileURLToPath(uri) : resolve(uri);
113
+ }
114
+ // ---------------------------------------------------------------------------
115
+ // Full indexing pipeline
116
+ // ---------------------------------------------------------------------------
117
+ export async function buildIndex(rootDir, excludes, includes, quiet) {
118
+ const start = Date.now();
119
+ log(quiet, '[coldstart] Walking filesystem...');
120
+ const walkedFiles = await walkDirectory({ rootDir, excludes, includes });
121
+ log(quiet, `[coldstart] Found ${walkedFiles.length} source files`);
122
+ log(quiet, '[coldstart] Parsing files...');
123
+ const indexedFiles = [];
124
+ const langCount = {};
125
+ const allSymbolEdges = [];
126
+ const BATCH_SIZE = 100;
127
+ const PROGRESS_INTERVAL = 500;
128
+ let parsed_count = 0;
129
+ for (let i = 0; i < walkedFiles.length; i += BATCH_SIZE) {
130
+ const batch = walkedFiles.slice(i, i + BATCH_SIZE);
131
+ await Promise.all(batch.map(async (wf) => {
132
+ try {
133
+ const id = buildFileId(wf.relativePath);
134
+ const parsed = await parseFile(wf.absolutePath, wf.language, id);
135
+ if (!parsed)
136
+ return;
137
+ const file = {
138
+ ...baseIndexedFile(id, wf.absolutePath, wf.relativePath, wf.language, parsed),
139
+ domainMap: buildFileDomains(wf.relativePath, parsed.exports),
140
+ importedByCount: 0,
141
+ transitiveImportedByCount: 0,
142
+ isBarrel: false,
143
+ isTestFile: isTestPath(wf.relativePath),
144
+ };
145
+ indexedFiles.push(file);
146
+ langCount[wf.language] = (langCount[wf.language] ?? 0) + 1;
147
+ }
148
+ catch (err) {
149
+ log(quiet, `[coldstart] Error parsing ${wf.relativePath}: ${err}`);
150
+ }
151
+ finally {
152
+ parsed_count++;
153
+ }
154
+ }));
155
+ const prevMilestone = Math.floor((parsed_count - batch.length) / PROGRESS_INTERVAL);
156
+ const currMilestone = Math.floor(parsed_count / PROGRESS_INTERVAL);
157
+ const isLast = parsed_count === walkedFiles.length;
158
+ if (currMilestone > prevMilestone || isLast) {
159
+ const pct = Math.round((parsed_count / walkedFiles.length) * 100);
160
+ log(quiet, `[coldstart] ${parsed_count} / ${walkedFiles.length} parsed (${pct}%)`);
161
+ }
162
+ }
163
+ log(quiet, '[coldstart] Resolving imports...');
164
+ const { edges, unresolved } = await resolveImports(indexedFiles, rootDir);
165
+ log(quiet, `[coldstart] Resolved ${edges.length} edges (${unresolved.length} unresolved)`);
166
+ const fullFileIdSet = new Set(indexedFiles.map(f => f.id));
167
+ await addRailsSyntheticEdges(indexedFiles, edges, fullFileIdSet, rootDir);
168
+ await addLaravelSyntheticEdges(indexedFiles, edges, fullFileIdSet, rootDir);
169
+ await addCSharpSyntheticEdges(indexedFiles, edges, fullFileIdSet, rootDir);
170
+ await addDjangoSyntheticEdges(indexedFiles, edges, fullFileIdSet, rootDir);
171
+ if (!quiet) {
172
+ const langById = new Map(indexedFiles.map(f => [f.id, f.language]));
173
+ const stats = {};
174
+ for (const e of edges) {
175
+ const l = langById.get(e.from);
176
+ (stats[l] ??= { r: 0, u: 0 }).r++;
177
+ }
178
+ for (const u of unresolved) {
179
+ const l = langById.get(u.from);
180
+ (stats[l] ??= { r: 0, u: 0 }).u++;
181
+ }
182
+ const breakdown = Object.entries(stats)
183
+ .sort((a, b) => (b[1].r + b[1].u) - (a[1].r + a[1].u))
184
+ .map(([l, s]) => `${l}(${s.r}/${s.r + s.u})`)
185
+ .join(', ');
186
+ if (breakdown)
187
+ process.stderr.write(`[coldstart] Resolution by language: ${breakdown}\n`);
188
+ }
189
+ log(quiet, '[coldstart] Building graph...');
190
+ const nodeIds = indexedFiles.map(f => f.id);
191
+ const { outEdges, inEdges } = buildGraph(nodeIds, edges);
192
+ for (const file of indexedFiles) {
193
+ file.importedByCount = inEdges.get(file.id)?.length ?? 0;
194
+ }
195
+ const filesMap = new Map(indexedFiles.map(f => [f.id, f]));
196
+ const allSymbolEdgesBuilt = buildSymbolEdges(indexedFiles, outEdges, filesMap);
197
+ for (const e of allSymbolEdgesBuilt)
198
+ allSymbolEdges.push(e);
199
+ for (const file of indexedFiles) {
200
+ if (file.language === 'typescript' || file.language === 'javascript') {
201
+ file.isBarrel = ((file.reexportRatio ?? 0) > 0.5 &&
202
+ file.importedByCount > 1 &&
203
+ file.exports.length > 0);
204
+ }
205
+ file.transitiveImportedByCount = file.importedByCount;
206
+ }
207
+ for (const file of indexedFiles) {
208
+ if (!file.isBarrel)
209
+ continue;
210
+ for (const [token, ev] of Object.entries(file.domainMap)) {
211
+ if (ev.filename === 0 && ev.path === 0) {
212
+ delete file.domainMap[token];
213
+ }
214
+ else {
215
+ file.domainMap[token] = { ...ev, symbol: 0 };
216
+ }
217
+ }
218
+ }
219
+ const tokenDocFreq = new Map();
220
+ for (const file of indexedFiles) {
221
+ if (file.isBarrel)
222
+ continue;
223
+ for (const token of Object.keys(file.domainMap)) {
224
+ tokenDocFreq.set(token, (tokenDocFreq.get(token) ?? 0) + 1);
225
+ }
226
+ }
227
+ const contentTokenPostings = buildContentTokenPostings(indexedFiles);
228
+ const gitHead = await getGitHead(rootDir);
229
+ const elapsed = ((Date.now() - start) / 1000).toFixed(1);
230
+ log(quiet, `[coldstart] Indexed ${indexedFiles.length} files in ${elapsed}s`);
231
+ log(quiet, `[coldstart] Languages: ${Object.entries(langCount).map(([l, c]) => `${l}(${c})`).join(', ')}`);
232
+ for (const file of indexedFiles) {
233
+ if (!file.isBarrel)
234
+ continue;
235
+ for (const childId of outEdges.get(file.id) ?? []) {
236
+ const child = filesMap.get(childId);
237
+ if (child)
238
+ child.transitiveImportedByCount += file.importedByCount;
239
+ }
240
+ }
241
+ return {
242
+ rootDir,
243
+ files: filesMap,
244
+ edges,
245
+ symbolEdges: allSymbolEdges,
246
+ outEdges,
247
+ inEdges,
248
+ tokenDocFreq,
249
+ contentTokenPostings,
250
+ indexedAt: Date.now(),
251
+ gitHead,
252
+ };
253
+ }
254
+ // Bucket an edge specifier into a coarse key for --probe output.
255
+ // - synthetic prefixes like `const:Foo` / `partial:UserService` → `const:*` / `partial:*`
256
+ // (last segment is class-like, generalize it)
257
+ // - `convention:django:middleware` → keep as-is (categorical, not class-specific)
258
+ // - regular imports (file paths, package names, no colon) → `regular`
259
+ function bucketSpecifier(spec) {
260
+ if (!spec.includes(':'))
261
+ return 'regular';
262
+ const parts = spec.split(':');
263
+ const last = parts[parts.length - 1];
264
+ if (/^[A-Z]/.test(last))
265
+ return parts.slice(0, -1).join(':') + ':*';
266
+ return spec;
267
+ }
268
+ // ---------------------------------------------------------------------------
269
+ // Probe mode: walk → parse → resolve, emit JSON stats to stdout, exit.
270
+ // Per-language: total imports, resolved, unresolved, plus top-N unresolved
271
+ // specifiers (with sample fromFile) so we can tell external libs apart from
272
+ // broken-internal resolution gaps.
273
+ // ---------------------------------------------------------------------------
274
+ async function runProbe(rootDir, excludes, includes) {
275
+ const start = Date.now();
276
+ const walked = await walkDirectory({ rootDir, excludes, includes });
277
+ const tWalk = Date.now() - start;
278
+ const tParseStart = Date.now();
279
+ const indexedFiles = [];
280
+ for (let i = 0; i < walked.length; i += 100) {
281
+ const batch = walked.slice(i, i + 100);
282
+ await Promise.all(batch.map(async (wf) => {
283
+ try {
284
+ const id = buildFileId(wf.relativePath);
285
+ const parsed = await parseFile(wf.absolutePath, wf.language, id);
286
+ if (!parsed)
287
+ return;
288
+ indexedFiles.push({
289
+ ...baseIndexedFile(id, wf.absolutePath, wf.relativePath, wf.language, parsed),
290
+ domainMap: {},
291
+ importedByCount: 0,
292
+ transitiveImportedByCount: 0,
293
+ isBarrel: false,
294
+ isTestFile: false,
295
+ });
296
+ }
297
+ catch { /* skip parse errors */ }
298
+ }));
299
+ }
300
+ const tParse = Date.now() - tParseStart;
301
+ const tResolveStart = Date.now();
302
+ const { resolveImportsForFiles, buildPackageIndex } = await import('./indexer/resolvers/index.js');
303
+ // Per-language resolve timing — pass the FULL fileIdSet (resolvers index it
304
+ // once via WeakMap) but invoke per-language so we can attribute time. Java
305
+ // resolves to Kotlin files and vice versa, so we must not narrow the set.
306
+ const fullFileIdSet = new Set(indexedFiles.map(f => f.id));
307
+ // JVM package index over the FULL set — anchors Java/Kotlin FQCN resolution on
308
+ // each file's declared `package`, so non-Maven layouts (e.g. JMRI's java/src/)
309
+ // resolve correctly instead of mis-deriving the package from the path.
310
+ const pkgById = buildPackageIndex(indexedFiles);
311
+ const filesByLang = new Map();
312
+ for (const f of indexedFiles) {
313
+ const arr = filesByLang.get(f.language) ?? [];
314
+ arr.push(f);
315
+ filesByLang.set(f.language, arr);
316
+ }
317
+ const langTimes = {};
318
+ let allEdges = [];
319
+ let allUnresolved = [];
320
+ for (const [lang, files] of filesByLang) {
321
+ const t0 = Date.now();
322
+ const r = await resolveImportsForFiles(files, fullFileIdSet, rootDir, pkgById);
323
+ langTimes[lang] = Date.now() - t0;
324
+ allEdges = allEdges.concat(r.edges);
325
+ allUnresolved = allUnresolved.concat(r.unresolved);
326
+ }
327
+ const edges = allEdges;
328
+ const unresolved = allUnresolved;
329
+ const tResolve = Date.now() - tResolveStart;
330
+ await addRailsSyntheticEdges(indexedFiles, edges, fullFileIdSet, rootDir);
331
+ await addLaravelSyntheticEdges(indexedFiles, edges, fullFileIdSet, rootDir);
332
+ await addCSharpSyntheticEdges(indexedFiles, edges, fullFileIdSet, rootDir);
333
+ await addDjangoSyntheticEdges(indexedFiles, edges, fullFileIdSet, rootDir);
334
+ const langById = new Map(indexedFiles.map(f => [f.id, f.language]));
335
+ const fileById = new Map(indexedFiles.map(f => [f.id, f.relativePath]));
336
+ const byLang = new Map();
337
+ const get = (l) => {
338
+ let b = byLang.get(l);
339
+ if (!b) {
340
+ b = { files: 0, totalImports: 0, resolved: 0, unresolved: 0, unresolvedBySpec: new Map() };
341
+ byLang.set(l, b);
342
+ }
343
+ return b;
344
+ };
345
+ for (const f of indexedFiles) {
346
+ get(f.language).files++;
347
+ get(f.language).totalImports += f.imports.length;
348
+ }
349
+ for (const e of edges) {
350
+ get(langById.get(e.from)).resolved++;
351
+ }
352
+ for (const u of unresolved) {
353
+ const b = get(langById.get(u.from));
354
+ b.unresolved++;
355
+ const existing = b.unresolvedBySpec.get(u.specifier);
356
+ if (existing)
357
+ existing.count++;
358
+ else
359
+ b.unresolvedBySpec.set(u.specifier, { count: 1, sampleFrom: fileById.get(u.from) ?? '' });
360
+ }
361
+ const edgesBySpecifier = {};
362
+ for (const e of edges) {
363
+ const bucket = bucketSpecifier(e.specifier);
364
+ edgesBySpecifier[bucket] = (edgesBySpecifier[bucket] ?? 0) + 1;
365
+ }
366
+ const sortedEdgesBySpecifier = Object.fromEntries(Object.entries(edgesBySpecifier).sort((a, b) => b[1] - a[1]));
367
+ const out = {
368
+ rootDir,
369
+ totalFiles: indexedFiles.length,
370
+ totalEdges: edges.length,
371
+ totalUnresolved: unresolved.length,
372
+ elapsedMs: Date.now() - start,
373
+ phaseMs: { walk: tWalk, parse: tParse, resolve: tResolve, resolveByLang: langTimes },
374
+ edgesBySpecifier: sortedEdgesBySpecifier,
375
+ languages: Object.fromEntries([...byLang.entries()]
376
+ .sort((a, b) => b[1].totalImports - a[1].totalImports)
377
+ .map(([lang, b]) => [lang, {
378
+ files: b.files,
379
+ totalImports: b.totalImports,
380
+ resolved: b.resolved,
381
+ unresolved: b.unresolved,
382
+ resolvedRatio: b.totalImports > 0 ? +(b.resolved / b.totalImports).toFixed(3) : 0,
383
+ topUnresolved: [...b.unresolvedBySpec.entries()]
384
+ .sort((a, b) => b[1].count - a[1].count)
385
+ .slice(0, 30)
386
+ .map(([specifier, v]) => ({ specifier, count: v.count, sampleFrom: v.sampleFrom })),
387
+ }])),
388
+ };
389
+ process.stdout.write(JSON.stringify(out, null, 2) + '\n');
390
+ }
391
+ // ---------------------------------------------------------------------------
392
+ // Shared: load from cache or build, then create and return an IndexManager
393
+ // ---------------------------------------------------------------------------
394
+ async function buildManager(finalRoot, excludes, includes, cacheDir, quiet, noCache) {
395
+ let index = null;
396
+ if (!noCache) {
397
+ index = await loadCachedIndex(finalRoot, cacheDir);
398
+ if (index) {
399
+ // Startup reconcile: the cache is trusted but files may have changed
400
+ // while no keeper watched (branch switch, edits, pulls). Patch the
401
+ // drift in place — a full rebuild only when the drift is repo-scale.
402
+ const rec = await reconcileChanges(index, finalRoot, excludes, includes);
403
+ const threshold = patchThreshold(index.files.size);
404
+ let outcome;
405
+ if (!rec) {
406
+ log(quiet, '[coldstart] Reconcile unavailable — rebuilding index...');
407
+ outcome = 'unavailable → rebuild';
408
+ index = null;
409
+ }
410
+ else if (rec.changed.size === 0) {
411
+ log(quiet, `[coldstart] Loaded from cache — fresh (${rec.reason})`);
412
+ outcome = `${rec.reason} → fresh`;
413
+ // HEAD can move with zero file drift (committing files the keeper
414
+ // already patched). Refresh the stored head or every reader pays the
415
+ // full HEAD-drift wait on every query, forever.
416
+ const head = await getGitHead(finalRoot);
417
+ if (head !== index.gitHead) {
418
+ index.gitHead = head;
419
+ try {
420
+ await updateCachedGitHead(finalRoot, head, cacheDir);
421
+ }
422
+ catch { /* next full save fixes it */ }
423
+ }
424
+ }
425
+ else if (rec.changed.size <= threshold) {
426
+ log(quiet, `[coldstart] Loaded from cache — patching ${rec.changed.size} drifted file(s) (${rec.reason})`);
427
+ try {
428
+ await patchIndex(index, rec.changed, finalRoot);
429
+ const problems = lintIndexInvariants(index);
430
+ if (problems.length > 0)
431
+ throw new Error(`invariant violation: ${problems.join(' | ')}`);
432
+ index.gitHead = await getGitHead(finalRoot);
433
+ index.indexedAt = Date.now();
434
+ await saveCachedIndex(index, cacheDir);
435
+ log(quiet, '[coldstart] Reconcile patch done');
436
+ outcome = `${rec.reason} → patched`;
437
+ }
438
+ catch (err) {
439
+ log(quiet, `[coldstart] Reconcile patch failed (${err}) — rebuilding index...`);
440
+ if (!noCache)
441
+ await appendRepairLog(finalRoot, 'reconcile-failed', String(err), cacheDir);
442
+ outcome = `${rec.reason} → patch failed, rebuild`;
443
+ index = null;
444
+ }
445
+ }
446
+ else {
447
+ log(quiet, `[coldstart] ${rec.changed.size} file(s) drifted (> ${threshold}) — rebuilding index...`);
448
+ outcome = `${rec.reason} → over threshold, rebuild`;
449
+ index = null;
450
+ }
451
+ if (!noCache)
452
+ await updateKeeperState(finalRoot, { lastReconcile: { at: Date.now(), detail: outcome } }, cacheDir);
453
+ }
454
+ }
455
+ if (!index) {
456
+ index = await buildIndex(finalRoot, excludes, includes, quiet);
457
+ if (!noCache) {
458
+ try {
459
+ await saveCachedIndex(index, cacheDir);
460
+ log(quiet, '[coldstart] Index saved to cache');
461
+ }
462
+ catch (err) {
463
+ log(quiet, `[coldstart] Warning: could not save cache: ${err}`);
464
+ }
465
+ }
466
+ }
467
+ const manager = new IndexManager(index, () => buildIndex(finalRoot, excludes, includes, quiet), cacheDir, noCache, quiet);
468
+ manager.startWatching();
469
+ return manager;
470
+ }
471
+ // ---------------------------------------------------------------------------
472
+ // Keeper mode (`--daemon`): keep the on-disk index FRESH. Watches the repo,
473
+ // patches/rebuilds the in-memory index, and writes it to the disk cache that
474
+ // the CLI and MCP readers load. It does NOT serve queries — every reader reads
475
+ // the cache directly. Lazy-spawned by the first reader; runs until killed or
476
+ // its lockfile is removed.
477
+ // ---------------------------------------------------------------------------
478
+ async function runKeeper(finalRoot, excludes, includes, cacheDir, _quiet, noCache) {
479
+ // Attach the file-backed logger BEFORE the first log() call: the keeper is
480
+ // auto-spawned with stdio: 'ignore', so the log file is the ONLY debug trace.
481
+ const detachLogger = attachDaemonLogger(finalRoot);
482
+ // The keeper IGNORES --quiet: its stderr goes to the log file, never a
483
+ // console (readers spawn it with stdio 'ignore'), and an empty log makes
484
+ // every keeper incident undiagnosable. --quiet keeps its meaning for the
485
+ // MCP reader / --no-daemon modes, which do write to a real stderr.
486
+ const quiet = false;
487
+ void _quiet;
488
+ log(quiet, `[coldstart] Keeper starting — root: ${finalRoot} (PID ${process.pid})`);
489
+ // Exit immediately if another keeper is already alive for this root.
490
+ const existing = await readLock(finalRoot);
491
+ if (existing && isDaemonAlive(existing.pid)) {
492
+ log(quiet, '[coldstart] Another keeper is already running — exiting.');
493
+ detachLogger();
494
+ process.exit(0);
495
+ }
496
+ // Version in the lock lets a reader replace an outdated keeper (cache-format
497
+ // compatibility) on upgrade.
498
+ await writeLock(finalRoot, process.pid, getCurrentVersion());
499
+ log(quiet, `[coldstart] Keeper lock written (PID ${process.pid})`);
500
+ if (!noCache)
501
+ await updateKeeperState(finalRoot, { startedAt: Date.now() }, cacheDir);
502
+ // Auto-migrate legacy npx-based .mcp.json entries (non-fatal).
503
+ try {
504
+ await migrateLegacyMcpConfig(finalRoot);
505
+ }
506
+ catch { /* ignore */ }
507
+ let manager = null;
508
+ try {
509
+ // buildManager loads-or-builds the index AND starts the watcher, which
510
+ // patches/rebuilds and debounce-saves the cache on every change.
511
+ manager = await buildManager(finalRoot, excludes, includes, cacheDir, quiet, noCache);
512
+ log(quiet, '[coldstart] Keeper index ready — watching for changes');
513
+ }
514
+ catch (err) {
515
+ log(quiet, `[coldstart] Keeper index build failed: ${err}`);
516
+ await deleteLock(finalRoot).catch(() => { });
517
+ detachLogger();
518
+ process.exit(1);
519
+ }
520
+ const cleanup = () => {
521
+ manager?.stopWatching();
522
+ deleteLock(finalRoot).catch(() => { }).finally(() => {
523
+ detachLogger();
524
+ process.exit(0);
525
+ });
526
+ };
527
+ // Exit cleanly if the user removes our lockfile.
528
+ const stopLockWatcher = watchOwnLockfile(finalRoot, () => {
529
+ log(quiet, '[coldstart] Lockfile deleted — shutting down keeper');
530
+ cleanup();
531
+ });
532
+ process.on('SIGTERM', cleanup);
533
+ process.on('SIGINT', cleanup);
534
+ process.on('exit', () => {
535
+ stopLockWatcher();
536
+ manager?.stopWatching();
537
+ detachLogger();
538
+ });
539
+ // Run forever.
540
+ await new Promise(() => { });
541
+ }
542
+ // ---------------------------------------------------------------------------
543
+ // A disk-cache reader for the long-lived MCP server: loads the keeper-maintained
544
+ // index and reloads it when the cache file's mtime advances (the keeper rewrites
545
+ // it ~5s after edits settle). This is how the MCP server stays live WITHOUT its
546
+ // own watcher — the keeper is the single freshness authority.
547
+ // ---------------------------------------------------------------------------
548
+ function makeCacheReader(finalRoot, cacheDir, quiet) {
549
+ let cached = null;
550
+ let lastMtimeMs = 0;
551
+ const metaPath = join(getCacheDir(finalRoot, cacheDir), 'meta.json');
552
+ const currentMtime = () => {
553
+ try {
554
+ return statSync(metaPath).mtimeMs;
555
+ }
556
+ catch {
557
+ return 0;
558
+ }
559
+ };
560
+ return async () => {
561
+ const m = currentMtime();
562
+ if (!cached || (m > 0 && m > lastMtimeMs)) {
563
+ // 'gs' profile: everything the MCP tools (find/gs/kb_search) read, without
564
+ // the keeper-only build segment. The keeper is the only 'full' loader.
565
+ const fresh = await loadCachedIndex(finalRoot, cacheDir, 'gs');
566
+ if (fresh) {
567
+ cached = fresh;
568
+ lastMtimeMs = m;
569
+ }
570
+ else if (!cached) {
571
+ // No cache yet (keeper still building its first index). Readers never
572
+ // build (B4) — wait for the keeper's first save; in-process build only
573
+ // if no keeper exists at all.
574
+ const wait = await waitForKeeperCache(finalRoot, cacheDir, 180_000, (msg) => log(quiet, msg));
575
+ if (wait === 'ready') {
576
+ for (let attempt = 0; attempt < 3 && !cached; attempt++) {
577
+ cached = await loadCachedIndex(finalRoot, cacheDir, 'gs');
578
+ if (!cached)
579
+ await new Promise((r) => setTimeout(r, 500));
580
+ }
581
+ }
582
+ if (!cached) {
583
+ if (wait === 'timeout') {
584
+ throw new Error('coldstart: the keeper is still building the index for this repo — retry in a minute');
585
+ }
586
+ log(quiet, '[coldstart] no keeper available — building in-process (one-off)');
587
+ cached = await buildIndex(finalRoot, [], [], quiet);
588
+ try {
589
+ await saveCachedIndex(cached, cacheDir);
590
+ }
591
+ catch { /* ignore */ }
592
+ }
593
+ lastMtimeMs = currentMtime();
594
+ }
595
+ }
596
+ return { index: cached, isRebuilding: false };
597
+ };
598
+ }
599
+ // ---------------------------------------------------------------------------
600
+ // Main
601
+ // ---------------------------------------------------------------------------
602
+ async function main() {
603
+ if (process.argv[2] === 'init') {
604
+ const { runInit } = await import('./init.js');
605
+ await runInit();
606
+ return;
607
+ }
608
+ if (process.argv[2] === 'status') {
609
+ const { runStatus } = await import('./status.js');
610
+ await runStatus();
611
+ return;
612
+ }
613
+ // `restart` — kill the keeper for this root (or --all) and clear its lock.
614
+ // The keeper respawns lazily on the next reader.
615
+ if (process.argv[2] === 'restart') {
616
+ const { runRestart } = await import('./restart.js');
617
+ await runRestart();
618
+ return;
619
+ }
620
+ // CLI query surface — pure-reader `find`/`gs`, plus single-writer `index` prep.
621
+ // These read the on-disk cache directly (kept fresh by the keeper), run the
622
+ // same engine the MCP reader uses, print, exit. (docs/cli-skill-spec.md)
623
+ if (process.argv[2] === 'gs') {
624
+ const { runGs } = await import('./cli.js');
625
+ process.exit(await runGs(process.argv.slice(3), buildIndex));
626
+ }
627
+ if (process.argv[2] === 'find') {
628
+ const { runFind } = await import('./cli.js');
629
+ process.exit(await runFind(process.argv.slice(3), buildIndex));
630
+ }
631
+ if (process.argv[2] === 'index') {
632
+ const { runIndexPrep } = await import('./cli.js');
633
+ process.exit(await runIndexPrep(process.argv.slice(3), buildIndex));
634
+ }
635
+ // Notebook KB — `kb search|write|status|lint|render|init|migrate`.
636
+ if (process.argv[2] === 'kb') {
637
+ const { runKb } = await import('./kb/cli.js');
638
+ process.exit(await runKb(process.argv.slice(3)));
639
+ }
640
+ const { root: cliRoot, rootExplicit, excludes, includes, cacheDir, quiet, noCache, daemon, noDaemon, probe } = parseArgs(process.argv);
641
+ // --probe: one-shot stats dump for the validation harness, then exit.
642
+ if (probe) {
643
+ await runProbe(resolve(cliRoot), excludes, includes);
644
+ return;
645
+ }
646
+ // --daemon: background keeper that keeps the on-disk cache fresh (no serving).
647
+ // Lazy-spawned by readers via ensureKeeper().
648
+ if (daemon) {
649
+ const finalRoot = resolve(cliRoot);
650
+ await runKeeper(finalRoot, excludes, includes, cacheDir, quiet, noCache);
651
+ return;
652
+ }
653
+ // --no-daemon: self-contained single-process stdio MCP (builds + watches +
654
+ // serves in-process, no background keeper). Useful for debugging / one-off use.
655
+ if (noDaemon) {
656
+ let manager = null;
657
+ let managerReadyResolve;
658
+ let managerReadyReject;
659
+ const managerReady = new Promise((res, rej) => {
660
+ managerReadyResolve = res;
661
+ managerReadyReject = rej;
662
+ });
663
+ const buildAndSet = async (finalRoot) => {
664
+ log(quiet, `[coldstart] Starting — root: ${finalRoot}`);
665
+ try {
666
+ await migrateLegacyMcpConfig(finalRoot);
667
+ manager = await buildManager(finalRoot, excludes, includes, cacheDir, quiet, noCache);
668
+ managerReadyResolve();
669
+ log(quiet, '[coldstart] MCP server ready');
670
+ }
671
+ catch (err) {
672
+ log(quiet, `[coldstart] Fatal: ${err}`);
673
+ managerReadyReject(err instanceof Error ? err : new Error(String(err)));
674
+ process.exit(1);
675
+ }
676
+ };
677
+ if (rootExplicit) {
678
+ buildAndSet(resolve(cliRoot)).catch(() => { });
679
+ }
680
+ await startMCPServer(async (clientRoots) => {
681
+ if (!rootExplicit) {
682
+ buildAndSet(resolveClientRoot(clientRoots, resolve(cliRoot))).catch(() => { });
683
+ }
684
+ }, async () => {
685
+ await managerReady;
686
+ return manager.getContext();
687
+ }, { cacheDir });
688
+ process.on('exit', () => manager?.stopWatching());
689
+ process.on('SIGINT', () => { manager?.stopWatching(); process.exit(0); });
690
+ process.on('SIGTERM', () => { manager?.stopWatching(); process.exit(0); });
691
+ return;
692
+ }
693
+ // Default: stdio MCP server in READER mode. Serves find/gs off the on-disk
694
+ // cache that a background keeper keeps fresh; spawns the keeper if absent.
695
+ // This is the path for no-shell MCP clients (e.g. Claude Desktop).
696
+ {
697
+ let reader = null;
698
+ let readerReadyResolve;
699
+ const readerReady = new Promise(res => { readerReadyResolve = res; });
700
+ const initRoot = async (finalRoot) => {
701
+ log(quiet, `[coldstart] MCP reader — root: ${finalRoot}`);
702
+ await ensureKeeper(finalRoot);
703
+ reader = makeCacheReader(finalRoot, cacheDir, quiet);
704
+ readerReadyResolve();
705
+ };
706
+ if (rootExplicit) {
707
+ void initRoot(resolve(cliRoot));
708
+ }
709
+ await startMCPServer(async (clientRoots) => {
710
+ if (!rootExplicit) {
711
+ void initRoot(resolveClientRoot(clientRoots, resolve(cliRoot)));
712
+ }
713
+ }, async () => {
714
+ await readerReady;
715
+ return reader();
716
+ }, { cacheDir });
717
+ return;
718
+ }
719
+ }
720
+ main().catch(err => {
721
+ process.stderr.write(`[coldstart] Fatal error: ${err}\n`);
722
+ process.exit(1);
723
+ });
724
+ //# sourceMappingURL=index.js.map