@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/init.js ADDED
@@ -0,0 +1,787 @@
1
+ /**
2
+ * coldstart init — wire coldstart into a project.
3
+ *
4
+ * Model: a single `coldstart.md` lives at the repo root and carries ALL the
5
+ * agent-facing guidance. Clients pull it in by reference, so future wording
6
+ * changes touch only coldstart.md, never the client's own rules file.
7
+ *
8
+ * init asks two things (or takes `--experience` / `--client` flags):
9
+ * 1. EXPERIENCE — how the agent invokes coldstart:
10
+ * - `cli` (recommended) → runs `coldstart find` / `coldstart gs` (shell).
11
+ * - `mcp` → calls the `find` / `gs` MCP tools (no shell).
12
+ * This selects the coldstart.md flavor and the hook matcher surface.
13
+ * 2. CLIENT — which tool to wire (never auto-detected; the user always picks):
14
+ * - Claude Code → CLAUDE.md imports `@coldstart.md`; find/gs hooks in
15
+ * `.claude/settings.json`. (MCP experience also writes `.mcp.json`.)
16
+ * - Cursor → `.cursor/rules/coldstart.mdc` references coldstart.md;
17
+ * Cursor-specific navigation + notebook hooks in `.cursor/hooks.json`.
18
+ * (MCP) also writes `.cursor/mcp.json`.
19
+ * - Codex → AGENTS.md points at coldstart.md; Codex-specific
20
+ * navigation + notebook hooks in `.codex/hooks.json`. (MCP) writes
21
+ * `[mcp_servers.coldstart]` into `.codex/config.toml`.
22
+ * - Other → write coldstart.md only; print wiring directions.
23
+ *
24
+ * Claude, Codex, and Cursor receive separate hook entrypoints. They share the
25
+ * protocol-neutral detector core; only input adaptation, the transcript walk,
26
+ * and the output envelope differ per host (Cursor capture parses its own JSONL).
27
+ */
28
+ import * as fs from 'node:fs';
29
+ import * as path from 'node:path';
30
+ import { execFileSync } from 'node:child_process';
31
+ import { createInterface } from 'node:readline/promises';
32
+ import { fileURLToPath } from 'node:url';
33
+ import { ensureKeeper } from './keeper.js';
34
+ import { initSkeleton, logMetric } from './kb/store.js';
35
+ const __filename = fileURLToPath(import.meta.url);
36
+ const DIVIDER = '─'.repeat(60);
37
+ const IMPORT_LINE = '@coldstart.md';
38
+ function out(msg) {
39
+ process.stderr.write(msg + '\n');
40
+ }
41
+ // ---------------------------------------------------------------------------
42
+ // coldstart.md content — two checked-in flavors under templates/, one written
43
+ // per experience. Editing docs = editing plain markdown (templates/coldstart.
44
+ // {cli,mcp}.md), not escaped TS template strings. templates/ ships in the npm
45
+ // package (package.json "files") beside dist/, so it resolves from the running
46
+ // install. Trade-off: the ~shared prose lives in both files — keep them in sync
47
+ // when you edit shared guidance (only the find/gs phrasing + notebook commands
48
+ // genuinely differ between flavors).
49
+ // ---------------------------------------------------------------------------
50
+ /** Absolute path to a flavored coldstart.md template. templates/ sits beside
51
+ * dist/ in both the checkout and the published package (dist/init.js → ..). */
52
+ function coldstartMdTemplatePath(mode) {
53
+ return path.resolve(path.dirname(path.dirname(__filename)), 'templates', `coldstart.${mode}.md`);
54
+ }
55
+ /** The coldstart.md body for a flavor, read from its checked-in template. */
56
+ export function coldstartMd(mode) {
57
+ return fs.readFileSync(coldstartMdTemplatePath(mode), 'utf8');
58
+ }
59
+ // ---------------------------------------------------------------------------
60
+ // Stable install resolution + MCP entry (printed for no-shell / other clients)
61
+ // ---------------------------------------------------------------------------
62
+ /**
63
+ * Package root of the running coldstart install — the dir holding `dist/`,
64
+ * `hooks/`, `templates/`, and `package.json`. Derived from THIS module's own
65
+ * location, so it resolves identically whether we run compiled (`dist/init.js`)
66
+ * or from source (`src/init.ts`); no HOME lookup, no copy.
67
+ *
68
+ * We point hooks and MCP config straight at this live install. `npx` is no
69
+ * longer a supported flow (install is `npm i -g @cstart/coldstart` then `coldstart
70
+ * init`), so the running path is always stable — which makes the old
71
+ * version-pinned copy into `~/.coldstart/versions/<v>/` pure liability:
72
+ * - `npm update -g @cstart/coldstart` is now picked up automatically (no stale snapshot).
73
+ * - `npm uninstall -g @cstart/coldstart` actually disables the wired hooks, instead of
74
+ * a hidden copy that keeps executing on every tool call after removal.
75
+ */
76
+ function installRoot() {
77
+ const root = path.dirname(path.dirname(__filename)); // <install>/dist/init.js → <install>
78
+ if (!fs.existsSync(path.join(root, 'hooks'))) {
79
+ throw new Error(`coldstart install at ${root} is missing hooks/ — reinstall coldstart`);
80
+ }
81
+ return root;
82
+ }
83
+ function mcpServerEntry(cwd) {
84
+ const entryPath = path.join(installRoot(), 'dist', 'index.js');
85
+ return { command: 'node', args: [entryPath, '--root', cwd] };
86
+ }
87
+ /**
88
+ * Absolute path to the shipped hooks dir (sibling of `dist/`) in the running
89
+ * install. The path written into settings.json / hooks.json points at the live
90
+ * install, so uninstalling coldstart disables the hooks (see installRoot).
91
+ */
92
+ function resolveHooksDir() {
93
+ return path.join(installRoot(), 'hooks');
94
+ }
95
+ // ---------------------------------------------------------------------------
96
+ // Notebook (kb) setup — skeleton, git wiring, and the Claude recall/capture
97
+ // hooks. Shared by `coldstart init` (always) and the `coldstart kb init` alias.
98
+ // ---------------------------------------------------------------------------
99
+ // The notebook recall (UserPromptSubmit) + capture (Stop/SubagentStop) hook
100
+ // entry files, in the shipped hooks/ dir. Distinct from the find/gs hooks, so
101
+ // they merge into settings.json independently.
102
+ const KB_HOOK_RECALL = 'kb-recall.mjs';
103
+ const KB_HOOK_ELICIT = 'kb-elicit.mjs';
104
+ /** True if a hook-array entry is one of OUR kb hooks (by filename) — so re-init
105
+ * strips + refreshes instead of duplicating. */
106
+ function isKbHookEntry(entry) {
107
+ const hooks = entry?.hooks;
108
+ if (!Array.isArray(hooks))
109
+ return false;
110
+ return hooks.some((h) => {
111
+ const cmd = h?.command;
112
+ return typeof cmd === 'string' && (cmd.includes(KB_HOOK_RECALL) || cmd.includes(KB_HOOK_ELICIT));
113
+ });
114
+ }
115
+ /**
116
+ * Merge the notebook recall/capture hooks into `.claude/settings.json`.
117
+ * Idempotent (strips prior kb entries, re-adds), preserves the find/gs hooks
118
+ * and any foreign entries, fail-safe on invalid JSON. Points at the same
119
+ * version-pinned stable hooks dir the find/gs hooks use (survives npm update).
120
+ */
121
+ export function wireClaudeKbHooks(cwd) {
122
+ let hooksDir;
123
+ try {
124
+ hooksDir = resolveHooksDir();
125
+ }
126
+ catch (e) {
127
+ return { error: `could not resolve a stable install path (${e})` };
128
+ }
129
+ const dir = path.join(cwd, '.claude');
130
+ const filePath = path.join(dir, 'settings.json');
131
+ let settings = {};
132
+ const existed = fs.existsSync(filePath);
133
+ if (existed) {
134
+ try {
135
+ const parsed = JSON.parse(fs.readFileSync(filePath, 'utf8'));
136
+ if (parsed && typeof parsed === 'object')
137
+ settings = parsed;
138
+ }
139
+ catch {
140
+ return { error: `${filePath} is not valid JSON — left untouched; wire the kb hooks manually` };
141
+ }
142
+ }
143
+ const hooksCfg = (settings.hooks && typeof settings.hooks === 'object' ? settings.hooks : {});
144
+ const stripOurs = (arr) => (Array.isArray(arr) ? arr : []).filter((e) => !isKbHookEntry(e));
145
+ const entry = (file) => ({ hooks: [{ type: 'command', command: `node ${path.join(hooksDir, file)}` }] });
146
+ hooksCfg.UserPromptSubmit = [...stripOurs(hooksCfg.UserPromptSubmit), entry(KB_HOOK_RECALL)];
147
+ hooksCfg.Stop = [...stripOurs(hooksCfg.Stop), entry(KB_HOOK_ELICIT)];
148
+ hooksCfg.SubagentStop = [...stripOurs(hooksCfg.SubagentStop), entry(KB_HOOK_ELICIT)];
149
+ settings.hooks = hooksCfg;
150
+ fs.mkdirSync(dir, { recursive: true });
151
+ fs.writeFileSync(filePath, JSON.stringify(settings, null, 2) + '\n');
152
+ return existed ? 'updated' : 'created';
153
+ }
154
+ /** Is any `.raw` note file already git-tracked? If so the repo is (or was)
155
+ * sharing its notebook — adding an ignore line would create a confusing
156
+ * tracked-but-ignored divergence, so we leave it shared. */
157
+ function notebookRawTracked(cwd) {
158
+ try {
159
+ const listed = execFileSync('git', ['ls-files', '.coldstart/notebook/.raw'], {
160
+ cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'],
161
+ });
162
+ return listed.trim().length > 0;
163
+ }
164
+ catch {
165
+ return false; // not a git repo, or git unavailable — treat as untracked
166
+ }
167
+ }
168
+ const NOTEBOOK_IGNORE_LINE = '.coldstart/';
169
+ const hasNotebookIgnore = (text) => text.split('\n').some((l) => { const t = l.trim(); return t === '.coldstart/' || t === '.coldstart'; });
170
+ /** Privacy default: add `.coldstart/` to the repo's root .gitignore so the
171
+ * notebook stays local. Skipped when `.raw` is already tracked (don't flip an
172
+ * existing shared repo). */
173
+ function addNotebookGitignore(cwd) {
174
+ if (notebookRawTracked(cwd))
175
+ return 'kept-shared';
176
+ const giPath = path.join(cwd, '.gitignore');
177
+ let gi = '';
178
+ try {
179
+ gi = fs.existsSync(giPath) ? fs.readFileSync(giPath, 'utf8') : '';
180
+ }
181
+ catch { /* create below */ }
182
+ if (hasNotebookIgnore(gi))
183
+ return 'present';
184
+ fs.appendFileSync(giPath, (gi && !gi.endsWith('\n') ? '\n' : '') + NOTEBOOK_IGNORE_LINE + '\n');
185
+ return 'added';
186
+ }
187
+ /** Opt-in to share: drop the `.coldstart/` ignore line if we (or the user) added
188
+ * it, so `.raw` becomes committable. The inner notebook .gitignore still keeps
189
+ * notes/ + .metrics/ out. */
190
+ function removeNotebookGitignore(cwd) {
191
+ const giPath = path.join(cwd, '.gitignore');
192
+ if (!fs.existsSync(giPath))
193
+ return 'absent';
194
+ const lines = fs.readFileSync(giPath, 'utf8').split('\n');
195
+ const kept = lines.filter((l) => { const t = l.trim(); return t !== '.coldstart/' && t !== '.coldstart'; });
196
+ if (kept.length === lines.length)
197
+ return 'absent';
198
+ fs.writeFileSync(giPath, kept.join('\n'));
199
+ return 'removed';
200
+ }
201
+ /** merge=union on the append-only .raw logs — concurrent appends conflict at
202
+ * EOF under naive merge; union keeps both sides and the ts-sorted fold makes
203
+ * interleave order irrelevant. */
204
+ function ensureGitattributes(cwd) {
205
+ const gaPath = path.join(cwd, '.gitattributes');
206
+ const line = '.coldstart/notebook/.raw/*.jsonl merge=union';
207
+ let ga = '';
208
+ try {
209
+ ga = fs.existsSync(gaPath) ? fs.readFileSync(gaPath, 'utf8') : '';
210
+ }
211
+ catch { /* create below */ }
212
+ if (ga.includes(line))
213
+ return;
214
+ try {
215
+ fs.appendFileSync(gaPath, (ga && !ga.endsWith('\n') ? '\n' : '') + line + '\n');
216
+ }
217
+ catch (e) {
218
+ out(` (could not write .gitattributes: ${e instanceof Error ? e.message : e})`);
219
+ }
220
+ }
221
+ /**
222
+ * Notebook file setup — client-agnostic. Creates the skeleton, sets the
223
+ * merge=union attribute, and applies the storage choice: private by default
224
+ * (gitignore `.coldstart/`), or committable when `commit` is set. Does NOT wire
225
+ * hooks — those are Claude-specific (see wireClaudeKbHooks).
226
+ */
227
+ export function setupNotebook(cwd, commit) {
228
+ initSkeleton(cwd);
229
+ ensureGitattributes(cwd);
230
+ if (commit) {
231
+ const r = removeNotebookGitignore(cwd);
232
+ out(` notebook — shared: .raw + okf.yaml committable, publish with \`coldstart kb commit\`${r === 'removed' ? ' (removed prior ignore rule)' : ''}`);
233
+ }
234
+ else {
235
+ const r = addNotebookGitignore(cwd);
236
+ const note = r === 'kept-shared' ? 'left shared (.raw already git-tracked)'
237
+ : r === 'added' ? 'private: .coldstart/ gitignored (share later with `coldstart init --commit-notebook`)'
238
+ : 'private: .coldstart/ already gitignored';
239
+ out(` notebook — ${note}`);
240
+ }
241
+ logMetric(cwd, 'capture', { event: 'init' });
242
+ }
243
+ // ---------------------------------------------------------------------------
244
+ // Writers
245
+ // ---------------------------------------------------------------------------
246
+ export function writeColdstartMd(cwd, mode) {
247
+ const filePath = path.join(cwd, 'coldstart.md');
248
+ const existed = fs.existsSync(filePath);
249
+ fs.writeFileSync(filePath, coldstartMd(mode));
250
+ return existed ? 'updated' : 'created';
251
+ }
252
+ /** Ensure CLAUDE.md exists and imports coldstart.md via `@coldstart.md`. */
253
+ export function wireClaudeImport(cwd) {
254
+ const filePath = path.join(cwd, 'CLAUDE.md');
255
+ if (!fs.existsSync(filePath)) {
256
+ fs.writeFileSync(filePath, `# Project guidance\n\n${IMPORT_LINE}\n`);
257
+ return 'created';
258
+ }
259
+ const existing = fs.readFileSync(filePath, 'utf8');
260
+ if (existing.includes(IMPORT_LINE))
261
+ return 'present';
262
+ const sep = existing.endsWith('\n') ? '' : '\n';
263
+ fs.appendFileSync(filePath, `${sep}\n${IMPORT_LINE}\n`);
264
+ return 'added';
265
+ }
266
+ // Hook entry files (in the shipped hooks/ dir). The PostToolUse nudge fires
267
+ // search-behaviour advice; the PreToolUse guard denies an exact find re-run.
268
+ const HOOK_PRE = 'find-preguard.mjs';
269
+ const HOOK_POST = 'find-nudge.mjs';
270
+ const CODEX_HOOK_PRE = 'codex-find-preguard.mjs';
271
+ const CODEX_HOOK_POST = 'codex-find-nudge.mjs';
272
+ const CODEX_KB_HOOK_RECALL = 'codex-kb-recall.mjs';
273
+ const CODEX_KB_HOOK_ELICIT = 'codex-kb-elicit.mjs';
274
+ const CURSOR_HOOK_PRE = 'cursor-find-preguard.mjs';
275
+ const CURSOR_HOOK_POST = 'cursor-find-nudge.mjs';
276
+ const CURSOR_KB_HOOK_RECALL = 'cursor-kb-recall.mjs';
277
+ const CURSOR_KB_HOOK_ELICIT = 'cursor-kb-elicit.mjs';
278
+ // PreToolUse matcher — surface-agnostic: fires for the CLI `coldstart find`
279
+ // (Bash) AND the `mcp__coldstart__find` tool. A plain regex alternation, so it
280
+ // works in both Claude's and Codex's matcher engine unchanged.
281
+ const PRE_MATCHER = 'Bash|mcp__coldstart__find';
282
+ /** True if a hook-array entry is one WE wrote (by entry filename), so re-running
283
+ * init can strip + refresh it instead of duplicating it. */
284
+ function isColdstartHookEntry(entry) {
285
+ const hooks = entry?.hooks;
286
+ if (!Array.isArray(hooks))
287
+ return false;
288
+ return hooks.some((h) => {
289
+ const cmd = h?.command;
290
+ return typeof cmd === 'string' && (cmd.includes(HOOK_PRE) || cmd.includes(HOOK_POST));
291
+ });
292
+ }
293
+ function isCodexHookEntry(entry) {
294
+ const hooks = entry?.hooks;
295
+ if (!Array.isArray(hooks))
296
+ return false;
297
+ const owned = [HOOK_PRE, HOOK_POST, KB_HOOK_RECALL, KB_HOOK_ELICIT,
298
+ CODEX_HOOK_PRE, CODEX_HOOK_POST, CODEX_KB_HOOK_RECALL, CODEX_KB_HOOK_ELICIT];
299
+ return hooks.some((h) => {
300
+ const cmd = h?.command;
301
+ return typeof cmd === 'string' && owned.some((file) => cmd.includes(file));
302
+ });
303
+ }
304
+ /**
305
+ * Build the coldstart hook entries (find-dedup guard + behavioral nudge).
306
+ * `postMatcher` differs by engine: Claude uses `*` for match-all, Codex uses the
307
+ * regex `.*`. Everything else is identical — the shipped handlers are the same.
308
+ */
309
+ function coldstartHooks(hooksDir, postMatcher) {
310
+ const preCmd = `node ${path.join(hooksDir, HOOK_PRE)}`;
311
+ const postCmd = `node ${path.join(hooksDir, HOOK_POST)}`;
312
+ return {
313
+ PreToolUse: [{ matcher: PRE_MATCHER, hooks: [{ type: 'command', command: preCmd }] }],
314
+ PostToolUse: [{ matcher: postMatcher, hooks: [{ type: 'command', command: postCmd }] }],
315
+ };
316
+ }
317
+ /** Merge our hook entries into an existing `hooks` config object, stripping any
318
+ * prior coldstart entries first (idempotent re-run) and preserving foreign ones. */
319
+ function mergeHooks(hooksCfg, entries) {
320
+ const stripOurs = (arr) => (Array.isArray(arr) ? arr : []).filter((e) => !isColdstartHookEntry(e));
321
+ hooksCfg.PreToolUse = [...stripOurs(hooksCfg.PreToolUse), ...entries.PreToolUse];
322
+ hooksCfg.PostToolUse = [...stripOurs(hooksCfg.PostToolUse), ...entries.PostToolUse];
323
+ }
324
+ /**
325
+ * Register the find/gs search hooks in the project's `.claude/settings.json`.
326
+ *
327
+ * Merges (never clobbers): preserves every other setting and any non-coldstart
328
+ * hooks. Idempotent — strips our prior entries (matched by entry filename) and
329
+ * re-adds them, so a re-run refreshes a stale hook path without duplicating.
330
+ * Fail-safe — if settings.json exists but is not valid JSON, we leave it alone
331
+ * and report, rather than overwrite a file the user owns.
332
+ */
333
+ export function wireClaudeHooks(cwd) {
334
+ let hooksDir;
335
+ try {
336
+ hooksDir = resolveHooksDir();
337
+ }
338
+ catch (e) {
339
+ return { error: `could not resolve a stable install path (${e})` };
340
+ }
341
+ const dir = path.join(cwd, '.claude');
342
+ const filePath = path.join(dir, 'settings.json');
343
+ let settings = {};
344
+ const existed = fs.existsSync(filePath);
345
+ if (existed) {
346
+ try {
347
+ const parsed = JSON.parse(fs.readFileSync(filePath, 'utf8'));
348
+ if (parsed && typeof parsed === 'object')
349
+ settings = parsed;
350
+ }
351
+ catch {
352
+ return { error: `${filePath} is not valid JSON — left untouched; wire hooks manually` };
353
+ }
354
+ }
355
+ const hooksCfg = settings.hooks && typeof settings.hooks === 'object'
356
+ ? settings.hooks
357
+ : {};
358
+ mergeHooks(hooksCfg, coldstartHooks(hooksDir, '*'));
359
+ settings.hooks = hooksCfg;
360
+ fs.mkdirSync(dir, { recursive: true });
361
+ fs.writeFileSync(filePath, JSON.stringify(settings, null, 2) + '\n');
362
+ return existed ? 'updated' : 'created';
363
+ }
364
+ /**
365
+ * Register Codex-specific navigation and notebook hooks. Event envelopes are
366
+ * intentionally handled by separate files so Codex rollout/subagent behavior
367
+ * can evolve without risking the Claude integration.
368
+ */
369
+ export function wireCodexHooks(cwd) {
370
+ let hooksDir;
371
+ try {
372
+ hooksDir = resolveHooksDir();
373
+ }
374
+ catch (e) {
375
+ return { error: `could not resolve a stable install path (${e})` };
376
+ }
377
+ const dir = path.join(cwd, '.codex');
378
+ const filePath = path.join(dir, 'hooks.json');
379
+ let config = {};
380
+ const existed = fs.existsSync(filePath);
381
+ if (existed) {
382
+ try {
383
+ const parsed = JSON.parse(fs.readFileSync(filePath, 'utf8'));
384
+ if (parsed && typeof parsed === 'object')
385
+ config = parsed;
386
+ }
387
+ catch {
388
+ return { error: `${filePath} is not valid JSON — left untouched; wire hooks manually` };
389
+ }
390
+ }
391
+ const hooksCfg = config.hooks && typeof config.hooks === 'object'
392
+ ? config.hooks
393
+ : {};
394
+ const stripOurs = (arr) => (Array.isArray(arr) ? arr : []).filter((entry) => !isCodexHookEntry(entry));
395
+ const command = (file) => ({ type: 'command', command: `node ${path.join(hooksDir, file)}` });
396
+ hooksCfg.PreToolUse = [
397
+ ...stripOurs(hooksCfg.PreToolUse),
398
+ { matcher: PRE_MATCHER, hooks: [command(CODEX_HOOK_PRE)] },
399
+ ];
400
+ hooksCfg.PostToolUse = [
401
+ ...stripOurs(hooksCfg.PostToolUse),
402
+ { matcher: '.*', hooks: [command(CODEX_HOOK_POST)] },
403
+ ];
404
+ hooksCfg.UserPromptSubmit = [
405
+ ...stripOurs(hooksCfg.UserPromptSubmit),
406
+ { hooks: [command(CODEX_KB_HOOK_RECALL)] },
407
+ ];
408
+ hooksCfg.Stop = [
409
+ ...stripOurs(hooksCfg.Stop),
410
+ { hooks: [command(CODEX_KB_HOOK_ELICIT)] },
411
+ ];
412
+ hooksCfg.SubagentStop = [
413
+ ...stripOurs(hooksCfg.SubagentStop),
414
+ { matcher: '.*', hooks: [command(CODEX_KB_HOOK_ELICIT)] },
415
+ ];
416
+ config.hooks = hooksCfg;
417
+ fs.mkdirSync(dir, { recursive: true });
418
+ fs.writeFileSync(filePath, JSON.stringify(config, null, 2) + '\n');
419
+ return existed ? 'updated' : 'created';
420
+ }
421
+ /** True if a `.cursor/hooks.json` entry (`{command}`) is one WE wrote. Cursor's
422
+ * entry shape is flatter than Codex's ({command} vs {matcher,hooks:[…]}), so it
423
+ * gets its own detector. */
424
+ function isCursorHookEntry(entry) {
425
+ const cmd = entry?.command;
426
+ const owned = [CURSOR_HOOK_PRE, CURSOR_HOOK_POST, CURSOR_KB_HOOK_RECALL, CURSOR_KB_HOOK_ELICIT];
427
+ return typeof cmd === 'string' && owned.some((file) => cmd.includes(file));
428
+ }
429
+ /**
430
+ * Register Cursor navigation + notebook hooks in `.cursor/hooks.json`.
431
+ *
432
+ * Cursor's config differs from Codex/Claude: a top-level `version` + a `hooks`
433
+ * map whose events hold FLAT `{command}` entries (no matcher / no nested hooks).
434
+ * `preToolUse`/`postToolUse` are generic (fire for Shell + Read + MCP), so no
435
+ * matcher is needed — the handler filters. The SAME hooks serve both experiences;
436
+ * the handlers normalize a Shell `coldstart find` (cli) and an MCP find (mcp) to
437
+ * one shape, mirroring Codex. Recall rides `beforeSubmitPrompt` (Cursor honors
438
+ * its `additional_context` despite the docs); capture rides `stop`+`subagentStop`.
439
+ *
440
+ * Merges idempotently (strips our prior entries by filename, preserves foreign
441
+ * ones) and is fail-safe on invalid JSON.
442
+ */
443
+ export function wireCursorHooks(cwd) {
444
+ let hooksDir;
445
+ try {
446
+ hooksDir = resolveHooksDir();
447
+ }
448
+ catch (e) {
449
+ return { error: `could not resolve a stable install path (${e})` };
450
+ }
451
+ const dir = path.join(cwd, '.cursor');
452
+ const filePath = path.join(dir, 'hooks.json');
453
+ let config = {};
454
+ const existed = fs.existsSync(filePath);
455
+ if (existed) {
456
+ try {
457
+ const parsed = JSON.parse(fs.readFileSync(filePath, 'utf8'));
458
+ if (parsed && typeof parsed === 'object')
459
+ config = parsed;
460
+ }
461
+ catch {
462
+ return { error: `${filePath} is not valid JSON — left untouched; wire hooks manually` };
463
+ }
464
+ }
465
+ const hooksCfg = config.hooks && typeof config.hooks === 'object'
466
+ ? config.hooks
467
+ : {};
468
+ const stripOurs = (arr) => (Array.isArray(arr) ? arr : []).filter((entry) => !isCursorHookEntry(entry));
469
+ const command = (file) => ({ command: `node ${path.join(hooksDir, file)}` });
470
+ hooksCfg.preToolUse = [...stripOurs(hooksCfg.preToolUse), command(CURSOR_HOOK_PRE)];
471
+ hooksCfg.postToolUse = [...stripOurs(hooksCfg.postToolUse), command(CURSOR_HOOK_POST)];
472
+ hooksCfg.beforeSubmitPrompt = [...stripOurs(hooksCfg.beforeSubmitPrompt), command(CURSOR_KB_HOOK_RECALL)];
473
+ hooksCfg.stop = [...stripOurs(hooksCfg.stop), command(CURSOR_KB_HOOK_ELICIT)];
474
+ hooksCfg.subagentStop = [...stripOurs(hooksCfg.subagentStop), command(CURSOR_KB_HOOK_ELICIT)];
475
+ config.hooks = hooksCfg;
476
+ if (config.version === undefined)
477
+ config.version = 1;
478
+ fs.mkdirSync(dir, { recursive: true });
479
+ fs.writeFileSync(filePath, JSON.stringify(config, null, 2) + '\n');
480
+ return existed ? 'updated' : 'created';
481
+ }
482
+ // ---------------------------------------------------------------------------
483
+ // Rules-file writers — each references coldstart.md, never duplicates it
484
+ // ---------------------------------------------------------------------------
485
+ /** Write `.cursor/rules/coldstart.mdc` — an always-applied rule that pulls in
486
+ * coldstart.md via Cursor's `@file` reference. Overwritten on re-run (it owns
487
+ * this file), so guidance edits flow through coldstart.md. */
488
+ export function wireCursorRule(cwd) {
489
+ const dir = path.join(cwd, '.cursor', 'rules');
490
+ const filePath = path.join(dir, 'coldstart.mdc');
491
+ const existed = fs.existsSync(filePath);
492
+ const body = `---
493
+ description: coldstart — fast codebase navigation (find/gs) before grep/read
494
+ alwaysApply: true
495
+ ---
496
+
497
+ Before grepping or reading files to orient in this codebase, use coldstart
498
+ (\`find\` to locate files, \`gs\` to inspect one). Full guidance:
499
+
500
+ @coldstart.md
501
+ `;
502
+ fs.mkdirSync(dir, { recursive: true });
503
+ fs.writeFileSync(filePath, body);
504
+ return existed ? 'updated' : 'created';
505
+ }
506
+ const AGENTS_START = '<!-- coldstart:start -->';
507
+ const AGENTS_END = '<!-- coldstart:end -->';
508
+ /** Ensure AGENTS.md carries a coldstart section pointing at coldstart.md.
509
+ * AGENTS.md has no import directive, so we inject a marked block and refresh it
510
+ * in place on re-run (idempotent), preserving everything else in the file. */
511
+ export function wireCodexAgents(cwd) {
512
+ const filePath = path.join(cwd, 'AGENTS.md');
513
+ const block = `${AGENTS_START}
514
+ ## Codebase navigation (coldstart)
515
+
516
+ Before grepping or reading files to orient in this repo, use coldstart. Read
517
+ \`coldstart.md\` at the repo root for the find/gs workflow and follow it.
518
+ ${AGENTS_END}`;
519
+ if (!fs.existsSync(filePath)) {
520
+ fs.writeFileSync(filePath, `# AGENTS.md\n\n${block}\n`);
521
+ return 'created';
522
+ }
523
+ const existing = fs.readFileSync(filePath, 'utf8');
524
+ const start = existing.indexOf(AGENTS_START);
525
+ if (start !== -1) {
526
+ const end = existing.indexOf(AGENTS_END, start);
527
+ if (end !== -1) {
528
+ const next = existing.slice(0, start) + block + existing.slice(end + AGENTS_END.length);
529
+ fs.writeFileSync(filePath, next);
530
+ return 'updated';
531
+ }
532
+ }
533
+ const sep = existing.endsWith('\n') ? '' : '\n';
534
+ fs.appendFileSync(filePath, `${sep}\n${block}\n`);
535
+ return 'updated';
536
+ }
537
+ // ---------------------------------------------------------------------------
538
+ // MCP config writers (only used for the `mcp` experience)
539
+ // ---------------------------------------------------------------------------
540
+ /** Merge `{ mcpServers: { coldstart: entry } }` into a JSON MCP config (used for
541
+ * Claude's `.mcp.json` and Cursor's `.cursor/mcp.json` — identical shape).
542
+ * Fail-safe on invalid JSON; preserves other servers and keys. */
543
+ export function wireJsonMcp(cwd, relFile, entry) {
544
+ const filePath = path.join(cwd, relFile);
545
+ let config = {};
546
+ const existed = fs.existsSync(filePath);
547
+ if (existed) {
548
+ try {
549
+ const parsed = JSON.parse(fs.readFileSync(filePath, 'utf8'));
550
+ if (parsed && typeof parsed === 'object')
551
+ config = parsed;
552
+ }
553
+ catch {
554
+ return { error: `${filePath} is not valid JSON — left untouched; add the MCP server manually` };
555
+ }
556
+ }
557
+ const servers = config.mcpServers && typeof config.mcpServers === 'object'
558
+ ? config.mcpServers
559
+ : {};
560
+ servers.coldstart = entry;
561
+ config.mcpServers = servers;
562
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
563
+ fs.writeFileSync(filePath, JSON.stringify(config, null, 2) + '\n');
564
+ return existed ? 'updated' : 'created';
565
+ }
566
+ /** Remove an existing `[mcp_servers.coldstart]` table (and any sub-tables) from
567
+ * TOML text, so we can append a fresh one without duplicating. Conservative:
568
+ * drops the header line and everything until the next top-level `[` or EOF. */
569
+ function stripCodexColdstartTable(toml) {
570
+ const lines = toml.split('\n');
571
+ const out = [];
572
+ let skipping = false;
573
+ const isColdstartHeader = (l) => /^\s*\[\[?mcp_servers\.coldstart(\..*)?\]\]?\s*$/.test(l);
574
+ const isTableHeader = (l) => /^\s*\[\[?[^\]]+\]\]?\s*$/.test(l);
575
+ for (const line of lines) {
576
+ if (skipping) {
577
+ if (isColdstartHeader(line))
578
+ continue; // a coldstart sub-table — keep skipping
579
+ if (isTableHeader(line))
580
+ skipping = false; // a different table — stop skipping
581
+ else
582
+ continue; // body of the coldstart table — drop
583
+ }
584
+ if (isColdstartHeader(line)) {
585
+ skipping = true;
586
+ continue;
587
+ }
588
+ out.push(line);
589
+ }
590
+ return out.join('\n');
591
+ }
592
+ /** Write `[mcp_servers.coldstart]` into `.codex/config.toml` (Codex's MCP config).
593
+ * Hand-rolled minimal TOML merge — no dependency: strip a prior coldstart table,
594
+ * then append a fresh one. Other config is preserved verbatim. */
595
+ export function wireCodexMcp(cwd, entry) {
596
+ const dir = path.join(cwd, '.codex');
597
+ const filePath = path.join(dir, 'config.toml');
598
+ const existed = fs.existsSync(filePath);
599
+ let content = existed ? fs.readFileSync(filePath, 'utf8') : '';
600
+ content = stripCodexColdstartTable(content).replace(/\n+$/, '');
601
+ const args = entry.args.map((a) => JSON.stringify(a)).join(', ');
602
+ const block = `[mcp_servers.coldstart]\ncommand = ${JSON.stringify(entry.command)}\nargs = [${args}]\n`;
603
+ const next = content.length ? `${content}\n\n${block}` : block;
604
+ fs.mkdirSync(dir, { recursive: true });
605
+ fs.writeFileSync(filePath, next);
606
+ return existed ? 'updated' : 'created';
607
+ }
608
+ const FLAVOR = (exp) => (exp === 'cli' ? 'CLI flavor' : 'MCP flavor');
609
+ /** Resolve + report the MCP entry, or null + a printed reason on failure. */
610
+ function mcpEntryOrNull(cwd) {
611
+ try {
612
+ return mcpServerEntry(cwd);
613
+ }
614
+ catch (e) {
615
+ out(` (could not resolve a stable install path for the MCP server: ${e})`);
616
+ return null;
617
+ }
618
+ }
619
+ function reportHooks(label, res) {
620
+ if (typeof res === 'object')
621
+ out(` ${label} — search hooks NOT wired: ${res.error}`);
622
+ else
623
+ out(` ${label} — ${res} find/gs search hooks (PreToolUse + PostToolUse)`);
624
+ }
625
+ function setupClaude(cwd, exp) {
626
+ out(` coldstart.md — ${writeColdstartMd(cwd, exp)} (${FLAVOR(exp)})`);
627
+ const imp = wireClaudeImport(cwd);
628
+ out(` CLAUDE.md — ${imp === 'present' ? 'already imports @coldstart.md' : `${imp} @coldstart.md import`}`);
629
+ if (exp === 'mcp') {
630
+ const entry = mcpEntryOrNull(cwd);
631
+ if (entry) {
632
+ const r = wireJsonMcp(cwd, '.mcp.json', entry);
633
+ out(typeof r === 'object' ? ` .mcp.json — NOT written: ${r.error}` : ` .mcp.json — ${r} coldstart MCP server`);
634
+ }
635
+ }
636
+ reportHooks('settings.json', wireClaudeHooks(cwd));
637
+ const kb = wireClaudeKbHooks(cwd);
638
+ out(typeof kb === 'object'
639
+ ? ` settings.json — notebook hooks NOT wired: ${kb.error}`
640
+ : ` settings.json — ${kb} notebook recall + capture hooks (UserPromptSubmit + Stop/SubagentStop)`);
641
+ }
642
+ function setupCodex(cwd, exp) {
643
+ out(` coldstart.md — ${writeColdstartMd(cwd, exp)} (${FLAVOR(exp)})`);
644
+ out(` AGENTS.md — ${wireCodexAgents(cwd)} coldstart navigation section`);
645
+ if (exp === 'mcp') {
646
+ const entry = mcpEntryOrNull(cwd);
647
+ if (entry)
648
+ out(` config.toml — ${wireCodexMcp(cwd, entry)} [mcp_servers.coldstart]`);
649
+ }
650
+ const hooks = wireCodexHooks(cwd);
651
+ out(typeof hooks === 'object'
652
+ ? ` hooks.json — Codex hooks NOT wired: ${hooks.error}`
653
+ : ` hooks.json — ${hooks} Codex navigation + notebook hooks`);
654
+ }
655
+ function setupCursor(cwd, exp) {
656
+ out(` coldstart.md — ${writeColdstartMd(cwd, exp)} (${FLAVOR(exp)})`);
657
+ out(` coldstart.mdc — ${wireCursorRule(cwd)} .cursor/rules rule (references @coldstart.md)`);
658
+ if (exp === 'mcp') {
659
+ const entry = mcpEntryOrNull(cwd);
660
+ if (entry) {
661
+ const r = wireJsonMcp(cwd, path.join('.cursor', 'mcp.json'), entry);
662
+ out(typeof r === 'object' ? ` .cursor/mcp.json — NOT written: ${r.error}` : ` mcp.json — ${r} .cursor coldstart MCP server`);
663
+ }
664
+ }
665
+ const hooks = wireCursorHooks(cwd);
666
+ out(typeof hooks === 'object'
667
+ ? ` hooks.json — Cursor hooks NOT wired: ${hooks.error}`
668
+ : ` hooks.json — ${hooks} Cursor navigation + notebook hooks`);
669
+ }
670
+ function setupOther(cwd, exp) {
671
+ out(` coldstart.md — ${writeColdstartMd(cwd, exp)} (${FLAVOR(exp)})`);
672
+ out('');
673
+ out(' Wire it into your client:');
674
+ out(' 1. Add coldstart.md to your client\'s rules / instructions / skill');
675
+ out(' (whatever your app uses — point it at this file or paste its contents).');
676
+ if (exp === 'mcp') {
677
+ out(' 2. Add the MCP server entry to your client\'s MCP config:');
678
+ out('');
679
+ const entry = mcpEntryOrNull(cwd);
680
+ if (entry) {
681
+ out(' ' + JSON.stringify({ mcpServers: { coldstart: entry } }, null, 2).split('\n').join('\n '));
682
+ }
683
+ }
684
+ else {
685
+ out(' 2. Make sure the `coldstart` CLI is on PATH so the agent can run');
686
+ out(' `coldstart find` / `coldstart gs` (npm i -g @cstart/coldstart).');
687
+ }
688
+ out('');
689
+ out(' Note: behavioral hooks need a supported host. coldstart ships separate');
690
+ out(' hook implementations for Claude Code and Codex.');
691
+ }
692
+ // ---------------------------------------------------------------------------
693
+ // Entry point
694
+ // ---------------------------------------------------------------------------
695
+ /** Read `--flag value` or `--flag=value` from an argv slice. */
696
+ function readFlag(argv, flag) {
697
+ for (let i = 0; i < argv.length; i++) {
698
+ const a = argv[i];
699
+ if (a === flag)
700
+ return argv[i + 1]?.trim().toLowerCase();
701
+ if (a.startsWith(`${flag}=`))
702
+ return a.slice(flag.length + 1).trim().toLowerCase();
703
+ }
704
+ return undefined;
705
+ }
706
+ function parseExperience(v) {
707
+ if (v === 'cli' || v === '1')
708
+ return 'cli';
709
+ if (v === 'mcp' || v === '2')
710
+ return 'mcp';
711
+ return undefined;
712
+ }
713
+ function parseClient(v) {
714
+ if (v === 'claude' || v === 'claude-code' || v === '1')
715
+ return 'claude';
716
+ if (v === 'cursor' || v === '2')
717
+ return 'cursor';
718
+ if (v === 'codex' || v === '3')
719
+ return 'codex';
720
+ if (v === 'other' || v === 'others' || v === '4')
721
+ return 'other';
722
+ return undefined;
723
+ }
724
+ export async function runInit() {
725
+ const cwd = process.cwd();
726
+ const argv = process.argv.slice(3); // tokens after `init`
727
+ let experience = parseExperience(readFlag(argv, '--experience'));
728
+ let client = parseClient(readFlag(argv, '--client'));
729
+ // Storage default is private (notebook gitignored); --commit-notebook opts in
730
+ // to committing .raw so the notebook can be shared with the team.
731
+ const commitNotebook = argv.includes('--commit-notebook');
732
+ out('');
733
+ out('coldstart init');
734
+ out(DIVIDER);
735
+ const rl = createInterface({ input: process.stdin, output: process.stderr });
736
+ const ask = async (q) => (await rl.question(q)).trim().toLowerCase();
737
+ if (!experience) {
738
+ out('');
739
+ out('How will the agent invoke coldstart?');
740
+ out('');
741
+ out(' 1 CLI (recommended) — runs `coldstart find` / `coldstart gs` (shell)');
742
+ out(' 2 MCP — calls the `find` / `gs` MCP tools (no shell)');
743
+ out('');
744
+ experience = parseExperience(await ask('Choose [1/2]: ')) ?? 'cli';
745
+ }
746
+ if (!client) {
747
+ out('');
748
+ out('Which client are you wiring coldstart into?');
749
+ out('');
750
+ out(' 1 Claude Code — rules import + find/gs hooks');
751
+ out(' 2 Cursor — rules + navigation/notebook hooks');
752
+ out(' 3 Codex — rules + navigation/notebook hooks');
753
+ out(' 4 Other — coldstart.md + wiring directions');
754
+ out('');
755
+ client = parseClient(await ask('Choose [1/2/3/4]: ')) ?? 'other';
756
+ }
757
+ rl.close();
758
+ out('');
759
+ switch (client) {
760
+ case 'claude':
761
+ setupClaude(cwd, experience);
762
+ break;
763
+ case 'cursor':
764
+ setupCursor(cwd, experience);
765
+ break;
766
+ case 'codex':
767
+ setupCodex(cwd, experience);
768
+ break;
769
+ default:
770
+ setupOther(cwd, experience);
771
+ }
772
+ // The notebook is always set up (files + git wiring). Claude and Codex hooks
773
+ // are wired by their client setup above; Cursor/other use the explicit CLI.
774
+ setupNotebook(cwd, commitNotebook);
775
+ // Warm the index now, while the user is here at setup, so the first lookup
776
+ // isn't a cold build. ensureKeeper spawns the background keeper (detached) and
777
+ // returns immediately; the keeper walks + indexes + watches from here on.
778
+ // Best-effort — a spawn failure just means the first query builds lazily.
779
+ out('');
780
+ out('Indexing this repo in the background — your first lookup will be instant.');
781
+ await ensureKeeper(cwd);
782
+ out('');
783
+ out(DIVIDER);
784
+ out('Done. Full docs: https://github.com/AkashGoenka/coldstart');
785
+ out('');
786
+ }
787
+ //# sourceMappingURL=init.js.map