@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/README.md ADDED
@@ -0,0 +1,236 @@
1
+ # coldstart
2
+
3
+ Codebase navigation **and a codebase notebook** for AI agents.
4
+
5
+ Two layers, one tool:
6
+
7
+ - **The notebook** (`coldstart kb`) — durable, agent-written notes about how *this* codebase actually works: what a file is for, how a flow spans files, which invariants hold. Captured after real tasks, recalled when a later task matches, and kept honest by the index — every note is anchored to real files, and a note whose evidence drifted is flagged, not served as truth.
8
+ - **Navigation** (`coldstart find` / `coldstart gs`) — a fast static index over file paths, symbol names, exports, and the import/call graph. It answers "which files are relevant to this task?" in milliseconds, with checkable evidence instead of a similarity score.
9
+
10
+ No embeddings, no model to run, no service to babysit. Agents are already good at reading and reasoning about code; what they waste tokens on is *finding* the right file and *re-deriving* what the last session already figured out. coldstart does those two parts and gets out of the way.
11
+
12
+ ---
13
+
14
+ ## The notebook
15
+
16
+ A repo-local knowledge base written and read by agents, in `.coldstart/notebook/`:
17
+
18
+ ```bash
19
+ coldstart kb search tile save lifecycle # plain task words, symbols, or file names
20
+ coldstart kb lookup src/models.py Tile # everything known at one exact address
21
+ coldstart kb write spec.json # the write gate (two-phase dedup)
22
+ coldstart kb commit # publish notes to git, nothing else rides along
23
+ coldstart kb view # open a single-file HTML browser of the notebook
24
+ coldstart kb status / lint / render / init / migrate
25
+ ```
26
+
27
+ **What a note is.** Three shapes: a **file note** (what a file is for — a single summary, or per-symbol facets for hub files), a **flow note** (a cross-file story: ordered steps, invariants), and a **lesson** (a trap, rule, bug-cause, rationale, or confirmed absence). Every note carries **anchors** — concrete file paths and symbols its claims rest on.
28
+
29
+ **Where notes reach the agent.** Three surfaces, no new habits required:
30
+
31
+ 1. **`Summary:` lines on `find` results** — a past agent's verified overview of a file, right where the file ranks. `[fresh]` means the file is byte-identical to when the summary was verified — the agent can rely on it without re-reading the file.
32
+ 2. **Recall at prompt time** (optional hook) — notes whose titles, aliases, or anchors match the incoming prompt are surfaced as a compact title + gist + path block, hard-capped, framed as reference data. Nothing matches → nothing injected.
33
+ 3. **`kb search` / `kb lookup`** — a search engine over the notebook for mid-task vocabulary changes, and an exact-address lookup (`path [symbol]`) before editing a file.
34
+
35
+ **Why it can be trusted.** This is the part that took the design work:
36
+
37
+ - **Freshness is mechanical, not hoped-for.** Every anchor is stamped with a content hash at write time; the index re-checks stamps as the code changes. A drifted note renders `[evidence changed: <path>]` and the guidance says re-verify — stale knowledge degrades into a labeled hypothesis instead of a confident lie.
38
+ - **The log is the truth.** Notes live in an append-only `.raw` event log (commit it — merges are unions, so parallel branches of notes reconcile without conflicts). The Markdown notes are derived, regenerated mechanically, and gitignored.
39
+ - **Writes go through a gate.** A new note's concept is first searched against existing notes — the agent must explicitly merge into a match (`--into <id>`) or declare it new (`--new`). Duplicates are gated at write time, not cleaned up later.
40
+ - **Concurrent sessions are safe.** Multiple agents can write at once: per-note append-only logs, exclusive creation for new note ids (a same-moment duplicate becomes two visible notes, never a silent merge), lossless merging for shared file notes, and atomic renders (a reader never sees a half-written note).
41
+ - **Corrections happen in-session.** If an agent finds a note wrong while the evidence is in its context, the guidance tells it to fix or retract the note right then — no better-placed future agent exists.
42
+
43
+ **Setup:** `coldstart kb init` creates the notebook skeleton, sets union-merge for the logs, and (on Claude Code) wires the two optional hooks — capture at session end, recall at prompt time. Other hosts can drive the notebook without the hooks: via the full `kb` CLI, or — for no-shell clients — the `kb_search` / `kb_lookup` / `kb_write` / `kb_status` MCP tools.
44
+
45
+ **Language-agnostic.** The notebook's freshness machinery is content-hash based, so it works on any codebase — including languages the navigation index doesn't parse. Where the index does parse, notes additionally get symbol-level freshness.
46
+
47
+ The notebook is young. What's verified today: notes written by agents in real sessions checked out accurate against the code; the stale-note loop closes end-to-end (flag → re-read → correction); capture, recall, and concurrent writes hold up under stress. The bet — stated as a bet — is that a corpus like this compounds over a repo's lifetime: the second time any question comes up, the answer is one `Read` away instead of a re-derivation.
48
+
49
+ ---
50
+
51
+ ## Navigation: the two operations
52
+
53
+ | | What it answers | Replaces |
54
+ |---|---|---|
55
+ | **`find <terms>`** | "Which files are about this?" — ranks files by how many of your query terms they cover (filenames, path segments, exported symbols, plus a repo-wide name-reference pass). | a flurry of `grep`/`glob` while orienting |
56
+ | **`gs <file>`** | "What is this file?" — top-level symbols with line ranges, who imports it, who calls each symbol, and name-related neighbors. | reading a whole file just to learn its shape and usage |
57
+
58
+ The intended flow: **`find`** a concept → pick the best path → **`gs`** that file for its shape and who uses it → `Read` only for the implementation inside a method body. Notebook summaries ride along on `find` results, so often the orientation step answers itself.
59
+
60
+ ### `find` — locate the files for a concept
61
+
62
+ ```bash
63
+ coldstart find auth session cookie
64
+ ```
65
+
66
+ Pass **every salient identifier** from your task — the symbol, the domain noun, the rare token you half-remember — not one distilled keyword. `find` ranks files by how many of your terms each one covers and shows, per file, which terms it defines vs. imports and a preview of the lines where they cluster. Often that's enough to answer without opening anything.
67
+
68
+ Speed-wise, `find` competes with raw grep: its repo-wide reference pass runs on **ripgrep** — yours from PATH, the bundled copy, or an editor's (`COLDSTART_RG` overrides) — with `git grep`/`grep` fallbacks, and the ranked page comes from the pre-built index, not a scan.
69
+
70
+ **Flags:** `--path GLOB` (scope; comma-combine, `!` excludes) · `--tests` (include test files) · `--via` (show name-reference relations) · `--json`
71
+
72
+ ### `gs` — drill into one file
73
+
74
+ ```bash
75
+ coldstart gs src/auth/service.ts
76
+ ```
77
+
78
+ Returns the file's symbols (with line ranges), its 1-hop internal imports, who imports it, and per-symbol cross-file callers — in one call. This is the answer to **"who uses this file / who calls this symbol"**; it is not a grep.
79
+
80
+ **Flags:** `--symbol a,b` (deliver named method bodies inline) · `--match TERM` (filter a god-file to one area; `a|b` = OR, `/regex/` = regex) · `--view symbols|imports|importers|callers` · `--json`
81
+
82
+ ### Batch independent lookups in one shell call
83
+
84
+ ```bash
85
+ coldstart find auth; coldstart find 'session cookie'; coldstart gs src/auth/service.ts
86
+ ```
87
+
88
+ ---
89
+
90
+ ## Two ways to call it, identical output
91
+
92
+ coldstart ships as one binary with two front doors:
93
+
94
+ - **CLI (primary)** — `coldstart find …` / `coldstart gs …` / `coldstart kb …`. For any shell-capable agent (Claude Code, Cursor, terminal use). This is the fast path.
95
+ - **MCP (for no-shell clients)** — the `find` and `gs` tools, plus the notebook as `kb_search` / `kb_lookup` / `kb_write` / `kb_status`, all byte-identical to the CLI. For clients like Claude Desktop that have no shell. (`kb commit` stays CLI/human-only — publishing notes to git is never an agent action.)
96
+
97
+ Same engine, same index, same results. Pick whichever your agent can reach.
98
+
99
+ It works best with **Claude Code**, **Codex**, and **Cursor**: all three get platform-specific find/gs hooks and notebook recall/capture hooks from `coldstart init`. Any other client gets `coldstart.md` plus printed wiring directions.
100
+
101
+ ---
102
+
103
+ ## Bring your own semantics
104
+
105
+ coldstart has no embeddings, no generated summaries, no semantic layer computed at index time — **on purpose. The semantic layer is the agent.** Every consumer is already a frontier model; pre-computing meaning at index time only duplicates that, worse and stale. So the index keeps what's cheap to keep *exact* — paths, symbols, exports, the import/call graph — and returns *why* each file ranked.
106
+
107
+ The notebook is the same philosophy applied to memory: coldstart still computes no meaning of its own. It stores, anchors, and freshness-checks the meaning **agents** author — written at task time, by the reasoner that had the full context, about the question that actually mattered. The full argument is in **[PHILOSOPHY.md](./PHILOSOPHY.md)**.
108
+
109
+ ---
110
+
111
+ ## Install
112
+
113
+ Requires Node.js 18+.
114
+
115
+ ```bash
116
+ npm install -g @cstart/coldstart --legacy-peer-deps
117
+ cd your-project
118
+ coldstart init # navigation: coldstart.md + client wiring + background index warm-up
119
+ coldstart kb init # notebook: skeleton + capture/recall hooks (optional but recommended)
120
+ ```
121
+
122
+ `init` asks two things — the **experience** (`cli`, recommended, or `mcp`) and the **client** — then writes a single `coldstart.md` at your repo root (the agent-facing guidance) and wires it in. Pass `--experience` / `--client` to skip the prompts. The client is never auto-detected; you always pick it.
123
+
124
+ - **Claude Code** → ensures `CLAUDE.md` imports it via `@coldstart.md`, and registers the find/gs search hooks in `.claude/settings.json` (a PostToolUse nudge + a PreToolUse find-dedup guard — merged into any existing settings, never overwriting them). The `mcp` experience also writes `.mcp.json`.
125
+ - **Codex** → adds a coldstart section to `AGENTS.md` and registers Codex-specific navigation plus notebook hooks in `.codex/hooks.json`. The capture hook understands Codex rollout and subagent transcripts. The `mcp` experience also writes `[mcp_servers.coldstart]` into `.codex/config.toml`.
126
+ - **Cursor** → writes `.cursor/rules/coldstart.mdc` (an always-applied rule referencing `@coldstart.md`) and registers Cursor-specific navigation plus notebook hooks in `.cursor/hooks.json` (a `preToolUse` find-dedup guard, a `postToolUse` nudge, `beforeSubmitPrompt` recall, and `stop`/`subagentStop` capture — merged into any existing hooks). The capture hook parses Cursor's own conversation transcript. The `mcp` experience also writes `.cursor/mcp.json`.
127
+ - **Other** → writes `coldstart.md` only, and prints the wiring directions (plus the MCP server entry for the `mcp` experience).
128
+
129
+ `init` then warms the index in the background, so your first lookup is instant. Re-running either init is safe — they never duplicate entries.
130
+
131
+ > **Why `--legacy-peer-deps`?** The tree-sitter grammar packages under-declare their peer-dep ranges (some say `^0.21.x`, others `^0.22.x`). Without the flag npm's strict resolver enters a long retry loop on a cold cache and can appear to hang. The flag tells npm to use our tested versions as-is. We can't set it from inside the package — npm reads install config only from your environment.
132
+
133
+ ### Upgrading
134
+
135
+ ```bash
136
+ npm install -g @cstart/coldstart@latest --legacy-peer-deps
137
+ coldstart init # re-run in each project to refresh coldstart.md
138
+ ```
139
+
140
+ A version stamp in the keeper's lockfile makes the old background keeper shut down on the next lookup; a fresh one spawns from the new binary. No manual restart needed.
141
+
142
+ > **Migrating from `coldstart-mcp`:** the package was renamed `coldstart-mcp` → **`coldstart`** at 2.0.0 (the CLI is now the primary surface). `coldstart-mcp` is deprecated but still installs; switch with `npm uninstall -g coldstart-mcp && npm install -g coldstart --legacy-peer-deps && coldstart init`. The `coldstart-mcp` binary name is kept as an alias, so existing MCP configs keep working.
143
+
144
+ ---
145
+
146
+ ## How the index stays fresh
147
+
148
+ coldstart is **one keeper, thin readers**:
149
+
150
+ ```
151
+ ┌─────────────────────────────────────────────┐
152
+ │ keeper (coldstart --daemon) │
153
+ │ watches repo → patch/rebuild → save cache │ ← keeps the cache fresh, serves nothing
154
+ └───────────────────────┬─────────────────────┘
155
+ │ on-disk cache
156
+ ┌───────────────────────────┼───────────────────────────┐
157
+ │ │ │
158
+ coldstart find coldstart gs MCP server
159
+ (reads cache, prints) (reads cache, prints) (reads cache, stdio to client)
160
+ ```
161
+
162
+ - A single **keeper** process per repo watches the filesystem and keeps the on-disk cache current. It does **not** answer queries.
163
+ - The CLI readers (`find`/`gs`) and the MCP server are **stateless readers** over that cache. The first reader for a repo lazily spawns the keeper, so even uncommitted edits stay live.
164
+ - **Readers never build the index.** On a cache miss they wait for the keeper's build (progress to stderr) instead of silently kicking off a multi-minute build inline — or three of them concurrently.
165
+ - No HTTP, no ports, no bridge. The keeper logs to `~/.coldstart/daemon/<root>.log` and exits when its lockfile is removed.
166
+
167
+ **There is no cache TTL.** The index is never discarded for being old — it's kept *correct* instead:
168
+
169
+ - **While the keeper runs:** edits are debounced (400 ms), then **patched incrementally** (~2–5 ms/file, up to 30 files or 20% of the repo, whichever is larger) or trigger a **background full rebuild** above that (served from the last good index until the swap). The cache re-saves ~5 s after edits settle, in **atomic generations** — a reader can never load a half-written mix of old and new.
170
+ - **When the keeper starts:** it **reconciles** — stat-checks every indexed file against its stored fingerprint (~150 ms even at 16k files) plus a git diff against the indexed HEAD — and patches exactly what changed while nothing was watching. A branch switch that used to force a 96-second rebuild on a 16k-file repo is now a ~3-second patch.
171
+ - **As a backstop:** every patch is lint-checked against index invariants (a violation triggers an automatic rebuild and lands in a repair log that `status` shows), and a rotating fingerprint audit after each save catches watcher-missed events.
172
+
173
+ The keeper also stamps the notebook's anchor freshness (a small sidecar, derived single-flight) — the notebook never loads the code index to answer a query.
174
+
175
+ ### Lifecycle commands
176
+
177
+ ```bash
178
+ coldstart status # keepers on this machine: alive? fresh? last patch/rebuild/save? repairs?
179
+ coldstart restart # kill the current repo's keeper (respawns on next lookup)
180
+ coldstart restart --root DIR # kill a specific repo's keeper from anywhere
181
+ coldstart restart --all # kill every keeper
182
+ coldstart index # build + save the cache once, up front (single-writer prep)
183
+ ```
184
+
185
+ `restart` is the right move whenever anything feels stale — a fresh keeper reconciles on start, so it comes back *correct*, not just alive. `status` answers "is my index fresh, and why?": liveness, cache age, the keeper's last reconcile/patch/rebuild/save stamps, and the tail of the repair log — no network probe.
186
+
187
+ ---
188
+
189
+ ## Supported languages
190
+
191
+ Navigation index: TypeScript, JavaScript, JSX/TSX, Vue, Svelte, Astro, AngularJS 1.x, Java, Kotlin, Ruby (Rails-aware: `has_many`/`belongs_to` associations, `routes.rb` resources, controller↔view edges), Python (Django convention edges), Go, Rust, C#, PHP (Laravel convention edges), C++, Groovy (incl. Gradle DSL), GraphQL, YAML, TOML, XML, and `.env` files.
192
+
193
+ **Not indexed:** Swift, Dart — no extension mapping; these files are not walked or parsed.
194
+
195
+ The **notebook works regardless** — its freshness stamps are content-hash based, so notes on a Swift repo are as trustworthy as notes on a TypeScript one (they just lack symbol-level freshness detail).
196
+
197
+ ---
198
+
199
+ ## When *not* to reach for it
200
+
201
+ - A literal string / phrase / regex inside file bodies → **Grep**.
202
+ - Reading an implementation → **Read**, after `gs` gives you the shape.
203
+ - `find` says *"no indexed file contains any of […]"* → those identifiers aren't in the repo. Don't grep spelling variants.
204
+
205
+ ---
206
+
207
+ ## Development
208
+
209
+ ```bash
210
+ npm install
211
+ npm run build
212
+ npm test
213
+
214
+ # run a query from your build:
215
+ node dist/index.js find auth --root .
216
+
217
+ # run the MCP server in a single process (no background keeper) for debugging:
218
+ node dist/index.js --root . --no-daemon
219
+ ```
220
+
221
+ See [PHILOSOPHY.md](./PHILOSOPHY.md) for why coldstart computes no semantics of its own, [ARCHITECTURE.md](./ARCHITECTURE.md) for the index pipeline, process model, and notebook internals, and [TROUBLESHOOTING.md](./TROUBLESHOOTING.md) for recovery procedures.
222
+
223
+ ---
224
+
225
+ ## Limitations
226
+
227
+ 1. It's a routing layer plus an agent-written notebook — no semantic analysis or generated code summaries. This is deliberate: the consuming agent is the semantic layer (see [PHILOSOPHY.md](./PHILOSOPHY.md)).
228
+ 2. `gs` callers are one-hop and file-scoped. Member-expression calls (`this.method()`, `api.method()`) aren't cross-file resolved; named function/constant calls are. Chase further hops by calling `gs` on the caller files.
229
+ 3. Dynamic/computed imports (`import(variable)`) and runtime-DSL references (polymorphic associations, gem/reflection-backed models) stay unresolved.
230
+ 4. Hidden directories and files over 1 MB are skipped by the index.
231
+ 5. The keeper is per-repo and per-machine — no sharing across projects or hosts. The notebook *does* travel: its `.raw` logs are committed and union-merge across branches and machines.
232
+ 6. Notebook quality is bounded by what writing agents actually read — notes are accurate about what they state, but a note is not a proof of completeness.
233
+
234
+ ## License
235
+
236
+ MIT
@@ -0,0 +1,14 @@
1
+ import type { CodebaseIndex } from '../types.js';
2
+ export type LoadProfile = 'find' | 'gs' | 'full';
3
+ export declare function getCacheDir(rootDir: string, baseCacheDir?: string): string;
4
+ /**
5
+ * Refresh ONLY the gitHead/timestamp in meta.json — for the reconcile-fresh
6
+ * case where HEAD moved but every indexed file is unchanged (e.g. committing
7
+ * files the keeper already patched). Without this the stored head lags
8
+ * forever and every reader pays the full HEAD-drift wait on every query.
9
+ * The mtime bump doubles as the "cache advanced" signal those readers poll.
10
+ */
11
+ export declare function updateCachedGitHead(rootDir: string, gitHead: string, baseCacheDir?: string): Promise<void>;
12
+ export declare function saveCachedIndex(index: CodebaseIndex, baseCacheDir?: string): Promise<void>;
13
+ export declare function loadCachedIndex(rootDir: string, baseCacheDir?: string, profile?: LoadProfile): Promise<CodebaseIndex | null>;
14
+ //# sourceMappingURL=disk-cache.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"disk-cache.d.ts","sourceRoot":"","sources":["../../src/cache/disk-cache.ts"],"names":[],"mappings":"AAsCA,OAAO,KAAK,EACV,aAAa,EAEd,MAAM,aAAa,CAAC;AASrB,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG,IAAI,GAAG,MAAM,CAAC;AASjD,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,YAAY,CAAC,EAAE,MAAM,GAAG,MAAM,CAG1E;AA4DD;;;;;;GAMG;AACH,wBAAsB,mBAAmB,CACvC,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,EACf,YAAY,CAAC,EAAE,MAAM,GACpB,OAAO,CAAC,IAAI,CAAC,CAMf;AAED,wBAAsB,eAAe,CACnC,KAAK,EAAE,aAAa,EACpB,YAAY,CAAC,EAAE,MAAM,GACpB,OAAO,CAAC,IAAI,CAAC,CAyJf;AAMD,wBAAsB,eAAe,CACnC,OAAO,EAAE,MAAM,EACf,YAAY,CAAC,EAAE,MAAM,EACrB,OAAO,GAAE,WAAoB,GAC5B,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC,CA4H/B"}
@@ -0,0 +1,371 @@
1
+ /**
2
+ * On-disk index cache, format v18 — consumer-scoped gzipped segments.
3
+ *
4
+ * The old format stored one giant JSON blob (132MB on a 16k-file repo) that
5
+ * every reader parsed in full (~900ms) even though `find` needs a fraction of
6
+ * it. v18 splits the index by CONSUMER and interns every file path once:
7
+ *
8
+ * meta.json version + gitHead + counts. Written LAST (commit marker).
9
+ * table.json.gz fileTable: relativePaths[]. Everything else refers to a
10
+ * file by its integer index in this table ("fileRef").
11
+ * core-<n>.json.gz find+gs: slim per-file tuples (language, line counts,
12
+ * import counts, flags, slim symbols WITHOUT calls[],
13
+ * contentTokens), chunked 5000 files per segment.
14
+ * graph.json.gz find+gs: file edges, out/in adjacency, contentToken
15
+ * postings — all int-ref encoded, SERIALIZED (no longer
16
+ * rebuilt on every load).
17
+ * callgraph.json.gz gs only: symbolEdges, int-ref encoded.
18
+ * build-<n>.json.gz keeper only: domainMap, exports, raw imports, hash,
19
+ * per-symbol calls[], resolver fields — the data needed
20
+ * to PATCH the index, never to query it.
21
+ * buildmeta.json.gz keeper only: tokenDocFreq.
22
+ * fingerprints.json per-file [mtimeMs, size] aligned to fileTable — the
23
+ * stat-walk reconcile backstop.
24
+ *
25
+ * Load profiles: 'find' (table+core+graph) · 'gs' (+callgraph) · 'full'
26
+ * (+build+fingerprints). A partial load fills the untouched fields with
27
+ * empties and stamps index.profile; saveCachedIndex refuses to persist a
28
+ * partial index so a reader can never clobber the keeper's full cache.
29
+ *
30
+ * No TTL: cache validity = format version + git HEAD (checked by callers) +
31
+ * the keeper's live watcher. Time never invalidates a correct index.
32
+ */
33
+ import { readFile, writeFile, mkdir, readdir, rm, rename } from 'node:fs/promises';
34
+ import { join, resolve, basename, sep } from 'node:path';
35
+ import { createHash } from 'node:crypto';
36
+ import { homedir } from 'node:os';
37
+ import { gzip as gzipCb, gunzip as gunzipCb } from 'node:zlib';
38
+ import { promisify } from 'node:util';
39
+ import { CACHE_VERSION } from '../constants.js';
40
+ const gzip = promisify(gzipCb);
41
+ const gunzip = promisify(gunzipCb);
42
+ const DEFAULT_CACHE_DIR = join(homedir(), '.coldstart', 'indexes');
43
+ const FILES_CHUNK_SIZE = 5000;
44
+ /** Compute a stable cache key for a root directory path. */
45
+ function cacheKey(rootDir) {
46
+ const abs = resolve(rootDir);
47
+ const hash = createHash('sha256').update(abs).digest('hex').slice(0, 16);
48
+ return `${basename(abs)}-${hash}`;
49
+ }
50
+ export function getCacheDir(rootDir, baseCacheDir) {
51
+ const base = baseCacheDir ?? DEFAULT_CACHE_DIR;
52
+ return join(base, cacheKey(rootDir));
53
+ }
54
+ const SYMBOL_EDGE_TYPES = ['calls', 'extends', 'implements', 'exports'];
55
+ const RESOLVER_FIELDS = [
56
+ 'constantReferences', 'partialDeclarations', 'eloquentRelations',
57
+ 'containerResolutions', 'djangoConventionRefs', 'submoduleImportCandidates',
58
+ ];
59
+ // ---------------------------------------------------------------------------
60
+ // Save
61
+ // ---------------------------------------------------------------------------
62
+ async function writeGz(path, data) {
63
+ const buf = await gzip(Buffer.from(JSON.stringify(data)));
64
+ // temp + rename: a reader never sees a half-written segment
65
+ const tmp = path + '.tmp';
66
+ await writeFile(tmp, buf);
67
+ await rename(tmp, path);
68
+ }
69
+ async function readGz(path) {
70
+ const buf = await gunzip(await readFile(path));
71
+ return JSON.parse(buf.toString('utf8'));
72
+ }
73
+ /** meta.json is the commit marker — its write must be atomic too. */
74
+ async function writeMetaAtomic(dir, meta) {
75
+ const path = join(dir, 'meta.json');
76
+ const tmp = path + '.tmp-meta';
77
+ await writeFile(tmp, JSON.stringify(meta, null, 2));
78
+ await rename(tmp, path);
79
+ }
80
+ /**
81
+ * Refresh ONLY the gitHead/timestamp in meta.json — for the reconcile-fresh
82
+ * case where HEAD moved but every indexed file is unchanged (e.g. committing
83
+ * files the keeper already patched). Without this the stored head lags
84
+ * forever and every reader pays the full HEAD-drift wait on every query.
85
+ * The mtime bump doubles as the "cache advanced" signal those readers poll.
86
+ */
87
+ export async function updateCachedGitHead(rootDir, gitHead, baseCacheDir) {
88
+ const dir = getCacheDir(rootDir, baseCacheDir);
89
+ const meta = JSON.parse(await readFile(join(dir, 'meta.json'), 'utf-8'));
90
+ meta.gitHead = gitHead;
91
+ meta.timestamp = Date.now();
92
+ await writeMetaAtomic(dir, meta);
93
+ }
94
+ export async function saveCachedIndex(index, baseCacheDir) {
95
+ if (index.profile && index.profile !== 'full') {
96
+ throw new Error(`refusing to save a partial index (profile=${index.profile}) — it would clobber the keeper's full cache`);
97
+ }
98
+ const dir = getCacheDir(index.rootDir, baseCacheDir);
99
+ await mkdir(dir, { recursive: true });
100
+ // Generations: segments are NEVER overwritten in place. Each save writes a
101
+ // fresh `g<N>-` set, commits it by writing meta.json (which names the gen)
102
+ // LAST, then sweeps older generations — keeping the previous one so a
103
+ // reader that grabbed the old meta moments ago still finds every segment
104
+ // it needs. Without this, a save racing a load could serve a mixed-
105
+ // generation index (new file table + old chunk = silently misaligned data).
106
+ let gen = 1;
107
+ try {
108
+ const old = JSON.parse(await readFile(join(dir, 'meta.json'), 'utf-8'));
109
+ if (typeof old.gen === 'number' && Number.isFinite(old.gen))
110
+ gen = old.gen + 1;
111
+ }
112
+ catch { /* no prior cache */ }
113
+ const seg = (name) => join(dir, `g${gen}-${name}`);
114
+ // --- file table + per-file refs -----------------------------------------
115
+ const paths = [];
116
+ const refOf = new Map();
117
+ for (const id of index.files.keys()) {
118
+ refOf.set(id, paths.length);
119
+ paths.push(id);
120
+ }
121
+ // symbol name → index per file, for symbolEdge encoding
122
+ const symIdx = new Map();
123
+ for (const [id, f] of index.files) {
124
+ const m = new Map();
125
+ f.symbols.forEach((s, i) => { if (!m.has(s.name))
126
+ m.set(s.name, i); });
127
+ symIdx.set(id, m);
128
+ }
129
+ const encodeSymRef = (ref) => {
130
+ const fileRef = refOf.get(ref);
131
+ if (fileRef !== undefined)
132
+ return fileRef;
133
+ const hash = ref.indexOf('#');
134
+ if (hash > 0) {
135
+ const fr = refOf.get(ref.slice(0, hash));
136
+ const si = fr !== undefined ? symIdx.get(ref.slice(0, hash)).get(ref.slice(hash + 1)) : undefined;
137
+ if (fr !== undefined && si !== undefined)
138
+ return [fr, si];
139
+ }
140
+ return ref; // unresolvable — keep verbatim
141
+ };
142
+ // --- core + build chunks (aligned to the file table) ---------------------
143
+ const writes = [];
144
+ const fps = [];
145
+ for (let start = 0; start < paths.length; start += FILES_CHUNK_SIZE) {
146
+ const core = [];
147
+ const build = [];
148
+ for (let i = start; i < Math.min(start + FILES_CHUNK_SIZE, paths.length); i++) {
149
+ const f = index.files.get(paths[i]);
150
+ const flags = (f.isBarrel ? 1 : 0) | (f.isTestFile ? 2 : 0) | (f.hasDefaultExport ? 4 : 0);
151
+ core.push([
152
+ f.language, f.lineCount, f.tokenEstimate, f.importedByCount, f.transitiveImportedByCount,
153
+ flags,
154
+ f.symbols.map((s) => [
155
+ s.name, s.kind, s.startLine, s.endLine, s.isExported ? 1 : 0,
156
+ s.extendsName ?? 0, s.implementsNames.length ? s.implementsNames : 0,
157
+ ]),
158
+ f.contentTokens ?? 0,
159
+ ]);
160
+ const extras = {};
161
+ for (const k of RESOLVER_FIELDS)
162
+ if (f[k] !== undefined)
163
+ extras[k] = f[k];
164
+ build.push({
165
+ d: f.domainMap, e: f.exports, i: f.imports, h: f.hash,
166
+ s: f.symbols.map((s) => [
167
+ s.calls.map((c) => [c.name, c.line]),
168
+ s.annotations?.length ? s.annotations : 0,
169
+ ]),
170
+ ...(f.reexportRatio !== undefined ? { rr: f.reexportRatio } : {}),
171
+ ...(f.packageName !== undefined ? { pn: f.packageName } : {}),
172
+ ...(Object.keys(extras).length ? { x: extras } : {}),
173
+ });
174
+ }
175
+ const n = start / FILES_CHUNK_SIZE;
176
+ writes.push(writeGz(seg(`core-${n}.json.gz`), core));
177
+ writes.push(writeGz(seg(`build-${n}.json.gz`), build));
178
+ }
179
+ for (const id of paths) {
180
+ const f = index.files.get(id);
181
+ fps.push([f.mtimeMs ?? 0, f.sizeBytes ?? 0]);
182
+ }
183
+ // --- graph (find+gs) ------------------------------------------------------
184
+ const adj = (m) => {
185
+ const out = paths.map(() => []);
186
+ for (const [k, list] of m) {
187
+ const r = refOf.get(k);
188
+ if (r === undefined)
189
+ continue;
190
+ out[r] = list.map((t) => refOf.get(t)).filter((x) => x !== undefined);
191
+ }
192
+ return out;
193
+ };
194
+ const postings = {};
195
+ for (const [tok, ids] of index.contentTokenPostings) {
196
+ postings[tok] = ids.map((t) => refOf.get(t)).filter((x) => x !== undefined);
197
+ }
198
+ writes.push(writeGz(seg('graph.json.gz'), {
199
+ edges: index.edges
200
+ .filter((e) => refOf.has(e.from) && refOf.has(e.to))
201
+ .map((e) => [refOf.get(e.from), refOf.get(e.to), e.type, e.specifier]),
202
+ out: adj(index.outEdges),
203
+ in: adj(index.inEdges),
204
+ postings,
205
+ }));
206
+ // --- callgraph (gs) -------------------------------------------------------
207
+ writes.push(writeGz(seg('callgraph.json.gz'), {
208
+ symbolEdges: index.symbolEdges.map((e) => [
209
+ encodeSymRef(e.from), encodeSymRef(e.to),
210
+ SYMBOL_EDGE_TYPES.indexOf(e.type), e.line ?? 0,
211
+ ]),
212
+ }));
213
+ // --- keeper-only leftovers + table + fingerprints -------------------------
214
+ writes.push(writeGz(seg('buildmeta.json.gz'), {
215
+ tokenDocFreq: Object.fromEntries(index.tokenDocFreq),
216
+ }));
217
+ writes.push(writeGz(seg('table.json.gz'), { paths }));
218
+ writes.push(writeFile(seg('fingerprints.json'), JSON.stringify(fps)));
219
+ await Promise.all(writes);
220
+ // meta last = commit marker
221
+ const meta = {
222
+ rootDir: index.rootDir,
223
+ gitHead: index.gitHead,
224
+ fileCount: index.files.size,
225
+ timestamp: index.indexedAt,
226
+ version: CACHE_VERSION,
227
+ gen,
228
+ };
229
+ await writeMetaAtomic(dir, meta);
230
+ // Sweep AFTER the commit: drop generations older than the previous one,
231
+ // plus any pre-generation / legacy-format files. keeper-state.json,
232
+ // repair.jsonl, kb-notes.json and meta.json never match.
233
+ try {
234
+ const existing = await readdir(dir);
235
+ const toDelete = existing.filter((f) => {
236
+ const m = /^g(\d+)-/.exec(f);
237
+ if (m)
238
+ return Number(m[1]) <= gen - 2;
239
+ return f === 'index.json' || f === 'graph.json' || f.startsWith('files-')
240
+ || f.endsWith('.gz') || f.endsWith('.gz.tmp') || f.endsWith('.tmp-meta')
241
+ || f === 'fingerprints.json';
242
+ });
243
+ await Promise.all(toDelete.map((f) => rm(join(dir, f), { force: true })));
244
+ }
245
+ catch { /* ignore */ }
246
+ }
247
+ // ---------------------------------------------------------------------------
248
+ // Load
249
+ // ---------------------------------------------------------------------------
250
+ export async function loadCachedIndex(rootDir, baseCacheDir, profile = 'full') {
251
+ const dir = getCacheDir(rootDir, baseCacheDir);
252
+ let meta;
253
+ try {
254
+ meta = JSON.parse(await readFile(join(dir, 'meta.json'), 'utf-8'));
255
+ }
256
+ catch {
257
+ return null;
258
+ }
259
+ if (meta.version !== CACHE_VERSION)
260
+ return null;
261
+ // gen-prefixed segment names (absent on pre-generation v18 caches).
262
+ const seg = (name) => join(dir, typeof meta.gen === 'number' ? `g${meta.gen}-${name}` : name);
263
+ try {
264
+ const { paths } = await readGz(seg('table.json.gz'));
265
+ const chunkCount = Math.ceil(paths.length / FILES_CHUNK_SIZE);
266
+ // Kick off every segment read for this profile concurrently.
267
+ const coreP = Promise.all(Array.from({ length: chunkCount }, (_v, n) => readGz(seg(`core-${n}.json.gz`))));
268
+ const graphP = readGz(seg('graph.json.gz'));
269
+ const callgraphP = profile !== 'find'
270
+ ? readGz(seg('callgraph.json.gz'))
271
+ : null;
272
+ const buildP = profile === 'full'
273
+ ? Promise.all(Array.from({ length: chunkCount }, (_v, n) => readGz(seg(`build-${n}.json.gz`))))
274
+ : null;
275
+ const buildMetaP = profile === 'full'
276
+ ? readGz(seg('buildmeta.json.gz'))
277
+ : null;
278
+ const fpP = profile === 'full'
279
+ ? readFile(seg('fingerprints.json'), 'utf-8').then((raw) => JSON.parse(raw))
280
+ : null;
281
+ const [coreChunks, graph, callgraph, buildChunks, buildMeta, fps] = await Promise.all([
282
+ coreP, graphP, callgraphP, buildP, buildMetaP, fpP,
283
+ ]);
284
+ const rootAbs = meta.rootDir;
285
+ const files = new Map();
286
+ for (let n = 0; n < chunkCount; n++) {
287
+ const core = coreChunks[n];
288
+ const build = buildChunks?.[n];
289
+ for (let j = 0; j < core.length; j++) {
290
+ const idx = n * FILES_CHUNK_SIZE + j;
291
+ const rel = paths[idx];
292
+ const [language, lineCount, tokenEstimate, importedByCount, transitiveImportedByCount, flags, slimSymbols, contentTokens] = core[j];
293
+ const b = build?.[j];
294
+ const symbols = slimSymbols.map((s, si) => ({
295
+ id: `${rel}#${s[0]}`,
296
+ name: s[0], kind: s[1], startLine: s[2], endLine: s[3],
297
+ isExported: s[4] === 1,
298
+ calls: b ? b.s[si][0].map(([name, line]) => ({ name, line })) : [],
299
+ ...(s[5] !== 0 ? { extendsName: s[5] } : {}),
300
+ implementsNames: s[6] === 0 ? [] : s[6],
301
+ ...(b && b.s[si][1] !== 0 ? { annotations: b.s[si][1] } : {}),
302
+ }));
303
+ const file = {
304
+ id: rel,
305
+ path: rootAbs + sep + rel,
306
+ relativePath: rel,
307
+ language,
308
+ domainMap: b ? b.d : {},
309
+ exports: b ? b.e : [],
310
+ hasDefaultExport: (flags & 4) !== 0,
311
+ imports: b ? b.i : [],
312
+ hash: b ? b.h : '',
313
+ lineCount, tokenEstimate, importedByCount, transitiveImportedByCount,
314
+ isBarrel: (flags & 1) !== 0,
315
+ isTestFile: (flags & 2) !== 0,
316
+ symbols,
317
+ ...(b?.rr !== undefined ? { reexportRatio: b.rr } : {}),
318
+ ...(b?.pn !== undefined ? { packageName: b.pn } : {}),
319
+ ...(contentTokens !== 0 ? { contentTokens } : {}),
320
+ ...(fps?.[idx]?.[0] ? { mtimeMs: fps[idx][0], sizeBytes: fps[idx][1] } : {}),
321
+ };
322
+ if (b?.x)
323
+ Object.assign(file, b.x);
324
+ files.set(rel, file);
325
+ }
326
+ }
327
+ const outEdges = new Map();
328
+ const inEdges = new Map();
329
+ graph.out.forEach((list, r) => { if (list.length)
330
+ outEdges.set(paths[r], list.map((t) => paths[t])); });
331
+ graph.in.forEach((list, r) => { if (list.length)
332
+ inEdges.set(paths[r], list.map((t) => paths[t])); });
333
+ const edges = graph.edges.map(([f, t, type, specifier]) => ({
334
+ from: paths[f], to: paths[t], type, specifier,
335
+ }));
336
+ const contentTokenPostings = new Map();
337
+ for (const [tok, refs] of Object.entries(graph.postings)) {
338
+ contentTokenPostings.set(tok, refs.map((r) => paths[r]));
339
+ }
340
+ const decodeSymRef = (ref) => {
341
+ if (typeof ref === 'number')
342
+ return paths[ref];
343
+ if (typeof ref === 'string')
344
+ return ref;
345
+ const [fr, si] = ref;
346
+ return `${paths[fr]}#${files.get(paths[fr]).symbols[si].name}`;
347
+ };
348
+ const symbolEdges = (callgraph?.symbolEdges ?? []).map(([f, t, ty, line]) => ({
349
+ from: decodeSymRef(f), to: decodeSymRef(t),
350
+ type: SYMBOL_EDGE_TYPES[ty],
351
+ ...(line !== 0 ? { line } : {}),
352
+ }));
353
+ return {
354
+ rootDir: meta.rootDir,
355
+ indexedAt: meta.timestamp,
356
+ gitHead: meta.gitHead,
357
+ files,
358
+ edges,
359
+ symbolEdges,
360
+ outEdges,
361
+ inEdges,
362
+ tokenDocFreq: new Map(Object.entries(buildMeta?.tokenDocFreq ?? {})),
363
+ contentTokenPostings,
364
+ ...(profile !== 'full' ? { profile } : {}),
365
+ };
366
+ }
367
+ catch {
368
+ return null; // missing/corrupt segment — caller rebuilds
369
+ }
370
+ }
371
+ //# sourceMappingURL=disk-cache.js.map