@remnic/core 1.1.12 → 1.1.13

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 (1324) hide show
  1. package/dist/access-cli.d.ts +2 -1
  2. package/dist/access-cli.js +263 -82
  3. package/dist/access-cli.js.map +1 -1
  4. package/dist/access-http.d.ts +26 -60
  5. package/dist/access-http.js +43 -29
  6. package/dist/access-mcp.d.ts +24 -6
  7. package/dist/access-mcp.js +35 -28
  8. package/dist/access-schema.d.ts +9 -6
  9. package/dist/access-schema.js +7 -5
  10. package/dist/access-service-DcCDmNYC.d.ts +1542 -0
  11. package/dist/access-service.d.ts +25 -7
  12. package/dist/access-service.js +33 -26
  13. package/dist/active-memory-bridge.js +2 -2
  14. package/dist/active-recall.js +11 -3
  15. package/dist/active-recall.js.map +1 -1
  16. package/dist/adapters/claude-code.d.ts +24 -0
  17. package/dist/adapters/claude-code.js +9 -0
  18. package/dist/adapters/codex.d.ts +25 -0
  19. package/dist/adapters/codex.js +9 -0
  20. package/dist/adapters/hermes.d.ts +35 -0
  21. package/dist/adapters/hermes.js +9 -0
  22. package/dist/adapters/index.d.ts +6 -0
  23. package/dist/adapters/index.js +26 -0
  24. package/dist/adapters/registry.d.ts +20 -0
  25. package/dist/adapters/registry.js +13 -0
  26. package/dist/adapters/replit.d.ts +28 -0
  27. package/dist/adapters/replit.js +9 -0
  28. package/dist/adapters/types.d.ts +43 -0
  29. package/dist/adapters/types.js +8 -0
  30. package/dist/bootstrap.d.ts +20 -5
  31. package/dist/boxes.d.ts +7 -0
  32. package/dist/boxes.js +1 -1
  33. package/dist/briefing.d.ts +5 -3
  34. package/dist/briefing.js +9 -6
  35. package/dist/buffer-surprise-report.js +1 -1
  36. package/dist/buffer.d.ts +18 -4
  37. package/dist/buffer.js +1 -1
  38. package/dist/calibration.js +4 -4
  39. package/dist/capsule-cli.d.ts +4 -4
  40. package/dist/capsule-cli.js +1 -1
  41. package/dist/capsule-crypto-5CYAGVC5.js +18 -0
  42. package/dist/capsule-merge-4MGKE7C5.js +189 -0
  43. package/dist/causal-behavior.d.ts +8 -28
  44. package/dist/causal-behavior.js +6 -3
  45. package/dist/causal-behavior.js.map +1 -1
  46. package/dist/causal-chain.js +3 -2
  47. package/dist/causal-consolidation.d.ts +1 -1
  48. package/dist/causal-consolidation.js +24 -13
  49. package/dist/causal-consolidation.js.map +1 -1
  50. package/dist/causal-retrieval.js +3 -3
  51. package/dist/causal-trajectory.js +1 -1
  52. package/dist/chunk-25MQ7IHJ.js +427 -0
  53. package/dist/chunk-25MQ7IHJ.js.map +1 -0
  54. package/dist/chunk-2F2W355T.js +256 -0
  55. package/dist/chunk-2F2W355T.js.map +1 -0
  56. package/dist/chunk-2KI4QFHU.js +228 -0
  57. package/dist/chunk-2KI4QFHU.js.map +1 -0
  58. package/dist/chunk-2PRQG7PV.js +86 -0
  59. package/dist/chunk-2PRQG7PV.js.map +1 -0
  60. package/dist/chunk-2QR3XXIC.js +2272 -0
  61. package/dist/chunk-2QR3XXIC.js.map +1 -0
  62. package/dist/chunk-2WWLHTZY.js +121 -0
  63. package/dist/chunk-326G7DJK.js +2185 -0
  64. package/dist/chunk-326G7DJK.js.map +1 -0
  65. package/dist/chunk-34DQE4KF.js +174 -0
  66. package/dist/chunk-34DQE4KF.js.map +1 -0
  67. package/dist/chunk-3APJ5EVB.js +601 -0
  68. package/dist/chunk-3APJ5EVB.js.map +1 -0
  69. package/dist/chunk-3HPAPHUK.js +51 -0
  70. package/dist/chunk-3HPAPHUK.js.map +1 -0
  71. package/dist/chunk-3JXBXXM2.js +69 -0
  72. package/dist/chunk-3JXBXXM2.js.map +1 -0
  73. package/dist/chunk-3KW65B36.js +681 -0
  74. package/dist/chunk-3KW65B36.js.map +1 -0
  75. package/dist/chunk-3UXOZBHV.js +20 -0
  76. package/dist/chunk-3UXOZBHV.js.map +1 -0
  77. package/dist/chunk-3VAL7ZL2.js +266 -0
  78. package/dist/chunk-3VAL7ZL2.js.map +1 -0
  79. package/dist/chunk-3Y4P7RXM.js +31 -0
  80. package/dist/chunk-3Y4P7RXM.js.map +1 -0
  81. package/dist/chunk-47VWKCAF.js +273 -0
  82. package/dist/chunk-47VWKCAF.js.map +1 -0
  83. package/dist/chunk-4CRG46BG.js +271 -0
  84. package/dist/chunk-5375UYTQ.js +914 -0
  85. package/dist/chunk-5375UYTQ.js.map +1 -0
  86. package/dist/chunk-56K5QLHX.js +506 -0
  87. package/dist/chunk-56K5QLHX.js.map +1 -0
  88. package/dist/chunk-5RGLBDQF.js +596 -0
  89. package/dist/chunk-5RGLBDQF.js.map +1 -0
  90. package/dist/chunk-5UZXUTVO.js +9 -0
  91. package/dist/chunk-5UZXUTVO.js.map +1 -0
  92. package/dist/chunk-65PG43EQ.js +105 -0
  93. package/dist/chunk-65PG43EQ.js.map +1 -0
  94. package/dist/chunk-66DHUKLO.js +57 -0
  95. package/dist/chunk-66DHUKLO.js.map +1 -0
  96. package/dist/chunk-6FC5EGNV.js +46 -0
  97. package/dist/chunk-6FC5EGNV.js.map +1 -0
  98. package/dist/chunk-6H2TESSP.js +62 -0
  99. package/dist/chunk-6H2TESSP.js.map +1 -0
  100. package/dist/chunk-6LVVDPJ4.js +32 -0
  101. package/dist/chunk-6LVVDPJ4.js.map +1 -0
  102. package/dist/chunk-6RVI47ZR.js +159 -0
  103. package/dist/chunk-6RVI47ZR.js.map +1 -0
  104. package/dist/chunk-7AAT6G4Q.js +5117 -0
  105. package/dist/chunk-7AAT6G4Q.js.map +1 -0
  106. package/dist/chunk-7DTASS5T.js +29 -0
  107. package/dist/chunk-7DTASS5T.js.map +1 -0
  108. package/dist/chunk-7IASACLB.js +596 -0
  109. package/dist/chunk-7MNMYOFP.js +32 -0
  110. package/dist/chunk-7MNMYOFP.js.map +1 -0
  111. package/dist/chunk-7N4KAIGN.js +133 -0
  112. package/dist/chunk-7N4KAIGN.js.map +1 -0
  113. package/dist/chunk-7OZ53EXP.js +101 -0
  114. package/dist/chunk-7OZ53EXP.js.map +1 -0
  115. package/dist/chunk-7XYTQGCC.js +134 -0
  116. package/dist/chunk-7XYTQGCC.js.map +1 -0
  117. package/dist/chunk-A2XUIMJ3.js +341 -0
  118. package/dist/chunk-A2XUIMJ3.js.map +1 -0
  119. package/dist/chunk-AGZQD76C.js +201 -0
  120. package/dist/chunk-AGZQD76C.js.map +1 -0
  121. package/dist/chunk-APO3DCMU.js +361 -0
  122. package/dist/chunk-APO3DCMU.js.map +1 -0
  123. package/dist/chunk-BFBF3XEF.js +283 -0
  124. package/dist/chunk-BFBF3XEF.js.map +1 -0
  125. package/dist/chunk-BJ3KMYTB.js +1974 -0
  126. package/dist/chunk-BJ3KMYTB.js.map +1 -0
  127. package/dist/chunk-CHEL3SKB.js +6758 -0
  128. package/dist/chunk-CHEL3SKB.js.map +1 -0
  129. package/dist/chunk-CQZRLNMV.js +1491 -0
  130. package/dist/chunk-CQZRLNMV.js.map +1 -0
  131. package/dist/chunk-D46YSIYX.js +892 -0
  132. package/dist/chunk-D46YSIYX.js.map +1 -0
  133. package/dist/chunk-DINWEURR.js +648 -0
  134. package/dist/chunk-DINWEURR.js.map +1 -0
  135. package/dist/chunk-DK5LDEQM.js +530 -0
  136. package/dist/chunk-DK5LDEQM.js.map +1 -0
  137. package/dist/chunk-DOM4GKSW.js +34 -0
  138. package/dist/chunk-DOM4GKSW.js.map +1 -0
  139. package/dist/chunk-EDTHC6UD.js +1075 -0
  140. package/dist/chunk-EFJ3MQ4V.js +721 -0
  141. package/dist/chunk-EHRTFRWW.js +89 -0
  142. package/dist/chunk-EHRTFRWW.js.map +1 -0
  143. package/dist/chunk-FAJ7FZYM.js +11 -0
  144. package/dist/chunk-FAJ7FZYM.js.map +1 -0
  145. package/dist/chunk-FBYESMQ2.js +570 -0
  146. package/dist/chunk-FDU6HUUL.js +147 -0
  147. package/dist/chunk-FF4KLI5W.js +99 -0
  148. package/dist/chunk-FF4KLI5W.js.map +1 -0
  149. package/dist/chunk-FIT6DMX6.js +310 -0
  150. package/dist/chunk-FIT6DMX6.js.map +1 -0
  151. package/dist/chunk-FJ43PRLT.js +272 -0
  152. package/dist/chunk-FJ43PRLT.js.map +1 -0
  153. package/dist/chunk-FKFMOY3N.js +32 -0
  154. package/dist/chunk-FKFMOY3N.js.map +1 -0
  155. package/dist/chunk-FLTNHQK6.js +262 -0
  156. package/dist/chunk-FLTNHQK6.js.map +1 -0
  157. package/dist/chunk-GA454ALV.js +12436 -0
  158. package/dist/chunk-GA454ALV.js.map +1 -0
  159. package/dist/chunk-GGKRUQOO.js +228 -0
  160. package/dist/chunk-GIF42EW3.js +63 -0
  161. package/dist/chunk-GIF42EW3.js.map +1 -0
  162. package/dist/chunk-GL6I6MEQ.js +647 -0
  163. package/dist/chunk-H3ME6L6D.js +709 -0
  164. package/dist/chunk-H3ME6L6D.js.map +1 -0
  165. package/dist/chunk-HHLLAQGZ.js +1 -0
  166. package/dist/chunk-HXXBL2KD.js +2040 -0
  167. package/dist/chunk-I5V2VDIW.js +219 -0
  168. package/dist/chunk-I5V2VDIW.js.map +1 -0
  169. package/dist/chunk-I6K5FBRQ.js +35 -0
  170. package/dist/chunk-I6K5FBRQ.js.map +1 -0
  171. package/dist/chunk-ICRIXAP2.js +121 -0
  172. package/dist/chunk-ICRIXAP2.js.map +1 -0
  173. package/dist/chunk-J4EB7DNW.js +11 -0
  174. package/dist/chunk-J4EB7DNW.js.map +1 -0
  175. package/dist/chunk-JLFA7DQG.js +62 -0
  176. package/dist/chunk-JLFA7DQG.js.map +1 -0
  177. package/dist/chunk-KJTKLXTH.js +9 -0
  178. package/dist/chunk-KJTKLXTH.js.map +1 -0
  179. package/dist/chunk-KLAO5DGL.js +917 -0
  180. package/dist/chunk-KLAO5DGL.js.map +1 -0
  181. package/dist/chunk-KNKUID7G.js +183 -0
  182. package/dist/chunk-KOSORCJG.js +624 -0
  183. package/dist/chunk-KOSORCJG.js.map +1 -0
  184. package/dist/chunk-KUJVMMZQ.js +1262 -0
  185. package/dist/chunk-KUJVMMZQ.js.map +1 -0
  186. package/dist/chunk-LCR46JY5.js +123 -0
  187. package/dist/chunk-LCR46JY5.js.map +1 -0
  188. package/dist/chunk-LLQ2LLWF.js +148 -0
  189. package/dist/chunk-LLQ2LLWF.js.map +1 -0
  190. package/dist/chunk-LPMVBPA3.js +236 -0
  191. package/dist/chunk-LT3NLYSI.js +50 -0
  192. package/dist/chunk-LT3NLYSI.js.map +1 -0
  193. package/dist/chunk-LUDTDZLK.js +287 -0
  194. package/dist/chunk-LUDTDZLK.js.map +1 -0
  195. package/dist/chunk-M23FSH32.js +3963 -0
  196. package/dist/chunk-M23FSH32.js.map +1 -0
  197. package/dist/chunk-MC26UJIM.js +118 -0
  198. package/dist/chunk-ME6ESPZU.js +119 -0
  199. package/dist/chunk-ME6ESPZU.js.map +1 -0
  200. package/dist/chunk-MGKYQQYF.js +272 -0
  201. package/dist/chunk-MJFNCJXV.js +66 -0
  202. package/dist/chunk-MJFNCJXV.js.map +1 -0
  203. package/dist/chunk-MSWG7JI6.js +237 -0
  204. package/dist/chunk-MSWG7JI6.js.map +1 -0
  205. package/dist/chunk-MT25YHYH.js +141 -0
  206. package/dist/chunk-MT25YHYH.js.map +1 -0
  207. package/dist/chunk-MT4HVDUZ.js +53 -0
  208. package/dist/chunk-MY6TPVXW.js +219 -0
  209. package/dist/chunk-N2D6GXBM.js +267 -0
  210. package/dist/chunk-N2D6GXBM.js.map +1 -0
  211. package/dist/chunk-NJ3MJQZX.js +46 -0
  212. package/dist/chunk-NJ3MJQZX.js.map +1 -0
  213. package/dist/chunk-NMZY542O.js +335 -0
  214. package/dist/chunk-NMZY542O.js.map +1 -0
  215. package/dist/chunk-NNVTUXEB.js +23 -0
  216. package/dist/chunk-NZL6GGQE.js +375 -0
  217. package/dist/chunk-NZL6GGQE.js.map +1 -0
  218. package/dist/chunk-P4NEIHUT.js +108 -0
  219. package/dist/chunk-P7FMDTKL.js +103 -0
  220. package/dist/chunk-P7FMDTKL.js.map +1 -0
  221. package/dist/chunk-PHK3HARR.js +32 -0
  222. package/dist/chunk-PHK3HARR.js.map +1 -0
  223. package/dist/chunk-PIRJPV5T.js +98 -0
  224. package/dist/chunk-PIRJPV5T.js.map +1 -0
  225. package/dist/chunk-PK7H5L6Y.js +159 -0
  226. package/dist/chunk-PK7H5L6Y.js.map +1 -0
  227. package/dist/chunk-PR5FBTFU.js +233 -0
  228. package/dist/chunk-PR5FBTFU.js.map +1 -0
  229. package/dist/chunk-PU63GXWS.js +174 -0
  230. package/dist/chunk-PU63GXWS.js.map +1 -0
  231. package/dist/chunk-PZIAX57I.js +124 -0
  232. package/dist/chunk-PZIAX57I.js.map +1 -0
  233. package/dist/chunk-Q7P4WJDP.js +26 -0
  234. package/dist/chunk-Q7P4WJDP.js.map +1 -0
  235. package/dist/chunk-QQUAB63I.js +63 -0
  236. package/dist/chunk-QQUAB63I.js.map +1 -0
  237. package/dist/chunk-QRNI5JBH.js +18 -0
  238. package/dist/chunk-RHY3HH7P.js +601 -0
  239. package/dist/chunk-RHY3HH7P.js.map +1 -0
  240. package/dist/chunk-RRF5UOBJ.js +91 -0
  241. package/dist/chunk-RXDLTSWT.js +124 -0
  242. package/dist/chunk-RXDLTSWT.js.map +1 -0
  243. package/dist/chunk-RYED3SPJ.js +42 -0
  244. package/dist/chunk-RYED3SPJ.js.map +1 -0
  245. package/dist/chunk-S7KDBTWT.js +106 -0
  246. package/dist/chunk-S7KDBTWT.js.map +1 -0
  247. package/dist/chunk-SEDEKFYQ.js +1 -0
  248. package/dist/chunk-TECVW3JP.js +36 -0
  249. package/dist/chunk-TECVW3JP.js.map +1 -0
  250. package/dist/chunk-TFO23QT4.js +88 -0
  251. package/dist/chunk-TFO23QT4.js.map +1 -0
  252. package/dist/chunk-TK4UEOSK.js +76 -0
  253. package/dist/chunk-TK4UEOSK.js.map +1 -0
  254. package/dist/chunk-TKWGAOLV.js +122 -0
  255. package/dist/chunk-TKWGAOLV.js.map +1 -0
  256. package/dist/chunk-TMM4S4IJ.js +597 -0
  257. package/dist/chunk-TMM4S4IJ.js.map +1 -0
  258. package/dist/chunk-TMQLARTH.js +188 -0
  259. package/dist/chunk-TMQLARTH.js.map +1 -0
  260. package/dist/chunk-TPDBFYEG.js +130 -0
  261. package/dist/chunk-TPDBFYEG.js.map +1 -0
  262. package/dist/chunk-TPMQ3G6Z.js +145 -0
  263. package/dist/chunk-TPMQ3G6Z.js.map +1 -0
  264. package/dist/chunk-TZOLIGIG.js +61 -0
  265. package/dist/chunk-TZOLIGIG.js.map +1 -0
  266. package/dist/chunk-U3PN77QT.js +113 -0
  267. package/dist/chunk-U3WSW6PZ.js +277 -0
  268. package/dist/chunk-U4SCL7B7.js +640 -0
  269. package/dist/chunk-U4SCL7B7.js.map +1 -0
  270. package/dist/chunk-UWK5OXUJ.js +156 -0
  271. package/dist/chunk-UWK5OXUJ.js.map +1 -0
  272. package/dist/chunk-UWVJF25J.js +74 -0
  273. package/dist/chunk-UXHQAFNA.js +1317 -0
  274. package/dist/chunk-UXHQAFNA.js.map +1 -0
  275. package/dist/chunk-V5OCT34X.js +1 -0
  276. package/dist/chunk-VLXA6PI2.js +304 -0
  277. package/dist/chunk-VLXA6PI2.js.map +1 -0
  278. package/dist/chunk-VNO6ZJ35.js +500 -0
  279. package/dist/chunk-VNO6ZJ35.js.map +1 -0
  280. package/dist/chunk-VW676BEI.js +827 -0
  281. package/dist/chunk-VW676BEI.js.map +1 -0
  282. package/dist/chunk-W3LR522O.js +2296 -0
  283. package/dist/chunk-W4L6CZKA.js +96 -0
  284. package/dist/chunk-W4L6CZKA.js.map +1 -0
  285. package/dist/chunk-W4RVMTHR.js +372 -0
  286. package/dist/chunk-W4RVMTHR.js.map +1 -0
  287. package/dist/chunk-WEHSQBFR.js +188 -0
  288. package/dist/chunk-WEHSQBFR.js.map +1 -0
  289. package/dist/chunk-WELDCG6C.js +380 -0
  290. package/dist/chunk-WELDCG6C.js.map +1 -0
  291. package/dist/chunk-WZYKANL3.js +2800 -0
  292. package/dist/chunk-WZYKANL3.js.map +1 -0
  293. package/dist/chunk-XIG5PDM7.js +48 -0
  294. package/dist/chunk-XJNBEDFE.js +193 -0
  295. package/dist/chunk-XJNBEDFE.js.map +1 -0
  296. package/dist/chunk-XVVIG67A.js +291 -0
  297. package/dist/chunk-XVVIG67A.js.map +1 -0
  298. package/dist/chunk-XVZ7B3HG.js +135 -0
  299. package/dist/chunk-YBPYIAA5.js +73 -0
  300. package/dist/chunk-YBPYIAA5.js.map +1 -0
  301. package/dist/chunk-Z734BLO3.js +21 -0
  302. package/dist/chunk-Z734BLO3.js.map +1 -0
  303. package/dist/chunk-ZKSK55RC.js +269 -0
  304. package/dist/chunk-ZKSK55RC.js.map +1 -0
  305. package/dist/chunk-ZTFCYYEZ.js +69 -0
  306. package/dist/chunk-ZTFCYYEZ.js.map +1 -0
  307. package/dist/chunk-ZY2MNJR6.js +329 -0
  308. package/dist/chunk-ZY2MNJR6.js.map +1 -0
  309. package/dist/cli-D3VpkVwB.d.ts +1136 -0
  310. package/dist/cli.d.ts +39 -10
  311. package/dist/cli.js +108 -49
  312. package/dist/commitment-ledger.js +1 -1
  313. package/dist/compat/checks.d.ts +5 -0
  314. package/dist/compat/checks.js +11 -0
  315. package/dist/compat/checks.js.map +1 -0
  316. package/dist/compat/types.d.ts +30 -0
  317. package/dist/compat/types.js +1 -0
  318. package/dist/compat/types.js.map +1 -0
  319. package/dist/compounding/engine.d.ts +221 -0
  320. package/dist/compounding/engine.js +32 -0
  321. package/dist/compounding/engine.js.map +1 -0
  322. package/dist/compounding/preference-consolidator.d.ts +92 -0
  323. package/dist/compounding/preference-consolidator.js +553 -0
  324. package/dist/compounding/preference-consolidator.js.map +1 -0
  325. package/dist/config.d.ts +4 -2
  326. package/dist/config.js +9 -4
  327. package/dist/conflict-policy-DyJ2wd-h.d.ts +4 -0
  328. package/dist/connectors/codex-materialize-runner.d.ts +64 -0
  329. package/dist/connectors/codex-materialize-runner.js +33 -0
  330. package/dist/connectors/codex-materialize-runner.js.map +1 -0
  331. package/dist/connectors/codex-materialize.d.ts +195 -0
  332. package/dist/connectors/codex-materialize.js +38 -0
  333. package/dist/connectors/codex-materialize.js.map +1 -0
  334. package/dist/connectors/index.d.ts +444 -0
  335. package/dist/connectors/index.js +115 -0
  336. package/dist/connectors/index.js.map +1 -0
  337. package/dist/connectors-cli-CwbyjGR7.d.ts +257 -0
  338. package/dist/connectors-cli.d.ts +1 -1
  339. package/dist/consolidation-provenance-check.d.ts +3 -1
  340. package/dist/consolidation-undo.d.ts +3 -1
  341. package/dist/contradiction/index.d.ts +258 -0
  342. package/dist/contradiction/index.js +43 -0
  343. package/dist/contradiction/index.js.map +1 -0
  344. package/dist/contradiction-review-ATP4S6IC.js +30 -0
  345. package/dist/contradiction-review-ATP4S6IC.js.map +1 -0
  346. package/dist/contradiction-scan-5A4IDZV5.js +13 -0
  347. package/dist/contradiction-scan-5A4IDZV5.js.map +1 -0
  348. package/dist/conversation-index/backend.d.ts +97 -0
  349. package/dist/conversation-index/backend.js +13 -0
  350. package/dist/conversation-index/backend.js.map +1 -0
  351. package/dist/conversation-index/chunker.d.ts +16 -0
  352. package/dist/conversation-index/chunker.js +8 -0
  353. package/dist/conversation-index/chunker.js.map +1 -0
  354. package/dist/conversation-index/cleanup.d.ts +11 -0
  355. package/dist/conversation-index/cleanup.js +9 -0
  356. package/dist/conversation-index/cleanup.js.map +1 -0
  357. package/dist/conversation-index/faiss-adapter.d.ts +6 -0
  358. package/dist/conversation-index/faiss-adapter.js +16 -0
  359. package/dist/conversation-index/faiss-adapter.js.map +1 -0
  360. package/dist/conversation-index/indexer.d.ts +23 -0
  361. package/dist/conversation-index/indexer.js +15 -0
  362. package/dist/conversation-index/indexer.js.map +1 -0
  363. package/dist/conversation-index/search.d.ts +6 -0
  364. package/dist/conversation-index/search.js +11 -0
  365. package/dist/conversation-index/search.js.map +1 -0
  366. package/dist/embedding-fallback.js +2 -2
  367. package/dist/enrichment/index.d.ts +163 -0
  368. package/dist/enrichment/index.js +18 -0
  369. package/dist/enrichment/index.js.map +1 -0
  370. package/dist/entity-retrieval.d.ts +4 -2
  371. package/dist/entity-retrieval.js +8 -5
  372. package/dist/evals.js +1 -1
  373. package/dist/explicit-capture.d.ts +20 -5
  374. package/dist/explicit-capture.js +2 -2
  375. package/dist/extraction-judge-training.js +1 -1
  376. package/dist/extraction.js +8 -8
  377. package/dist/faiss-adapter-CzPghc4C.d.ts +70 -0
  378. package/dist/fallback-llm.d.ts +2 -0
  379. package/dist/fallback-llm.js +4 -4
  380. package/dist/graph-edge-decay-5DI5GUNL.js +207 -0
  381. package/dist/index.d.ts +66 -711
  382. package/dist/index.js +556 -2680
  383. package/dist/index.js.map +1 -1
  384. package/dist/lcm/archive.d.ts +89 -0
  385. package/dist/lcm/archive.js +12 -0
  386. package/dist/lcm/archive.js.map +1 -0
  387. package/dist/lcm/dag.d.ts +48 -0
  388. package/dist/lcm/dag.js +8 -0
  389. package/dist/lcm/dag.js.map +1 -0
  390. package/dist/lcm/engine.d.ts +116 -0
  391. package/dist/lcm/engine.js +20 -0
  392. package/dist/lcm/engine.js.map +1 -0
  393. package/dist/lcm/index.d.ts +12 -0
  394. package/dist/lcm/index.js +44 -0
  395. package/dist/lcm/index.js.map +1 -0
  396. package/dist/lcm/queue.d.ts +62 -0
  397. package/dist/lcm/queue.js +8 -0
  398. package/dist/lcm/queue.js.map +1 -0
  399. package/dist/lcm/recall.d.ts +20 -0
  400. package/dist/lcm/recall.js +8 -0
  401. package/dist/lcm/recall.js.map +1 -0
  402. package/dist/lcm/schema.d.ts +16 -0
  403. package/dist/lcm/schema.js +14 -0
  404. package/dist/lcm/schema.js.map +1 -0
  405. package/dist/lcm/summarizer.d.ts +38 -0
  406. package/dist/lcm/summarizer.js +12 -0
  407. package/dist/lcm/summarizer.js.map +1 -0
  408. package/dist/lcm/tools.d.ts +29 -0
  409. package/dist/lcm/tools.js +8 -0
  410. package/dist/lcm/tools.js.map +1 -0
  411. package/dist/live-connectors-runner.js +5 -5
  412. package/dist/local-llm.js +3 -3
  413. package/dist/maintenance/archive-observations.d.ts +18 -0
  414. package/dist/maintenance/archive-observations.js +8 -0
  415. package/dist/maintenance/archive-observations.js.map +1 -0
  416. package/dist/maintenance/backup-stamp.d.ts +3 -0
  417. package/dist/maintenance/backup-stamp.js +8 -0
  418. package/dist/maintenance/backup-stamp.js.map +1 -0
  419. package/dist/maintenance/memory-governance-cron.d.ts +85 -0
  420. package/dist/maintenance/memory-governance-cron.js +22 -0
  421. package/dist/maintenance/memory-governance-cron.js.map +1 -0
  422. package/dist/maintenance/memory-governance.d.ts +137 -0
  423. package/dist/maintenance/memory-governance.js +40 -0
  424. package/dist/maintenance/memory-governance.js.map +1 -0
  425. package/dist/maintenance/migrate-observations.d.ts +18 -0
  426. package/dist/maintenance/migrate-observations.js +9 -0
  427. package/dist/maintenance/migrate-observations.js.map +1 -0
  428. package/dist/maintenance/observation-ledger-utils.d.ts +10 -0
  429. package/dist/maintenance/observation-ledger-utils.js +10 -0
  430. package/dist/maintenance/observation-ledger-utils.js.map +1 -0
  431. package/dist/maintenance/rebuild-memory-lifecycle-ledger.d.ts +15 -0
  432. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +28 -0
  433. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js.map +1 -0
  434. package/dist/maintenance/rebuild-memory-projection.d.ts +77 -0
  435. package/dist/maintenance/rebuild-memory-projection.js +35 -0
  436. package/dist/maintenance/rebuild-memory-projection.js.map +1 -0
  437. package/dist/maintenance/rebuild-observations.d.ts +17 -0
  438. package/dist/maintenance/rebuild-observations.js +9 -0
  439. package/dist/maintenance/rebuild-observations.js.map +1 -0
  440. package/dist/mcp-memory-inspector-app.d.ts +24 -6
  441. package/dist/memory-projection-store.d.ts +108 -3
  442. package/dist/memory-projection-store.js +2 -1
  443. package/dist/memory-worth-outcomes.d.ts +4 -2
  444. package/dist/migrate/from-engram.d.ts +24 -0
  445. package/dist/migrate/from-engram.js +12 -0
  446. package/dist/migrate/from-engram.js.map +1 -0
  447. package/dist/namespaces/migrate.d.ts +50 -0
  448. package/dist/namespaces/migrate.js +50 -0
  449. package/dist/namespaces/migrate.js.map +1 -0
  450. package/dist/namespaces/principal.d.ts +17 -0
  451. package/dist/namespaces/principal.js +16 -0
  452. package/dist/namespaces/principal.js.map +1 -0
  453. package/dist/namespaces/search.d.ts +46 -0
  454. package/dist/namespaces/search.js +28 -0
  455. package/dist/namespaces/search.js.map +1 -0
  456. package/dist/namespaces/storage.d.ts +32 -0
  457. package/dist/namespaces/storage.js +28 -0
  458. package/dist/namespaces/storage.js.map +1 -0
  459. package/dist/network/tailscale.d.ts +41 -0
  460. package/dist/network/tailscale.js +9 -0
  461. package/dist/network/tailscale.js.map +1 -0
  462. package/dist/network/webdav.d.ts +39 -0
  463. package/dist/network/webdav.js +10 -0
  464. package/dist/network/webdav.js.map +1 -0
  465. package/dist/objective-state-writers.js +2 -2
  466. package/dist/operator-toolkit.d.ts +4 -2
  467. package/dist/operator-toolkit.js +32 -14
  468. package/dist/opik-exporter.js +2 -2
  469. package/dist/opik-exporter.js.map +1 -1
  470. package/dist/orchestrator-DuWl9Hwx.d.ts +1244 -0
  471. package/dist/orchestrator.d.ts +22 -7
  472. package/dist/orchestrator.js +79 -44
  473. package/dist/path-MR5JPYOP.js +9 -0
  474. package/dist/path-MR5JPYOP.js.map +1 -0
  475. package/dist/qmd-recall-cache.d.ts +1 -1
  476. package/dist/qmd.d.ts +102 -3
  477. package/dist/qmd.js +23 -5
  478. package/dist/recall-explain-renderer.js +3 -3
  479. package/dist/recall-xray-cli.js +4 -4
  480. package/dist/recall-xray-renderer.js +3 -3
  481. package/dist/recall-xray.js +2 -2
  482. package/dist/replay/normalizers/chatgpt.d.ts +6 -0
  483. package/dist/replay/normalizers/chatgpt.js +11 -0
  484. package/dist/replay/normalizers/chatgpt.js.map +1 -0
  485. package/dist/replay/normalizers/claude.d.ts +6 -0
  486. package/dist/replay/normalizers/claude.js +11 -0
  487. package/dist/replay/normalizers/claude.js.map +1 -0
  488. package/dist/replay/normalizers/openclaw.d.ts +6 -0
  489. package/dist/replay/normalizers/openclaw.js +11 -0
  490. package/dist/replay/normalizers/openclaw.js.map +1 -0
  491. package/dist/replay/normalizers/shared.d.ts +16 -0
  492. package/dist/replay/normalizers/shared.js +14 -0
  493. package/dist/replay/normalizers/shared.js.map +1 -0
  494. package/dist/replay/runner.d.ts +35 -0
  495. package/dist/replay/runner.js +16 -0
  496. package/dist/replay/runner.js.map +1 -0
  497. package/dist/replay/types.d.ts +57 -0
  498. package/dist/replay/types.js +19 -0
  499. package/dist/replay/types.js.map +1 -0
  500. package/dist/resolution-B7FNQSSP.js +12 -0
  501. package/dist/resolution-B7FNQSSP.js.map +1 -0
  502. package/dist/resolve-provider-secret.js +2 -2
  503. package/dist/resume-bundles.js +8 -6
  504. package/dist/retrieval-agents.d.ts +1 -1
  505. package/dist/routing/engine.d.ts +35 -0
  506. package/dist/routing/engine.js +16 -0
  507. package/dist/routing/engine.js.map +1 -0
  508. package/dist/routing/store.d.ts +27 -0
  509. package/dist/routing/store.js +10 -0
  510. package/dist/routing/store.js.map +1 -0
  511. package/dist/runtime/better-sqlite.d.ts +8 -0
  512. package/dist/runtime/better-sqlite.js +10 -0
  513. package/dist/runtime/better-sqlite.js.map +1 -0
  514. package/dist/runtime/child-process.d.ts +32 -0
  515. package/dist/runtime/child-process.js +10 -0
  516. package/dist/runtime/child-process.js.map +1 -0
  517. package/dist/runtime/env.d.ts +5 -0
  518. package/dist/runtime/env.js +12 -0
  519. package/dist/runtime/env.js.map +1 -0
  520. package/dist/schemas.d.ts +22 -22
  521. package/dist/sdk-compat.js +1 -1
  522. package/dist/search/document-scanner.d.ts +22 -0
  523. package/dist/search/document-scanner.js +8 -0
  524. package/dist/search/document-scanner.js.map +1 -0
  525. package/dist/search/embed-helper.d.ts +35 -0
  526. package/dist/search/embed-helper.js +9 -0
  527. package/dist/search/embed-helper.js.map +1 -0
  528. package/dist/search/factory.d.ts +32 -0
  529. package/dist/search/factory.js +29 -0
  530. package/dist/search/factory.js.map +1 -0
  531. package/dist/search/index.d.ts +15 -0
  532. package/dist/search/index.js +50 -0
  533. package/dist/search/index.js.map +1 -0
  534. package/dist/search/lancedb-backend.d.ts +51 -0
  535. package/dist/search/lancedb-backend.js +10 -0
  536. package/dist/search/lancedb-backend.js.map +1 -0
  537. package/dist/search/meilisearch-backend.d.ts +48 -0
  538. package/dist/search/meilisearch-backend.js +10 -0
  539. package/dist/search/meilisearch-backend.js.map +1 -0
  540. package/dist/search/noop-backend.d.ts +26 -0
  541. package/dist/search/noop-backend.js +8 -0
  542. package/dist/search/noop-backend.js.map +1 -0
  543. package/dist/search/orama-backend.d.ts +53 -0
  544. package/dist/search/orama-backend.js +10 -0
  545. package/dist/search/orama-backend.js.map +1 -0
  546. package/dist/search/port.d.ts +61 -0
  547. package/dist/search/port.js +1 -0
  548. package/dist/search/port.js.map +1 -0
  549. package/dist/search/remote-backend.d.ts +39 -0
  550. package/dist/search/remote-backend.js +9 -0
  551. package/dist/search/remote-backend.js.map +1 -0
  552. package/dist/secure-store/index.d.ts +890 -0
  553. package/dist/secure-store/index.js +156 -0
  554. package/dist/secure-store/index.js.map +1 -0
  555. package/dist/semantic-VwGI14Ok.d.ts +69 -0
  556. package/dist/semantic-consolidation-4HkHWgeI.d.ts +180 -0
  557. package/dist/semantic-consolidation.d.ts +2 -2
  558. package/dist/semantic-consolidation.js +13 -6
  559. package/dist/semantic-rule-promotion.js +8 -5
  560. package/dist/semantic-rule-verifier.js +8 -5
  561. package/dist/shared-context/manager.d.ts +131 -0
  562. package/dist/shared-context/manager.js +15 -0
  563. package/dist/shared-context/manager.js.map +1 -0
  564. package/dist/skills-registry.js +13 -1
  565. package/dist/skills-registry.js.map +1 -1
  566. package/dist/state-store-VZU2IA53.js +16 -0
  567. package/dist/state-store-VZU2IA53.js.map +1 -0
  568. package/dist/storage-paths.d.ts +9 -0
  569. package/dist/storage-paths.js +20 -0
  570. package/dist/storage-paths.js.map +1 -0
  571. package/dist/storage.d.ts +3 -1
  572. package/dist/storage.js +7 -4
  573. package/dist/summarizer.d.ts +5 -0
  574. package/dist/summarizer.js +9 -8
  575. package/dist/summary-snapshot.js +2 -1
  576. package/dist/surfaces/dreams.d.ts +16 -0
  577. package/dist/surfaces/dreams.js +282 -0
  578. package/dist/surfaces/dreams.js.map +1 -0
  579. package/dist/surfaces/heartbeat.d.ts +17 -0
  580. package/dist/surfaces/heartbeat.js +265 -0
  581. package/dist/surfaces/heartbeat.js.map +1 -0
  582. package/dist/temporal-supersession.d.ts +3 -1
  583. package/dist/threading.d.ts +5 -0
  584. package/dist/threading.js +2 -1
  585. package/dist/tier-migration.d.ts +4 -2
  586. package/dist/tokens.js +2 -2
  587. package/dist/transcript.d.ts +15 -1
  588. package/dist/transcript.js +2 -1
  589. package/dist/transfer/autodetect.d.ts +4 -0
  590. package/dist/transfer/autodetect.js +15 -0
  591. package/dist/transfer/autodetect.js.map +1 -0
  592. package/dist/transfer/backup.d.ts +21 -0
  593. package/dist/transfer/backup.js +17 -0
  594. package/dist/transfer/backup.js.map +1 -0
  595. package/dist/transfer/capsule-export.d.ts +113 -0
  596. package/dist/transfer/capsule-export.js +19 -0
  597. package/dist/transfer/capsule-export.js.map +1 -0
  598. package/dist/transfer/capsule-import.d.ts +124 -0
  599. package/dist/transfer/capsule-import.js +16 -0
  600. package/dist/transfer/capsule-import.js.map +1 -0
  601. package/dist/transfer/constants.d.ts +13 -0
  602. package/dist/transfer/constants.js +12 -0
  603. package/dist/transfer/constants.js.map +1 -0
  604. package/dist/transfer/export-json.d.ts +11 -0
  605. package/dist/transfer/export-json.js +11 -0
  606. package/dist/transfer/export-json.js.map +1 -0
  607. package/dist/transfer/export-md.d.ts +10 -0
  608. package/dist/transfer/export-md.js +13 -0
  609. package/dist/transfer/export-md.js.map +1 -0
  610. package/dist/transfer/export-sqlite.d.ts +9 -0
  611. package/dist/transfer/export-sqlite.js +12 -0
  612. package/dist/transfer/export-sqlite.js.map +1 -0
  613. package/dist/transfer/fs-utils.d.ts +61 -0
  614. package/dist/transfer/fs-utils.js +40 -0
  615. package/dist/transfer/fs-utils.js.map +1 -0
  616. package/dist/transfer/import-json.d.ts +16 -0
  617. package/dist/transfer/import-json.js +13 -0
  618. package/dist/transfer/import-json.js.map +1 -0
  619. package/dist/transfer/import-md.d.ts +14 -0
  620. package/dist/transfer/import-md.js +11 -0
  621. package/dist/transfer/import-md.js.map +1 -0
  622. package/dist/transfer/import-sqlite.d.ts +14 -0
  623. package/dist/transfer/import-sqlite.js +12 -0
  624. package/dist/transfer/import-sqlite.js.map +1 -0
  625. package/dist/transfer/sqlite-schema.d.ts +4 -0
  626. package/dist/transfer/sqlite-schema.js +10 -0
  627. package/dist/transfer/sqlite-schema.js.map +1 -0
  628. package/dist/transfer/types.d.ts +916 -0
  629. package/dist/transfer/types.js +30 -0
  630. package/dist/transfer/types.js.map +1 -0
  631. package/dist/types.d.ts +28 -1
  632. package/dist/types.js +1 -1
  633. package/dist/verified-recall.js +9 -6
  634. package/dist/work/board.d.ts +43 -0
  635. package/dist/work/board.js +14 -0
  636. package/dist/work/board.js.map +1 -0
  637. package/dist/work/boundary.d.ts +8 -0
  638. package/dist/work/boundary.js +14 -0
  639. package/dist/work/boundary.js.map +1 -0
  640. package/dist/work/storage.d.ts +39 -0
  641. package/dist/work/storage.js +11 -0
  642. package/dist/work/storage.js.map +1 -0
  643. package/dist/work/types.d.ts +75 -0
  644. package/dist/work/types.js +1 -0
  645. package/dist/work/types.js.map +1 -0
  646. package/package.json +2767 -6
  647. package/scripts/faiss_index.py +816 -0
  648. package/scripts/faiss_requirements.txt +3 -0
  649. package/skills/remnic-entities/SKILL.md +51 -0
  650. package/skills/remnic-memory-workflow/SKILL.md +61 -0
  651. package/skills/remnic-recall/SKILL.md +51 -0
  652. package/skills/remnic-remember/SKILL.md +56 -0
  653. package/skills/remnic-search/SKILL.md +51 -0
  654. package/skills/remnic-status/SKILL.md +51 -0
  655. package/src/abort-error.test.ts +49 -0
  656. package/src/abort-error.ts +46 -0
  657. package/src/abstraction-nodes.ts +162 -0
  658. package/src/access-audit.test.ts +178 -0
  659. package/src/access-audit.ts +125 -0
  660. package/src/access-cli.test.ts +439 -0
  661. package/src/access-cli.ts +438 -0
  662. package/src/access-http.test.ts +225 -0
  663. package/src/access-http.ts +1899 -0
  664. package/src/access-idempotency.ts +232 -0
  665. package/src/access-mcp.test.ts +568 -0
  666. package/src/access-mcp.ts +3056 -0
  667. package/src/access-schema-pi.test.ts +60 -0
  668. package/src/access-schema.ts +522 -0
  669. package/src/access-service-namespace.test.ts +123 -0
  670. package/src/access-service.ts +5629 -0
  671. package/src/action-confidence.test.ts +206 -0
  672. package/src/action-confidence.ts +466 -0
  673. package/src/active-memory-bridge.test.ts +285 -0
  674. package/src/active-memory-bridge.ts +217 -0
  675. package/src/active-recall.test.ts +484 -0
  676. package/src/active-recall.ts +459 -0
  677. package/src/adapters/claude-code.ts +56 -0
  678. package/src/adapters/codex.ts +57 -0
  679. package/src/adapters/hermes.ts +64 -0
  680. package/src/adapters/index.ts +6 -0
  681. package/src/adapters/registry.ts +41 -0
  682. package/src/adapters/replit.ts +55 -0
  683. package/src/adapters/types.ts +51 -0
  684. package/src/behavior-learner.ts +144 -0
  685. package/src/behavior-signals.ts +73 -0
  686. package/src/binary-lifecycle/backend.ts +117 -0
  687. package/src/binary-lifecycle/index.ts +35 -0
  688. package/src/binary-lifecycle/manifest.ts +79 -0
  689. package/src/binary-lifecycle/pipeline.ts +352 -0
  690. package/src/binary-lifecycle/scanner.ts +89 -0
  691. package/src/binary-lifecycle/types.ts +89 -0
  692. package/src/bootstrap.ts +178 -0
  693. package/src/boxes.ts +521 -0
  694. package/src/briefing.test.ts +1535 -0
  695. package/src/briefing.ts +1382 -0
  696. package/src/buffer-session.test.ts +443 -0
  697. package/src/buffer-surprise-report.ts +176 -0
  698. package/src/buffer-surprise-telemetry.test.ts +606 -0
  699. package/src/buffer-surprise-trigger.test.ts +766 -0
  700. package/src/buffer-surprise.test.ts +339 -0
  701. package/src/buffer-surprise.ts +203 -0
  702. package/src/buffer.ts +900 -0
  703. package/src/bulk-import/cli-command.test.ts +204 -0
  704. package/src/bulk-import/index.ts +34 -0
  705. package/src/bulk-import/pipeline.test.ts +445 -0
  706. package/src/bulk-import/pipeline.ts +178 -0
  707. package/src/bulk-import/registry.test.ts +151 -0
  708. package/src/bulk-import/registry.ts +72 -0
  709. package/src/bulk-import/types.test.ts +272 -0
  710. package/src/bulk-import/types.ts +145 -0
  711. package/src/calibration.ts +394 -0
  712. package/src/capsule-cli.test.ts +398 -0
  713. package/src/capsule-cli.ts +565 -0
  714. package/src/causal-behavior.ts +308 -0
  715. package/src/causal-chain.ts +419 -0
  716. package/src/causal-consolidation.ts +370 -0
  717. package/src/causal-retrieval.ts +286 -0
  718. package/src/causal-trajectory-graph.ts +60 -0
  719. package/src/causal-trajectory.ts +303 -0
  720. package/src/chunking.ts +220 -0
  721. package/src/citations.ts +232 -0
  722. package/src/cli.ts +9403 -0
  723. package/src/codex-cli-fallback.ts +162 -0
  724. package/src/codex-thread-key.ts +1 -0
  725. package/src/coding/access-coding-context.test.ts +197 -0
  726. package/src/coding/coding-branch-scope.test.ts +281 -0
  727. package/src/coding/coding-namespace.test.ts +360 -0
  728. package/src/coding/coding-namespace.ts +412 -0
  729. package/src/coding/coding-orchestrator.test.ts +249 -0
  730. package/src/coding/git-context.test.ts +507 -0
  731. package/src/coding/git-context.ts +336 -0
  732. package/src/coding/mcp-set-coding-context.test.ts +174 -0
  733. package/src/coding/review-context.test.ts +316 -0
  734. package/src/coding/review-context.ts +349 -0
  735. package/src/coding/wire-coding-context.test.ts +468 -0
  736. package/src/commitment-ledger.test.ts +78 -0
  737. package/src/commitment-ledger.ts +337 -0
  738. package/src/compat/checks.test.ts +206 -0
  739. package/src/compat/checks.ts +716 -0
  740. package/src/compat/types.ts +33 -0
  741. package/src/compounding/engine.ts +1686 -0
  742. package/src/compounding/preference-consolidator.ts +778 -0
  743. package/src/compression-optimizer.ts +312 -0
  744. package/src/config.test.ts +930 -0
  745. package/src/config.ts +3807 -0
  746. package/src/connectors/codex/instructions.md +160 -0
  747. package/src/connectors/codex/resources/namespace-cheatsheet.md +48 -0
  748. package/src/connectors/codex-marketplace.ts +500 -0
  749. package/src/connectors/codex-materialize-runner.ts +212 -0
  750. package/src/connectors/codex-materialize.ts +983 -0
  751. package/src/connectors/coerce.ts +62 -0
  752. package/src/connectors/index.test.ts +1570 -0
  753. package/src/connectors/index.ts +3222 -0
  754. package/src/connectors/live/framework.ts +164 -0
  755. package/src/connectors/live/github.test.ts +1218 -0
  756. package/src/connectors/live/github.ts +1068 -0
  757. package/src/connectors/live/gmail.test.ts +1706 -0
  758. package/src/connectors/live/gmail.ts +1293 -0
  759. package/src/connectors/live/google-drive.test.ts +696 -0
  760. package/src/connectors/live/google-drive.ts +724 -0
  761. package/src/connectors/live/index.ts +101 -0
  762. package/src/connectors/live/live-connectors.test.ts +689 -0
  763. package/src/connectors/live/notion.test.ts +1109 -0
  764. package/src/connectors/live/notion.ts +978 -0
  765. package/src/connectors/live/registry.ts +103 -0
  766. package/src/connectors/live/state-store.ts +399 -0
  767. package/src/connectors/live/transient-errors.ts +150 -0
  768. package/src/connectors/weclone-installer.test.ts +850 -0
  769. package/src/connectors-cli.ts +513 -0
  770. package/src/console/state.test.ts +224 -0
  771. package/src/console/state.ts +514 -0
  772. package/src/console/trace.test.ts +813 -0
  773. package/src/console/trace.ts +603 -0
  774. package/src/console/tui.test.ts +582 -0
  775. package/src/console/tui.ts +508 -0
  776. package/src/consolidation-operator.ts +182 -0
  777. package/src/consolidation-provenance-check.ts +551 -0
  778. package/src/consolidation-undo.ts +718 -0
  779. package/src/contradiction/contradiction-judge.test.ts +189 -0
  780. package/src/contradiction/contradiction-judge.ts +333 -0
  781. package/src/contradiction/contradiction-review.ts +574 -0
  782. package/src/contradiction/contradiction-scan.ts +504 -0
  783. package/src/contradiction/contradiction.test.ts +2230 -0
  784. package/src/contradiction/index.ts +37 -0
  785. package/src/contradiction/resolution.ts +383 -0
  786. package/src/conversation-index/backend.ts +323 -0
  787. package/src/conversation-index/chunker.ts +47 -0
  788. package/src/conversation-index/cleanup.ts +53 -0
  789. package/src/conversation-index/faiss-adapter.ts +384 -0
  790. package/src/conversation-index/indexer.test.ts +164 -0
  791. package/src/conversation-index/indexer.ts +192 -0
  792. package/src/conversation-index/search.ts +37 -0
  793. package/src/cross-namespace-budget.test.ts +275 -0
  794. package/src/cross-namespace-budget.ts +365 -0
  795. package/src/cue-anchors.ts +163 -0
  796. package/src/curation/index.ts +544 -0
  797. package/src/dashboard-runtime.ts +337 -0
  798. package/src/day-summary.ts +122 -0
  799. package/src/dedup/index.ts +330 -0
  800. package/src/dedup/semantic.test.ts +1577 -0
  801. package/src/dedup/semantic.ts +148 -0
  802. package/src/delinearize.ts +193 -0
  803. package/src/direct-answer-wiring.test.ts +473 -0
  804. package/src/direct-answer-wiring.ts +180 -0
  805. package/src/direct-answer.test.ts +484 -0
  806. package/src/direct-answer.ts +273 -0
  807. package/src/embedding-fallback.ts +565 -0
  808. package/src/enrichment/audit.ts +89 -0
  809. package/src/enrichment/index.ts +27 -0
  810. package/src/enrichment/pipeline.ts +197 -0
  811. package/src/enrichment/provider-registry.ts +85 -0
  812. package/src/enrichment/types.ts +100 -0
  813. package/src/enrichment/web-search-provider.ts +63 -0
  814. package/src/entity-retrieval.ts +774 -0
  815. package/src/entity-schema.ts +239 -0
  816. package/src/evals.ts +1312 -0
  817. package/src/event-order-recall.test.ts +4164 -0
  818. package/src/event-order-recall.ts +2802 -0
  819. package/src/evidence-pack.test.ts +89 -0
  820. package/src/evidence-pack.ts +388 -0
  821. package/src/explicit-capture.ts +530 -0
  822. package/src/explicit-cue-recall.test.ts +3019 -0
  823. package/src/explicit-cue-recall.ts +5545 -0
  824. package/src/extraction-judge-telemetry.ts +234 -0
  825. package/src/extraction-judge-training.ts +221 -0
  826. package/src/extraction-judge.ts +846 -0
  827. package/src/extraction-timeout.test.ts +265 -0
  828. package/src/extraction.ts +2719 -0
  829. package/src/fallback-llm.test.ts +1060 -0
  830. package/src/fallback-llm.ts +918 -0
  831. package/src/focused-list-recall.test.ts +734 -0
  832. package/src/focused-list-recall.ts +1160 -0
  833. package/src/graph-dashboard-diff.ts +35 -0
  834. package/src/graph-dashboard-key.ts +5 -0
  835. package/src/graph-dashboard-parser.ts +104 -0
  836. package/src/graph-edge-reinforcement.ts +192 -0
  837. package/src/graph-events.ts +151 -0
  838. package/src/graph-recall.test.ts +164 -0
  839. package/src/graph-recall.ts +189 -0
  840. package/src/graph-retrieval.test.ts +809 -0
  841. package/src/graph-retrieval.ts +823 -0
  842. package/src/graph-snapshot.ts +329 -0
  843. package/src/graph.ts +813 -0
  844. package/src/harmonic-retrieval.ts +223 -0
  845. package/src/himem.ts +154 -0
  846. package/src/hygiene.ts +87 -0
  847. package/src/identity-continuity.ts +333 -0
  848. package/src/importance.ts +328 -0
  849. package/src/importers/base.test.ts +294 -0
  850. package/src/importers/base.ts +436 -0
  851. package/src/importers/index.ts +21 -0
  852. package/src/index.ts +1204 -0
  853. package/src/intent.ts +154 -0
  854. package/src/json-extract.ts +85 -0
  855. package/src/json-store.ts +42 -0
  856. package/src/lcm/archive.ts +617 -0
  857. package/src/lcm/dag.ts +199 -0
  858. package/src/lcm/engine.ts +645 -0
  859. package/src/lcm/index.ts +7 -0
  860. package/src/lcm/queue.test.ts +178 -0
  861. package/src/lcm/queue.ts +200 -0
  862. package/src/lcm/recall.ts +117 -0
  863. package/src/lcm/schema.ts +154 -0
  864. package/src/lcm/summarizer.ts +235 -0
  865. package/src/lcm/tools.ts +191 -0
  866. package/src/lcm-engine.test.ts +660 -0
  867. package/src/legacy-hook-compat.test.ts +20 -0
  868. package/src/legacy-hook-compat.ts +45 -0
  869. package/src/lifecycle.ts +289 -0
  870. package/src/live-connectors-runner.ts +385 -0
  871. package/src/local-llm-qos.test.ts +303 -0
  872. package/src/local-llm-thinking.test.ts +292 -0
  873. package/src/local-llm.ts +1464 -0
  874. package/src/logger.ts +49 -0
  875. package/src/maintenance/archive-observations.ts +147 -0
  876. package/src/maintenance/backup-stamp.ts +3 -0
  877. package/src/maintenance/dreams-ledger.ts +516 -0
  878. package/src/maintenance/first-start-migration.ts +362 -0
  879. package/src/maintenance/forget.test.ts +206 -0
  880. package/src/maintenance/forget.ts +126 -0
  881. package/src/maintenance/graph-edge-decay.test.ts +409 -0
  882. package/src/maintenance/graph-edge-decay.ts +394 -0
  883. package/src/maintenance/memory-governance-cron.ts +447 -0
  884. package/src/maintenance/memory-governance.ts +1039 -0
  885. package/src/maintenance/migrate-observations.ts +216 -0
  886. package/src/maintenance/observation-ledger-utils.ts +54 -0
  887. package/src/maintenance/pattern-reinforcement.test.ts +875 -0
  888. package/src/maintenance/pattern-reinforcement.ts +369 -0
  889. package/src/maintenance/purge.ts +334 -0
  890. package/src/maintenance/rebuild-memory-lifecycle-ledger.ts +78 -0
  891. package/src/maintenance/rebuild-memory-projection.ts +1234 -0
  892. package/src/maintenance/rebuild-observations.ts +178 -0
  893. package/src/maintenance/tier-stats.test.ts +378 -0
  894. package/src/maintenance/tier-stats.ts +222 -0
  895. package/src/mcp-memory-inspector-app.ts +421 -0
  896. package/src/memory-action-policy.ts +80 -0
  897. package/src/memory-cache.ts +208 -0
  898. package/src/memory-extension/claude-code-publisher.ts +51 -0
  899. package/src/memory-extension/codex-publisher.ts +149 -0
  900. package/src/memory-extension/hermes-publisher.ts +51 -0
  901. package/src/memory-extension/index.ts +100 -0
  902. package/src/memory-extension/shared-instructions.ts +133 -0
  903. package/src/memory-extension/types.ts +86 -0
  904. package/src/memory-extension-host/host-discovery.ts +276 -0
  905. package/src/memory-extension-host/index.ts +14 -0
  906. package/src/memory-extension-host/render-extensions-block.ts +73 -0
  907. package/src/memory-extension-host/types.ts +21 -0
  908. package/src/memory-lifecycle-ledger-utils.ts +116 -0
  909. package/src/memory-projection-format.ts +11 -0
  910. package/src/memory-projection-store.ts +951 -0
  911. package/src/memory-provenance.test.ts +196 -0
  912. package/src/memory-provenance.ts +484 -0
  913. package/src/memory-worth-bench.test.ts +71 -0
  914. package/src/memory-worth-bench.ts +265 -0
  915. package/src/memory-worth-filter.test.ts +209 -0
  916. package/src/memory-worth-filter.ts +204 -0
  917. package/src/memory-worth-frontmatter.test.ts +311 -0
  918. package/src/memory-worth-outcomes.test.ts +316 -0
  919. package/src/memory-worth-outcomes.ts +286 -0
  920. package/src/memory-worth.test.ts +317 -0
  921. package/src/memory-worth.ts +215 -0
  922. package/src/message-parts/index.ts +806 -0
  923. package/src/message-parts/message-parts.test.ts +421 -0
  924. package/src/migrate/from-engram.ts +789 -0
  925. package/src/model-registry.ts +313 -0
  926. package/src/models-json.ts +76 -0
  927. package/src/namespaces/migrate.ts +187 -0
  928. package/src/namespaces/path.ts +25 -0
  929. package/src/namespaces/principal.test.ts +195 -0
  930. package/src/namespaces/principal.ts +86 -0
  931. package/src/namespaces/search.test.ts +105 -0
  932. package/src/namespaces/search.ts +233 -0
  933. package/src/namespaces/storage.ts +74 -0
  934. package/src/native-knowledge.ts +1823 -0
  935. package/src/negative.ts +72 -0
  936. package/src/network/tailscale.ts +179 -0
  937. package/src/network/webdav.ts +385 -0
  938. package/src/objective-state-writers.ts +951 -0
  939. package/src/objective-state.ts +320 -0
  940. package/src/onboarding/index.ts +529 -0
  941. package/src/openai-chat-compat.ts +56 -0
  942. package/src/operator-toolkit.ts +2132 -0
  943. package/src/opik-exporter.test.ts +72 -0
  944. package/src/opik-exporter.ts +587 -0
  945. package/src/orchestrator-extraction-queue.test.ts +197 -0
  946. package/src/orchestrator-flush.test.ts +1171 -0
  947. package/src/orchestrator-pattern-reinforcement.test.ts +128 -0
  948. package/src/orchestrator-source-attribution.test.ts +701 -0
  949. package/src/orchestrator.ts +16368 -0
  950. package/src/page-versioning.ts +450 -0
  951. package/src/patterns-cli.ts +574 -0
  952. package/src/peers/index.ts +54 -0
  953. package/src/peers/migrate-from-identity-anchor.test.ts +291 -0
  954. package/src/peers/migrate-from-identity-anchor.ts +350 -0
  955. package/src/peers/peers.test.ts +419 -0
  956. package/src/peers/profile-reasoner.ts +694 -0
  957. package/src/peers/storage.ts +1350 -0
  958. package/src/peers/types.ts +138 -0
  959. package/src/plugin-id.ts +84 -0
  960. package/src/policy-runtime.ts +209 -0
  961. package/src/procedural/procedure-miner.ts +150 -0
  962. package/src/procedural/procedure-recall.ts +93 -0
  963. package/src/procedural/procedure-stats.ts +213 -0
  964. package/src/procedural/procedure-types.ts +132 -0
  965. package/src/procedural/reinforcement-core.test.ts +132 -0
  966. package/src/procedural/reinforcement-core.ts +73 -0
  967. package/src/profiling.test.ts +263 -0
  968. package/src/profiling.ts +435 -0
  969. package/src/projection/index.ts +398 -0
  970. package/src/qmd-recall-cache.test.ts +138 -0
  971. package/src/qmd-recall-cache.ts +111 -0
  972. package/src/qmd.test.ts +257 -0
  973. package/src/qmd.ts +2614 -0
  974. package/src/reasoning-trace-recall.ts +201 -0
  975. package/src/reasoning-trace-types.ts +235 -0
  976. package/src/recall-audit-anomaly.test.ts +246 -0
  977. package/src/recall-audit-anomaly.ts +297 -0
  978. package/src/recall-audit.test.ts +51 -0
  979. package/src/recall-audit.ts +72 -0
  980. package/src/recall-budget-config.test.ts +87 -0
  981. package/src/recall-disclosure-escalation.test.ts +196 -0
  982. package/src/recall-disclosure-escalation.ts +158 -0
  983. package/src/recall-disclosure-shaping.test.ts +146 -0
  984. package/src/recall-disclosure.test.ts +214 -0
  985. package/src/recall-explain-renderer.test.ts +140 -0
  986. package/src/recall-explain-renderer.ts +356 -0
  987. package/src/recall-mmr.test.ts +808 -0
  988. package/src/recall-mmr.ts +607 -0
  989. package/src/recall-qos.test.ts +85 -0
  990. package/src/recall-qos.ts +82 -0
  991. package/src/recall-query-policy.ts +221 -0
  992. package/src/recall-state.test.ts +233 -0
  993. package/src/recall-state.ts +456 -0
  994. package/src/recall-tag-filter.ts +143 -0
  995. package/src/recall-tokenization.ts +35 -0
  996. package/src/recall-xray-cli.test.ts +118 -0
  997. package/src/recall-xray-cli.ts +100 -0
  998. package/src/recall-xray-disclosure-telemetry.test.ts +183 -0
  999. package/src/recall-xray-renderer.test.ts +539 -0
  1000. package/src/recall-xray-renderer.ts +487 -0
  1001. package/src/recall-xray.test.ts +503 -0
  1002. package/src/recall-xray.ts +621 -0
  1003. package/src/reconstruct.ts +41 -0
  1004. package/src/release-changelog.ts +35 -0
  1005. package/src/relevance.ts +67 -0
  1006. package/src/replay/normalizers/chatgpt.ts +133 -0
  1007. package/src/replay/normalizers/claude.ts +102 -0
  1008. package/src/replay/normalizers/openclaw.ts +119 -0
  1009. package/src/replay/normalizers/shared.ts +69 -0
  1010. package/src/replay/runner.ts +197 -0
  1011. package/src/replay/types.ts +143 -0
  1012. package/src/rerank.test.ts +48 -0
  1013. package/src/rerank.ts +176 -0
  1014. package/src/resolve-auth-token.test.ts +226 -0
  1015. package/src/resolve-auth-token.ts +151 -0
  1016. package/src/resolve-provider-secret.test.ts +187 -0
  1017. package/src/resolve-provider-secret.ts +410 -0
  1018. package/src/response-guidance-recall.test.ts +3952 -0
  1019. package/src/response-guidance-recall.ts +4431 -0
  1020. package/src/resume-bundles.ts +415 -0
  1021. package/src/retrieval-agents.ts +623 -0
  1022. package/src/retrieval-tiers.ts +25 -0
  1023. package/src/retrieval.ts +104 -0
  1024. package/src/review/index.test.ts +201 -0
  1025. package/src/review/index.ts +536 -0
  1026. package/src/routing/engine.ts +162 -0
  1027. package/src/routing/store.ts +321 -0
  1028. package/src/runtime/better-sqlite.test.ts +32 -0
  1029. package/src/runtime/better-sqlite.ts +76 -0
  1030. package/src/runtime/child-process.ts +67 -0
  1031. package/src/runtime/env.ts +48 -0
  1032. package/src/sanitize.ts +58 -0
  1033. package/src/schemas.ts +449 -0
  1034. package/src/sdk-compat.ts +87 -0
  1035. package/src/search/document-scanner.ts +96 -0
  1036. package/src/search/embed-helper.ts +142 -0
  1037. package/src/search/factory.ts +189 -0
  1038. package/src/search/index.ts +10 -0
  1039. package/src/search/lancedb-backend.ts +342 -0
  1040. package/src/search/meilisearch-backend.ts +232 -0
  1041. package/src/search/noop-backend.ts +57 -0
  1042. package/src/search/orama-backend.ts +358 -0
  1043. package/src/search/port.ts +86 -0
  1044. package/src/search/remote-backend.ts +124 -0
  1045. package/src/secure-store/cipher.ts +271 -0
  1046. package/src/secure-store/cli-handlers.ts +355 -0
  1047. package/src/secure-store/cli-renderer.ts +131 -0
  1048. package/src/secure-store/header.ts +373 -0
  1049. package/src/secure-store/index.ts +137 -0
  1050. package/src/secure-store/kdf.ts +263 -0
  1051. package/src/secure-store/keyring.ts +106 -0
  1052. package/src/secure-store/metadata.ts +394 -0
  1053. package/src/secure-store/passphrase-reader.ts +252 -0
  1054. package/src/secure-store/secure-fs.ts +571 -0
  1055. package/src/secure-store/secure-store.test.ts +755 -0
  1056. package/src/semantic-chunking.ts +545 -0
  1057. package/src/semantic-consolidation.test.ts +182 -0
  1058. package/src/semantic-consolidation.ts +432 -0
  1059. package/src/semantic-rule-promotion.ts +183 -0
  1060. package/src/semantic-rule-verifier.ts +160 -0
  1061. package/src/session-integrity.ts +569 -0
  1062. package/src/session-observer-bands.ts +11 -0
  1063. package/src/session-observer-state.ts +346 -0
  1064. package/src/session-toggles.test.ts +96 -0
  1065. package/src/session-toggles.ts +159 -0
  1066. package/src/shared-context/manager.ts +810 -0
  1067. package/src/signal.ts +84 -0
  1068. package/src/skills-registry.test.ts +277 -0
  1069. package/src/skills-registry.ts +120 -0
  1070. package/src/source-attribution-roundtrip.test.ts +215 -0
  1071. package/src/source-attribution.test.ts +1425 -0
  1072. package/src/source-attribution.ts +639 -0
  1073. package/src/spaces/index.ts +627 -0
  1074. package/src/storage-paths.ts +117 -0
  1075. package/src/storage.ts +6657 -0
  1076. package/src/store-contract.ts +55 -0
  1077. package/src/summarizer.ts +844 -0
  1078. package/src/summary-snapshot.test.ts +681 -0
  1079. package/src/summary-snapshot.ts +238 -0
  1080. package/src/surfaces/dreams.test.ts +394 -0
  1081. package/src/surfaces/dreams.ts +346 -0
  1082. package/src/surfaces/heartbeat.test.ts +415 -0
  1083. package/src/surfaces/heartbeat.ts +325 -0
  1084. package/src/sync/index.ts +308 -0
  1085. package/src/targeted-fact-recall.test.ts +1694 -0
  1086. package/src/targeted-fact-recall.ts +2905 -0
  1087. package/src/taxonomy/default-taxonomy.ts +87 -0
  1088. package/src/taxonomy/index.ts +26 -0
  1089. package/src/taxonomy/resolver-doc-generator.ts +57 -0
  1090. package/src/taxonomy/resolver.ts +184 -0
  1091. package/src/taxonomy/taxonomy-loader.ts +186 -0
  1092. package/src/taxonomy/types.ts +48 -0
  1093. package/src/telemetry-transcript.ts +70 -0
  1094. package/src/temporal-index.ts +890 -0
  1095. package/src/temporal-supersession.test.ts +2703 -0
  1096. package/src/temporal-supersession.ts +493 -0
  1097. package/src/temporal-validity.test.ts +448 -0
  1098. package/src/temporal-validity.ts +123 -0
  1099. package/src/threading.ts +395 -0
  1100. package/src/tier-migration.ts +124 -0
  1101. package/src/tier-routing.ts +102 -0
  1102. package/src/tmt.ts +462 -0
  1103. package/src/tokens.test.ts +178 -0
  1104. package/src/tokens.ts +279 -0
  1105. package/src/topics.ts +147 -0
  1106. package/src/training-export/cli-date-validation.test.ts +258 -0
  1107. package/src/training-export/converter.test.ts +452 -0
  1108. package/src/training-export/converter.ts +319 -0
  1109. package/src/training-export/date-parse.ts +117 -0
  1110. package/src/training-export/index.ts +26 -0
  1111. package/src/training-export/registry.test.ts +85 -0
  1112. package/src/training-export/registry.ts +57 -0
  1113. package/src/training-export/types.ts +31 -0
  1114. package/src/transcript.ts +1179 -0
  1115. package/src/transfer/autodetect.ts +30 -0
  1116. package/src/transfer/backup.ts +138 -0
  1117. package/src/transfer/capsule-crypto.ts +485 -0
  1118. package/src/transfer/capsule-encrypt.test.ts +690 -0
  1119. package/src/transfer/capsule-export.ts +543 -0
  1120. package/src/transfer/capsule-fork.ts +375 -0
  1121. package/src/transfer/capsule-import.ts +564 -0
  1122. package/src/transfer/capsule-merge.ts +433 -0
  1123. package/src/transfer/conflict-policy.ts +16 -0
  1124. package/src/transfer/constants.ts +13 -0
  1125. package/src/transfer/exclusions.ts +37 -0
  1126. package/src/transfer/export-json.ts +65 -0
  1127. package/src/transfer/export-md.ts +59 -0
  1128. package/src/transfer/export-sqlite.ts +52 -0
  1129. package/src/transfer/fs-utils.ts +269 -0
  1130. package/src/transfer/import-json.ts +108 -0
  1131. package/src/transfer/import-md.ts +84 -0
  1132. package/src/transfer/import-sqlite.ts +100 -0
  1133. package/src/transfer/integrity.ts +71 -0
  1134. package/src/transfer/sqlite-schema.ts +16 -0
  1135. package/src/transfer/types.ts +297 -0
  1136. package/src/trust-zones.ts +1186 -0
  1137. package/src/types.ts +3074 -0
  1138. package/src/user-model.test.ts +124 -0
  1139. package/src/user-model.ts +162 -0
  1140. package/src/utility-learner.ts +353 -0
  1141. package/src/utility-runtime.ts +88 -0
  1142. package/src/utility-telemetry.ts +215 -0
  1143. package/src/utils/category-dir.ts +44 -0
  1144. package/src/utils/errno.ts +6 -0
  1145. package/src/utils/iso-timestamp.test.ts +37 -0
  1146. package/src/utils/iso-timestamp.ts +164 -0
  1147. package/src/utils/path.ts +26 -0
  1148. package/src/verified-recall.ts +138 -0
  1149. package/src/version-utils.test.ts +10 -0
  1150. package/src/version-utils.ts +9 -0
  1151. package/src/whitespace.ts +10 -0
  1152. package/src/work/board.ts +359 -0
  1153. package/src/work/boundary.ts +107 -0
  1154. package/src/work/storage.ts +436 -0
  1155. package/src/work/types.ts +82 -0
  1156. package/src/work-product-ledger.ts +265 -0
  1157. package/dist/access-service-DDjzFALq.d.ts +0 -2088
  1158. package/dist/capsule-crypto-SJS5VVAP.js +0 -18
  1159. package/dist/capsule-export-7QNCBZOQ.js +0 -17
  1160. package/dist/capsule-import-EPBHD2EN.js +0 -16
  1161. package/dist/capsule-merge-DI7PNQ2H.js +0 -189
  1162. package/dist/chunk-23ZZK64Y.js +0 -26
  1163. package/dist/chunk-23ZZK64Y.js.map +0 -1
  1164. package/dist/chunk-242S3I2A.js +0 -647
  1165. package/dist/chunk-2LGMW3DJ.js +0 -111
  1166. package/dist/chunk-3B6KIRBH.js +0 -5213
  1167. package/dist/chunk-3B6KIRBH.js.map +0 -1
  1168. package/dist/chunk-457A4P3L.js +0 -119
  1169. package/dist/chunk-457A4P3L.js.map +0 -1
  1170. package/dist/chunk-4IS4SXIQ.js +0 -2040
  1171. package/dist/chunk-4YM32CRU.js +0 -721
  1172. package/dist/chunk-6TBWYBJ3.js +0 -236
  1173. package/dist/chunk-74EMIVE4.js +0 -329
  1174. package/dist/chunk-74EMIVE4.js.map +0 -1
  1175. package/dist/chunk-767ODGE6.js +0 -183
  1176. package/dist/chunk-7V22HTMD.js +0 -623
  1177. package/dist/chunk-7V22HTMD.js.map +0 -1
  1178. package/dist/chunk-7ZM3BFKK.js +0 -9705
  1179. package/dist/chunk-7ZM3BFKK.js.map +0 -1
  1180. package/dist/chunk-AQJNPMOA.js +0 -643
  1181. package/dist/chunk-AQJNPMOA.js.map +0 -1
  1182. package/dist/chunk-ASAITVLA.js +0 -64
  1183. package/dist/chunk-ASAITVLA.js.map +0 -1
  1184. package/dist/chunk-BBE34QBJ.js +0 -275
  1185. package/dist/chunk-BBE34QBJ.js.map +0 -1
  1186. package/dist/chunk-BZSQEPRW.js +0 -14710
  1187. package/dist/chunk-BZSQEPRW.js.map +0 -1
  1188. package/dist/chunk-CPKTBRS2.js +0 -891
  1189. package/dist/chunk-CPKTBRS2.js.map +0 -1
  1190. package/dist/chunk-D4GAOFF6.js +0 -562
  1191. package/dist/chunk-D4GAOFF6.js.map +0 -1
  1192. package/dist/chunk-D54LZC5L.js +0 -147
  1193. package/dist/chunk-DF3RVK3X.js +0 -119
  1194. package/dist/chunk-DF3RVK3X.js.map +0 -1
  1195. package/dist/chunk-DZZPC36E.js +0 -1451
  1196. package/dist/chunk-DZZPC36E.js.map +0 -1
  1197. package/dist/chunk-E2UCDP5S.js +0 -570
  1198. package/dist/chunk-E6K4NIEU.js +0 -747
  1199. package/dist/chunk-E6K4NIEU.js.map +0 -1
  1200. package/dist/chunk-EEQLFRUM.js +0 -89
  1201. package/dist/chunk-ETOW6ACV.js +0 -158
  1202. package/dist/chunk-ETOW6ACV.js.map +0 -1
  1203. package/dist/chunk-FMEBPEAO.js +0 -347
  1204. package/dist/chunk-FMEBPEAO.js.map +0 -1
  1205. package/dist/chunk-FQDPCE3I.js +0 -1837
  1206. package/dist/chunk-FQDPCE3I.js.map +0 -1
  1207. package/dist/chunk-FYIYMQ5N.js +0 -221
  1208. package/dist/chunk-FYIYMQ5N.js.map +0 -1
  1209. package/dist/chunk-G2WADRQ3.js +0 -219
  1210. package/dist/chunk-G4SK7DSQ.js +0 -121
  1211. package/dist/chunk-GVPWB7EY.js +0 -390
  1212. package/dist/chunk-GVPWB7EY.js.map +0 -1
  1213. package/dist/chunk-HELQZFZO.js +0 -1075
  1214. package/dist/chunk-HL5LRPNA.js +0 -1914
  1215. package/dist/chunk-HL5LRPNA.js.map +0 -1
  1216. package/dist/chunk-HQZVVSVB.js +0 -147
  1217. package/dist/chunk-HQZVVSVB.js.map +0 -1
  1218. package/dist/chunk-HY3L4WKC.js +0 -2195
  1219. package/dist/chunk-HY3L4WKC.js.map +0 -1
  1220. package/dist/chunk-IB3BFHGN.js +0 -228
  1221. package/dist/chunk-IXEJRKCZ.js +0 -18
  1222. package/dist/chunk-JBMSGZEQ.js +0 -441
  1223. package/dist/chunk-JBMSGZEQ.js.map +0 -1
  1224. package/dist/chunk-JESOB2HO.js +0 -108
  1225. package/dist/chunk-JKDVIE52.js +0 -272
  1226. package/dist/chunk-JRNQ3RNA.js +0 -284
  1227. package/dist/chunk-JRNQ3RNA.js.map +0 -1
  1228. package/dist/chunk-K6WK37A6.js +0 -865
  1229. package/dist/chunk-K6WK37A6.js.map +0 -1
  1230. package/dist/chunk-MARWOCVP.js +0 -48
  1231. package/dist/chunk-MNU6ZBWT.js +0 -4454
  1232. package/dist/chunk-MNU6ZBWT.js.map +0 -1
  1233. package/dist/chunk-N5AKDXAI.js +0 -74
  1234. package/dist/chunk-OA3L7BFR.js +0 -183
  1235. package/dist/chunk-OA3L7BFR.js.map +0 -1
  1236. package/dist/chunk-OR64ZGRZ.js +0 -23
  1237. package/dist/chunk-P77UEOU2.js +0 -1521
  1238. package/dist/chunk-P77UEOU2.js.map +0 -1
  1239. package/dist/chunk-PH4C2U43.js +0 -239
  1240. package/dist/chunk-PH4C2U43.js.map +0 -1
  1241. package/dist/chunk-RVPLBATS.js +0 -1586
  1242. package/dist/chunk-RVPLBATS.js.map +0 -1
  1243. package/dist/chunk-U5JMRGKX.js +0 -340
  1244. package/dist/chunk-U5JMRGKX.js.map +0 -1
  1245. package/dist/chunk-URB2WSKZ.js +0 -350
  1246. package/dist/chunk-URB2WSKZ.js.map +0 -1
  1247. package/dist/chunk-UVMUAWVT.js +0 -596
  1248. package/dist/chunk-WEJG4TB5.js +0 -118
  1249. package/dist/chunk-X7HPGUVG.js +0 -271
  1250. package/dist/chunk-XAMBKFQS.js +0 -2777
  1251. package/dist/chunk-XAMBKFQS.js.map +0 -1
  1252. package/dist/chunk-XJKFSSDW.js +0 -726
  1253. package/dist/chunk-XJKFSSDW.js.map +0 -1
  1254. package/dist/chunk-XMHBH5H6.js +0 -283
  1255. package/dist/chunk-XMHBH5H6.js.map +0 -1
  1256. package/dist/chunk-XMVFHBHT.js +0 -277
  1257. package/dist/chunk-Y3VMVTYX.js +0 -53
  1258. package/dist/chunk-YNB73F22.js +0 -137
  1259. package/dist/chunk-YNB73F22.js.map +0 -1
  1260. package/dist/chunk-Z2E7VW55.js +0 -335
  1261. package/dist/chunk-Z2E7VW55.js.map +0 -1
  1262. package/dist/chunk-ZG7PTKBK.js +0 -2296
  1263. package/dist/chunk-ZNQN6ZTA.js +0 -135
  1264. package/dist/chunk-ZVTKDVVM.js +0 -827
  1265. package/dist/chunk-ZVTKDVVM.js.map +0 -1
  1266. package/dist/cli-BR8KpIU0.d.ts +0 -1259
  1267. package/dist/codex-materialize-CQlLTzke.d.ts +0 -139
  1268. package/dist/connectors-cli-DFGtY2DB.d.ts +0 -257
  1269. package/dist/contradiction-review-5LTTVDQV.js +0 -22
  1270. package/dist/contradiction-scan-QTXAMBUA.js +0 -414
  1271. package/dist/contradiction-scan-QTXAMBUA.js.map +0 -1
  1272. package/dist/engine-35M5BKQ7.js +0 -28
  1273. package/dist/fs-utils-IRVUFB6G.js +0 -30
  1274. package/dist/graph-edge-decay-PWB63GRE.js +0 -207
  1275. package/dist/memory-governance-IMPQZXFC.js +0 -37
  1276. package/dist/memory-projection-store-CY8TU40w.d.ts +0 -222
  1277. package/dist/orchestrator-DDMPqU6R.d.ts +0 -1792
  1278. package/dist/path-RMTY5Y5A.js +0 -9
  1279. package/dist/port-B6VEDIkC.d.ts +0 -53
  1280. package/dist/resolution-YGIBORXI.js +0 -101
  1281. package/dist/resolution-YGIBORXI.js.map +0 -1
  1282. package/dist/secure-store-4R2GSO7S.js +0 -156
  1283. package/dist/semantic-consolidation-ByBXb-sf.d.ts +0 -180
  1284. package/dist/state-store-3EH7HYIN.js +0 -16
  1285. package/dist/types-V3FJ26TF.js +0 -30
  1286. /package/dist/{capsule-crypto-SJS5VVAP.js.map → adapters/claude-code.js.map} +0 -0
  1287. /package/dist/{capsule-export-7QNCBZOQ.js.map → adapters/codex.js.map} +0 -0
  1288. /package/dist/{capsule-import-EPBHD2EN.js.map → adapters/hermes.js.map} +0 -0
  1289. /package/dist/{contradiction-review-5LTTVDQV.js.map → adapters/index.js.map} +0 -0
  1290. /package/dist/{engine-35M5BKQ7.js.map → adapters/registry.js.map} +0 -0
  1291. /package/dist/{fs-utils-IRVUFB6G.js.map → adapters/replit.js.map} +0 -0
  1292. /package/dist/{memory-governance-IMPQZXFC.js.map → adapters/types.js.map} +0 -0
  1293. /package/dist/{path-RMTY5Y5A.js.map → capsule-crypto-5CYAGVC5.js.map} +0 -0
  1294. /package/dist/{capsule-merge-DI7PNQ2H.js.map → capsule-merge-4MGKE7C5.js.map} +0 -0
  1295. /package/dist/{chunk-G4SK7DSQ.js.map → chunk-2WWLHTZY.js.map} +0 -0
  1296. /package/dist/{chunk-X7HPGUVG.js.map → chunk-4CRG46BG.js.map} +0 -0
  1297. /package/dist/{chunk-UVMUAWVT.js.map → chunk-7IASACLB.js.map} +0 -0
  1298. /package/dist/{chunk-HELQZFZO.js.map → chunk-EDTHC6UD.js.map} +0 -0
  1299. /package/dist/{chunk-4YM32CRU.js.map → chunk-EFJ3MQ4V.js.map} +0 -0
  1300. /package/dist/{chunk-E2UCDP5S.js.map → chunk-FBYESMQ2.js.map} +0 -0
  1301. /package/dist/{chunk-D54LZC5L.js.map → chunk-FDU6HUUL.js.map} +0 -0
  1302. /package/dist/{chunk-IB3BFHGN.js.map → chunk-GGKRUQOO.js.map} +0 -0
  1303. /package/dist/{chunk-242S3I2A.js.map → chunk-GL6I6MEQ.js.map} +0 -0
  1304. /package/dist/{secure-store-4R2GSO7S.js.map → chunk-HHLLAQGZ.js.map} +0 -0
  1305. /package/dist/{chunk-4IS4SXIQ.js.map → chunk-HXXBL2KD.js.map} +0 -0
  1306. /package/dist/{chunk-767ODGE6.js.map → chunk-KNKUID7G.js.map} +0 -0
  1307. /package/dist/{chunk-6TBWYBJ3.js.map → chunk-LPMVBPA3.js.map} +0 -0
  1308. /package/dist/{chunk-WEJG4TB5.js.map → chunk-MC26UJIM.js.map} +0 -0
  1309. /package/dist/{chunk-JKDVIE52.js.map → chunk-MGKYQQYF.js.map} +0 -0
  1310. /package/dist/{chunk-Y3VMVTYX.js.map → chunk-MT4HVDUZ.js.map} +0 -0
  1311. /package/dist/{chunk-G2WADRQ3.js.map → chunk-MY6TPVXW.js.map} +0 -0
  1312. /package/dist/{chunk-OR64ZGRZ.js.map → chunk-NNVTUXEB.js.map} +0 -0
  1313. /package/dist/{chunk-JESOB2HO.js.map → chunk-P4NEIHUT.js.map} +0 -0
  1314. /package/dist/{chunk-IXEJRKCZ.js.map → chunk-QRNI5JBH.js.map} +0 -0
  1315. /package/dist/{chunk-EEQLFRUM.js.map → chunk-RRF5UOBJ.js.map} +0 -0
  1316. /package/dist/{state-store-3EH7HYIN.js.map → chunk-SEDEKFYQ.js.map} +0 -0
  1317. /package/dist/{chunk-2LGMW3DJ.js.map → chunk-U3PN77QT.js.map} +0 -0
  1318. /package/dist/{chunk-XMVFHBHT.js.map → chunk-U3WSW6PZ.js.map} +0 -0
  1319. /package/dist/{chunk-N5AKDXAI.js.map → chunk-UWVJF25J.js.map} +0 -0
  1320. /package/dist/{types-V3FJ26TF.js.map → chunk-V5OCT34X.js.map} +0 -0
  1321. /package/dist/{chunk-ZG7PTKBK.js.map → chunk-W3LR522O.js.map} +0 -0
  1322. /package/dist/{chunk-MARWOCVP.js.map → chunk-XIG5PDM7.js.map} +0 -0
  1323. /package/dist/{chunk-ZNQN6ZTA.js.map → chunk-XVZ7B3HG.js.map} +0 -0
  1324. /package/dist/{graph-edge-decay-PWB63GRE.js.map → graph-edge-decay-5DI5GUNL.js.map} +0 -0
@@ -0,0 +1,3222 @@
1
+ /**
2
+ * @remnic/core — Connector Manager
3
+ *
4
+ * Metadata-driven registry for host adapters (Codex CLI, Claude Code, Cursor, etc.).
5
+ * Manages connector lifecycle: install, remove, configure, health.
6
+ */
7
+
8
+ import fs from "node:fs";
9
+ import path from "node:path";
10
+ import os from "node:os";
11
+ import { createRequire } from "node:module";
12
+ import { fileURLToPath } from "node:url";
13
+
14
+ import { generateToken, revokeToken, buildTokenEntry, commitTokenEntry, loadTokenStore, saveTokenStore } from "../tokens.js";
15
+ import { launchProcessSync } from "../runtime/child-process.js";
16
+ import { mergeEnv, readEnvVar, resolveHomeDir } from "../runtime/env.js";
17
+ import { expandTildePath } from "../utils/path.js";
18
+ import { coerceInstallExtension } from "./coerce.js";
19
+
20
+ // Native memory artifact materialization for Codex CLI (#378). Surfaced here
21
+ // so downstream callers can `import { materializeForNamespace } from "@remnic/core/connectors"`.
22
+ export {
23
+ materializeForNamespace,
24
+ ensureSentinel,
25
+ describeMemoriesDir,
26
+ renderMemorySummary,
27
+ renderMemoryMd,
28
+ renderRawMemories,
29
+ renderRolloutSummary,
30
+ validateMemoryMd,
31
+ approximateTokenCount,
32
+ truncateToTokenBudget,
33
+ MATERIALIZE_VERSION,
34
+ SENTINEL_FILE,
35
+ TMP_DIR,
36
+ type MaterializeOptions,
37
+ type MaterializeResult,
38
+ type RolloutSummaryInput,
39
+ type MemoryMdValidation,
40
+ } from "./codex-materialize.js";
41
+ export {
42
+ runCodexMaterialize,
43
+ type RunMaterializeOptions,
44
+ } from "./codex-materialize-runner.js";
45
+ export {
46
+ generateMarketplaceManifest,
47
+ validateMarketplaceManifest,
48
+ checkMarketplaceManifest,
49
+ writeMarketplaceManifest,
50
+ installFromMarketplace,
51
+ MARKETPLACE_SCHEMA_VERSION,
52
+ MARKETPLACE_MANIFEST_FILENAME,
53
+ type MarketplaceManifest,
54
+ type MarketplaceEntry,
55
+ type MarketplaceConfig,
56
+ type MarketplaceInstallType,
57
+ type MarketplaceInstallResult,
58
+ type MarketplaceValidation,
59
+ type MarketplaceLogger,
60
+ } from "./codex-marketplace.js";
61
+
62
+ // ── Types ────────────────────────────────────────────────────────────────────
63
+
64
+ export interface ConnectorManifest {
65
+ /** Unique connector ID (e.g. "claude-code", "codex-cli") */
66
+ id: string;
67
+ /** Human-readable name */
68
+ name: string;
69
+ /** Version */
70
+ version: string;
71
+ /** Description */
72
+ description: string;
73
+ /** Capabilities */
74
+ capabilities: ConnectorCapability;
75
+ /** Required config fields */
76
+ configSchema?: Record<string, string>;
77
+ /** Whether currently installed */
78
+ installed?: boolean;
79
+ /** Homepage URL */
80
+ homepage?: string;
81
+ /** Author */
82
+ author?: string;
83
+ /** Repository URL */
84
+ repository?: string;
85
+ /** Tags */
86
+ tags?: string[];
87
+ /**
88
+ * Whether this connector requires a bearer token for daemon authentication.
89
+ * When false (the default), installConnector will NOT generate or persist a
90
+ * token entry in tokens.json — credentials are never materialized on disk for
91
+ * connectors that use MCP, embedded, CLI, or SDK transports that don't need
92
+ * token auth. Set to true only for HTTP connectors that actually authenticate
93
+ * requests with a bearer token (e.g. hermes, replit, generic-mcp).
94
+ */
95
+ requiresToken?: boolean;
96
+ }
97
+
98
+ export interface ConnectorCapability {
99
+ /** Can observe conversations */
100
+ observe: boolean;
101
+ /** Can recall/query memories */
102
+ recall: boolean;
103
+ /** Can store memories */
104
+ store: boolean;
105
+ /** Can search */
106
+ search: boolean;
107
+ /** Can manage entities */
108
+ entities: boolean;
109
+ /** Supports real-time sync */
110
+ realtimeSync: boolean;
111
+ /** Supports batch operations */
112
+ batch: boolean;
113
+ /** Max memory budget in chars */
114
+ maxBudgetChars?: number;
115
+ /** Connection type */
116
+ connectionType: "mcp" | "http" | "cli" | "sdk" | "embedded";
117
+ }
118
+
119
+ export interface ConnectorInstance {
120
+ /** Connector ID */
121
+ connectorId: string;
122
+ /** Resolved config */
123
+ config: Record<string, unknown>;
124
+ /** Status */
125
+ status: "installed" | "running" | "error" | "disabled";
126
+ /** Installed at timestamp */
127
+ installedAt?: string;
128
+ /** Error message if erro */
129
+ error?: string;
130
+ }
131
+
132
+ export interface ConnectorRegistry {
133
+ /** Known connectors */
134
+ connectors: ConnectorManifest[];
135
+ /** Registry file path */
136
+ registryPath: string;
137
+ }
138
+
139
+ export interface InstallOptions {
140
+ /** Connector ID to install */
141
+ connectorId: string;
142
+ /** Config values */
143
+ config?: Record<string, unknown>;
144
+ /** Memory directory */
145
+ memoryDir?: string;
146
+ /** Whether to force reinstall */
147
+ force?: boolean;
148
+ }
149
+
150
+ export interface InstallResult {
151
+ /** Connector ID */
152
+ connectorId: string;
153
+ /** Status */
154
+ status: "installed" | "already_installed" | "config_required" | "error";
155
+ /** Config path */
156
+ configPath?: string;
157
+ /** Message */
158
+ message: string;
159
+ }
160
+
161
+ export interface RemoveResult {
162
+ /** Connector ID */
163
+ connectorId: string;
164
+ /** Removed config path */
165
+ configPath: string;
166
+ /** Message */
167
+ message: string;
168
+ /** Status: "removed" on success, "error" if the removal failed partway, "not_found" if the connector was not installed, "skipped" if removal was aborted (e.g. malformed config). */
169
+ status: "removed" | "error" | "not_found" | "skipped";
170
+ /** Machine-readable skip reason (present when status === "skipped"). */
171
+ reason?: string;
172
+ }
173
+
174
+ export interface DoctorResult {
175
+ /** Connector ID */
176
+ connectorId: string;
177
+ /** Checks */
178
+ checks: DoctorCheck[];
179
+ /** All healthy */
180
+ healthy: boolean;
181
+ }
182
+
183
+ export interface DoctorCheck {
184
+ /** Check name */
185
+ name: string;
186
+ /** Passed */
187
+ ok: boolean;
188
+ /** Detail */
189
+ detail: string;
190
+ }
191
+
192
+ // ── Helpers (Finding 4) ───────────────────────────────────────────────────
193
+
194
+ // Re-export coerceInstallExtension so existing import sites
195
+ // (`import { coerceInstallExtension } from "./index.js"`) keep working without
196
+ // change. The binding comes from the top-level import above.
197
+ export { coerceInstallExtension };
198
+
199
+ // ── Built-in connector definitions ─────────────────────────────────────────
200
+
201
+ const BUILTIN_CONNECTORS: ConnectorManifest[] = [
202
+ {
203
+ id: "claude-code",
204
+ name: "Claude Code",
205
+ version: "1.0.0",
206
+ description: "Anthropic's Claude Code CLI — direct memory access via MCP",
207
+ capabilities: {
208
+ observe: true,
209
+ recall: true,
210
+ store: true,
211
+ search: true,
212
+ entities: true,
213
+ realtimeSync: true,
214
+ batch: false,
215
+ maxBudgetChars: 32000,
216
+ connectionType: "mcp",
217
+ },
218
+ configSchema: {
219
+ mcpServerUrl: "URL of the MCP Remnic server",
220
+ namespace: "Optional namespace (default: 'default')",
221
+ },
222
+ homepage: "https://claude.ai/code",
223
+ author: "Anthropic",
224
+ tags: ["official", "ai", "claude"],
225
+ requiresToken: true,
226
+ },
227
+ {
228
+ id: "codex-cli",
229
+ name: "Codex CLI",
230
+ version: "1.0.0",
231
+ description: "OpenAI Codex CLI — memory via MCP tool",
232
+ capabilities: {
233
+ observe: true,
234
+ recall: true,
235
+ store: true,
236
+ search: false,
237
+ entities: false,
238
+ realtimeSync: false,
239
+ batch: true,
240
+ maxBudgetChars: 8000,
241
+ connectionType: "mcp",
242
+ },
243
+ configSchema: {
244
+ mcpServerUrl: "URL of the MCP Remnic server",
245
+ namespace: "Optional namespace",
246
+ },
247
+ homepage: "https://openai.com/codex",
248
+ author: "OpenAI",
249
+ tags: ["official", "ai", "codex"],
250
+ requiresToken: true,
251
+ },
252
+ {
253
+ id: "cursor",
254
+ name: "Cursor IDE",
255
+ version: "1.0.0",
256
+ description: "Cursor IDE — memory via config file + tool calls",
257
+ capabilities: {
258
+ observe: false,
259
+ recall: true,
260
+ store: false,
261
+ search: true,
262
+ entities: false,
263
+ realtimeSync: false,
264
+ batch: false,
265
+ maxBudgetChars: 32000,
266
+ connectionType: "embedded",
267
+ },
268
+ configSchema: {
269
+ memoryDir: "Path to Remnic memory directory",
270
+ },
271
+ homepage: "https://cursor.com",
272
+ author: "Cursor Inc.",
273
+ tags: ["official", "ide"],
274
+ },
275
+ {
276
+ id: "cline",
277
+ name: "Cline",
278
+ version: "1.0.0",
279
+ description: "VS Code Cline extension — memory via MCP",
280
+ capabilities: {
281
+ observe: true,
282
+ recall: true,
283
+ store: true,
284
+ search: false,
285
+ entities: false,
286
+ realtimeSync: false,
287
+ batch: true,
288
+ maxBudgetChars: 8000,
289
+ connectionType: "mcp",
290
+ },
291
+ configSchema: {
292
+ mcpServerUrl: "URL of the MCP Remnic server",
293
+ namespace: "Optional namespace",
294
+ },
295
+ homepage: "https://github.com/cline/cline",
296
+ author: "Cline",
297
+ tags: ["community", "vscode"],
298
+ },
299
+ {
300
+ id: "github-copilot",
301
+ name: "GitHub Copilot",
302
+ version: "1.0.0",
303
+ description: "GitHub Copilot — memory via MCP server",
304
+ capabilities: {
305
+ observe: false,
306
+ recall: true,
307
+ store: false,
308
+ search: true,
309
+ entities: false,
310
+ realtimeSync: false,
311
+ batch: false,
312
+ maxBudgetChars: 16000,
313
+ connectionType: "mcp",
314
+ },
315
+ configSchema: {
316
+ mcpServerUrl: "URL of the MCP Remnic server",
317
+ },
318
+ homepage: "https://github.com/features/copilot",
319
+ author: "GitHub",
320
+ tags: ["official", "ai", "github"],
321
+ },
322
+ {
323
+ id: "roo-code",
324
+ name: "Roo Code",
325
+ version: "1.0.0",
326
+ description: "Roo Code — memory via MCP",
327
+ capabilities: {
328
+ observe: true,
329
+ recall: true,
330
+ store: true,
331
+ search: false,
332
+ entities: false,
333
+ realtimeSync: false,
334
+ batch: true,
335
+ maxBudgetChars: 16000,
336
+ connectionType: "mcp",
337
+ },
338
+ configSchema: {
339
+ mcpServerUrl: "URL of the MCP Remnic server",
340
+ namespace: "Optional namespace",
341
+ },
342
+ homepage: "https://roocode.com",
343
+ author: "Roo Code",
344
+ tags: ["community", "vscode"],
345
+ },
346
+ {
347
+ id: "windsurf",
348
+ name: "Windsurf",
349
+ version: "1.0.0",
350
+ description: "Windsurf IDE — memory via MCP",
351
+ capabilities: {
352
+ observe: true,
353
+ recall: true,
354
+ store: true,
355
+ search: true,
356
+ entities: false,
357
+ realtimeSync: false,
358
+ batch: false,
359
+ maxBudgetChars: 32000,
360
+ connectionType: "mcp",
361
+ },
362
+ configSchema: {
363
+ mcpServerUrl: "URL of the MCP Remnic server",
364
+ },
365
+ homepage: "https://windsurf.com",
366
+ author: "Codeium",
367
+ tags: ["official", "ide"],
368
+ },
369
+ {
370
+ id: "amp",
371
+ name: "Amp",
372
+ version: "1.0.0",
373
+ description: "Amp coding agent — memory via MCP",
374
+ capabilities: {
375
+ observe: true,
376
+ recall: true,
377
+ store: true,
378
+ search: true,
379
+ entities: false,
380
+ realtimeSync: false,
381
+ batch: false,
382
+ maxBudgetChars: 32000,
383
+ connectionType: "mcp",
384
+ },
385
+ configSchema: {
386
+ mcpServerUrl: "URL of the MCP Remnic server",
387
+ },
388
+ homepage: "https://ampcode.com",
389
+ author: "Sourcegraph",
390
+ tags: ["official", "ai"],
391
+ },
392
+ {
393
+ id: "pi",
394
+ name: "Pi Coding Agent",
395
+ version: "1.0.0",
396
+ description: "Pi Coding Agent — native extension for recall, observe, MCP tools, and compaction coordination",
397
+ capabilities: {
398
+ observe: true,
399
+ recall: true,
400
+ store: true,
401
+ search: true,
402
+ entities: true,
403
+ realtimeSync: true,
404
+ batch: true,
405
+ maxBudgetChars: 32000,
406
+ connectionType: "http",
407
+ },
408
+ configSchema: {
409
+ remnicDaemonUrl: "URL of the Remnic daemon (default: http://127.0.0.1:4318)",
410
+ namespace: "Optional namespace",
411
+ installExtension: "Install the Pi extension into ~/.pi/agent/extensions/remnic (default: true)",
412
+ },
413
+ homepage: "https://pi.dev",
414
+ author: "Remnic",
415
+ tags: ["official", "ai", "pi", "coding-agent"],
416
+ requiresToken: true,
417
+ },
418
+ {
419
+ id: "replit",
420
+ name: "Replit Agent",
421
+ version: "1.0.0",
422
+ description: "Replit Agent — memory via HTTP API (reduced capabilities)",
423
+ capabilities: {
424
+ observe: true,
425
+ recall: true,
426
+ store: true,
427
+ search: false,
428
+ entities: false,
429
+ realtimeSync: false,
430
+ batch: false,
431
+ maxBudgetChars: 8000,
432
+ connectionType: "http",
433
+ },
434
+ configSchema: {
435
+ apiUrl: "URL of the Remnic HTTP API",
436
+ authToken: "Bearer token for authentication",
437
+ },
438
+ homepage: "https://replit.com",
439
+ author: "Replit",
440
+ tags: ["official", "cloud"],
441
+ requiresToken: true,
442
+ },
443
+ {
444
+ id: "generic-mcp",
445
+ name: "Generic MCP Client",
446
+ version: "1.0.0",
447
+ description: "Any MCP-compatible client — connect via standard MCP protocol",
448
+ capabilities: {
449
+ observe: true,
450
+ recall: true,
451
+ store: true,
452
+ search: true,
453
+ entities: true,
454
+ realtimeSync: true,
455
+ batch: true,
456
+ maxBudgetChars: 64000,
457
+ connectionType: "mcp",
458
+ },
459
+ configSchema: {
460
+ mcpServerUrl: "URL of the MCP Remnic server",
461
+ namespace: "Optional namespace",
462
+ authToken: "Bearer token for authentication",
463
+ },
464
+ homepage: "https://github.com/joshuaswarren/remnic",
465
+ author: "Remnic",
466
+ tags: ["generic", "mcp"],
467
+ requiresToken: true,
468
+ },
469
+ {
470
+ id: "weclone",
471
+ name: "WeClone Avatar",
472
+ version: "1.0.0",
473
+ description:
474
+ "Memory-aware OpenAI-compatible proxy for deployed WeClone avatars — " +
475
+ "injects Remnic recall into chat completions and buffers turns via observe",
476
+ capabilities: {
477
+ observe: true,
478
+ recall: true,
479
+ store: false,
480
+ search: false,
481
+ entities: false,
482
+ realtimeSync: false,
483
+ batch: false,
484
+ maxBudgetChars: 32000,
485
+ connectionType: "http",
486
+ },
487
+ configSchema: {
488
+ wecloneApiUrl:
489
+ "Base URL of the WeClone OpenAI-compatible API (e.g. http://localhost:8000/v1)",
490
+ proxyPort: "Local port where the memory proxy will listen (default 8100)",
491
+ remnicDaemonUrl:
492
+ "URL of the Remnic daemon exposing /engram/v1/recall and /engram/v1/observe",
493
+ sessionStrategy:
494
+ "Per-caller session mapping strategy: 'caller-id' | 'single'",
495
+ wecloneModelName: "Optional fine-tuned model name passed through to WeClone",
496
+ },
497
+ homepage: "https://github.com/xming521/weclone",
498
+ author: "Remnic",
499
+ tags: ["official", "ai", "weclone", "proxy"],
500
+ requiresToken: true,
501
+ },
502
+ {
503
+ id: "hermes",
504
+ name: "Hermes Agent",
505
+ version: "1.0.0",
506
+ description: "Hermes Agent MemoryProvider — automatic recall/observe on every turn via Python plugin protocol",
507
+ capabilities: {
508
+ observe: true,
509
+ recall: true,
510
+ store: true,
511
+ search: true,
512
+ entities: false,
513
+ realtimeSync: true,
514
+ batch: false,
515
+ maxBudgetChars: 32000,
516
+ connectionType: "http",
517
+ },
518
+ configSchema: {
519
+ host: "Remnic daemon host (default: 127.0.0.1)",
520
+ port: "Remnic daemon port (default: 4318)",
521
+ profile: "Hermes profile name (default: default)",
522
+ },
523
+ homepage: "https://github.com/joshuaswarren/remnic/tree/main/packages/plugin-hermes",
524
+ author: "Remnic",
525
+ tags: ["official", "python", "hermes"],
526
+ requiresToken: true,
527
+ },
528
+ ];
529
+
530
+ // ── Registry management ───────────────────────────────────────────────────
531
+
532
+ const REGISTRY_DIR_NAME = ".engram-connectors";
533
+
534
+ export function getRegistryPath(): string {
535
+ const xdgConfigHome = readEnvVar("XDG_CONFIG_HOME");
536
+ const configDir = xdgConfigHome
537
+ ? path.join(xdgConfigHome, "engram")
538
+ : path.join(resolveHomeDir(), ".config", "engram");
539
+ return path.join(configDir, REGISTRY_DIR_NAME, "registry.json");
540
+ }
541
+
542
+ export function loadRegistry(): ConnectorRegistry {
543
+ const regPath = getRegistryPath();
544
+
545
+ if (!fs.existsSync(regPath)) {
546
+ // First time — bootstrap with built-in connectors
547
+ const registry: ConnectorRegistry = {
548
+ connectors: BUILTIN_CONNECTORS,
549
+ registryPath: regPath,
550
+ };
551
+ saveRegistry(registry);
552
+ return registry;
553
+ }
554
+
555
+ const raw = fs.readFileSync(regPath, "utf8");
556
+ try {
557
+ const parsed = JSON.parse(raw);
558
+ // Built-ins always take precedence over persisted entries with the same ID.
559
+ // This ensures that upgraded manifests (e.g. newly-added requiresToken: true)
560
+ // are never shadowed by stale registry.json entries from an older version.
561
+ // Only connectors whose IDs are NOT in BUILTIN_CONNECTORS are preserved from
562
+ // the persisted file — those are genuine user-added custom connectors.
563
+ const builtinIds = new Set(BUILTIN_CONNECTORS.map((b) => b.id));
564
+ const customOnly = (parsed.connectors ?? []).filter((c: ConnectorManifest) => !builtinIds.has(c.id));
565
+ const merged = [...BUILTIN_CONNECTORS, ...customOnly];
566
+ return {
567
+ connectors: merged,
568
+ registryPath: regPath,
569
+ };
570
+ } catch {
571
+ const registry: ConnectorRegistry = {
572
+ connectors: BUILTIN_CONNECTORS,
573
+ registryPath: regPath,
574
+ };
575
+ saveRegistry(registry);
576
+ return registry;
577
+ }
578
+ }
579
+
580
+ export function saveRegistry(registry: ConnectorRegistry): void {
581
+ const regPath = registry.registryPath;
582
+ fs.mkdirSync(path.dirname(regPath), { recursive: true });
583
+ fs.writeFileSync(regPath, JSON.stringify({ connectors: registry.connectors }, null, 2));
584
+ }
585
+
586
+ // ── List connectors ────────────────────────────────────────────────────────
587
+
588
+ export function listConnectors(): {
589
+ installed: ConnectorInstance[];
590
+ available: ConnectorManifest[];
591
+ } {
592
+ const registry = loadRegistry();
593
+ const connectorsDir = getConnectorsDir();
594
+ const installedIds = new Set<string>();
595
+
596
+ // Find installed connectors
597
+ if (fs.existsSync(connectorsDir)) {
598
+ for (const entry of fs.readdirSync(connectorsDir)) {
599
+ if (entry.endsWith(".json")) {
600
+ try {
601
+ const config = JSON.parse(
602
+ fs.readFileSync(path.join(connectorsDir, entry), "utf8"),
603
+ );
604
+ installedIds.add(config.connectorId as string);
605
+ } catch {
606
+ // ignore malformed configs
607
+ }
608
+ }
609
+ }
610
+ }
611
+
612
+ // Mark installed vs available
613
+ const available: ConnectorManifest[] = registry.connectors.map((manifest) => ({
614
+ ...manifest,
615
+ installed: installedIds.has(manifest.id),
616
+ }));
617
+
618
+ // Build installed list
619
+ const installed: ConnectorInstance[] = [];
620
+ for (const id of installedIds) {
621
+ const configPath = path.join(connectorsDir, `${id}.json`);
622
+ try {
623
+ const raw = JSON.parse(fs.readFileSync(configPath, "utf8")) as Record<string, unknown>;
624
+ // Codex P1 (PRRT_kwDORJXyws56U9U0): strip any legacy `token` field from
625
+ // the returned config so that `remnic connectors list --json` never prints
626
+ // a bearer token — tokens live only in tokens.json. This handles existing
627
+ // on-disk connector.json files written by older Remnic versions without
628
+ // rewriting user files.
629
+ const { token: _redacted, ...config } = raw;
630
+ installed.push({
631
+ connectorId: id,
632
+ config,
633
+ status: "installed",
634
+ installedAt: raw.installedAt as string | undefined,
635
+ });
636
+ } catch {
637
+ // ignore
638
+ }
639
+ }
640
+
641
+ return { installed, available };
642
+ }
643
+
644
+ // ── Get connector token ────────────────────────────────────────────────────
645
+ // Codex P1 (PRRT_kwDORJXyws56U9U0): tokens are stored exclusively in
646
+ // tokens.json. This helper is the canonical way to retrieve the bearer token
647
+ // for a connector — connector.json never contains it.
648
+
649
+ export function getConnectorToken(connectorId: string): string | undefined {
650
+ try {
651
+ return loadTokenStore().tokens.find((t) => t.connector === connectorId)?.token;
652
+ } catch {
653
+ return undefined;
654
+ }
655
+ }
656
+
657
+ function readSavedConnectorConfig(configPath: string): Record<string, unknown> {
658
+ if (!fs.existsSync(configPath)) return {};
659
+ try {
660
+ const parsed = JSON.parse(fs.readFileSync(configPath, "utf8"));
661
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
662
+ const {
663
+ connectorId: _connectorId,
664
+ installedAt: _installedAt,
665
+ token: _token,
666
+ ...config
667
+ } = parsed as Record<string, unknown>;
668
+ return config;
669
+ } catch {
670
+ return {};
671
+ }
672
+ }
673
+
674
+ function removeClearedSavedConnectorConfig(
675
+ savedConnectorConfig: Record<string, unknown>,
676
+ rawUserConfig: Record<string, unknown>,
677
+ ): Record<string, unknown> {
678
+ const merged: Record<string, unknown> = { ...savedConnectorConfig };
679
+ for (const [key, value] of Object.entries(rawUserConfig)) {
680
+ if (value === undefined || value === null || value === "") {
681
+ delete merged[key];
682
+ }
683
+ }
684
+ return merged;
685
+ }
686
+
687
+ function compactConnectorConfigOverrides(rawUserConfig: Record<string, unknown>): Record<string, unknown> {
688
+ const safeUserConfig: Record<string, unknown> = {};
689
+ for (const [key, value] of Object.entries(rawUserConfig)) {
690
+ if (value === undefined || value === null || value === "") continue;
691
+ safeUserConfig[key] = value;
692
+ }
693
+ return safeUserConfig;
694
+ }
695
+
696
+ // ── Install connector ───────────────────────────────────────────────────────
697
+
698
+ export function installConnector(options: InstallOptions): InstallResult {
699
+ const registry = loadRegistry();
700
+ const manifest = registry.connectors.find((c) => c.id === options.connectorId);
701
+
702
+ if (!manifest) {
703
+ return {
704
+ connectorId: options.connectorId,
705
+ status: "error",
706
+ message: `Unknown connector: ${options.connectorId}`,
707
+ };
708
+ }
709
+
710
+ // Check if already installed
711
+ const existing = listConnectors().installed.find(
712
+ (c) => c.connectorId === options.connectorId,
713
+ );
714
+
715
+ if (existing && !options.force) {
716
+ return {
717
+ connectorId: options.connectorId,
718
+ status: "already_installed",
719
+ message: "Already installed. Use --force to reinstall.",
720
+ };
721
+ }
722
+
723
+ // Write config
724
+ const configDir = getConnectorsDir();
725
+ fs.mkdirSync(configDir, { recursive: true });
726
+
727
+ const configPath = path.join(configDir, `${options.connectorId}.json`);
728
+ const savedConnectorConfig = existing ? readSavedConnectorConfig(configPath) : {};
729
+
730
+ // For the hermes connector, resolve profile/host/port with the following
731
+ // precedence: saved-connector-JSON → explicit options.config → defaults.
732
+ // Reading happens BEFORE we overwrite the connector JSON so that a
733
+ // force-reinstall without re-supplied --config options preserves the
734
+ // previously configured values and writes the new token to the correct
735
+ // Hermes profile rather than resetting to "default"/127.0.0.1/4318.
736
+ //
737
+ // Issue C fix: sanitizer calls during options resolution are wrapped in
738
+ // try-catch so that invalid user-supplied values (e.g. --config port=abc)
739
+ // return a clean failed InstallResult instead of throwing.
740
+ let hermesSavedProfile: string | undefined;
741
+ let hermesSavedHost: string | undefined;
742
+ let hermesSavedPort: number | undefined;
743
+ // Resolved values (used both in resolvedConfig and in the YAML update below)
744
+ let hermesResolvedProfile: string | undefined;
745
+ let hermesResolvedHost: string | undefined;
746
+ let hermesResolvedPort: number | undefined;
747
+ if (options.connectorId === "hermes") {
748
+ if (fs.existsSync(configPath)) {
749
+ try {
750
+ const prev = JSON.parse(fs.readFileSync(configPath, "utf8"));
751
+ // Fix 2: coerce saved values through sanitizers so that CLI-written
752
+ // string ports ("5555") are accepted just like number ports (5555).
753
+ // Pass each through its sanitizer and fall back to undefined on error
754
+ // so a corrupt saved value doesn't prevent install from defaulting.
755
+ if (prev?.profile != null) {
756
+ try {
757
+ hermesSavedProfile = sanitizeHermesProfile(String(prev.profile));
758
+ } catch {
759
+ // Invalid saved profile — fall through to default
760
+ }
761
+ }
762
+ if (prev?.host != null) {
763
+ try {
764
+ hermesSavedHost = sanitizeHermesHost(String(prev.host));
765
+ } catch {
766
+ // Invalid saved host — fall through to default
767
+ }
768
+ }
769
+ if (prev?.port != null) {
770
+ try {
771
+ const coercedPort = Number(String(prev.port));
772
+ hermesSavedPort = sanitizeHermesPort(coercedPort);
773
+ } catch {
774
+ // Invalid saved port — fall through to default
775
+ }
776
+ }
777
+ } catch {
778
+ // Could not read existing config — fall through to defaults
779
+ }
780
+ }
781
+ // Use saved/default values here; user-supplied profile/host are validated
782
+ // and applied in the sanitization block below (single point of validation).
783
+ hermesResolvedProfile = hermesSavedProfile ?? "default";
784
+ hermesResolvedHost = hermesSavedHost ?? "127.0.0.1";
785
+
786
+ // Issue C: wrap sanitizeHermesPort (and profile/host) in try-catch so
787
+ // that invalid user-supplied values return a clean error result.
788
+ if (options.config?.port !== undefined) {
789
+ try {
790
+ hermesResolvedPort = sanitizeHermesPort(Number(String(options.config.port)));
791
+ } catch (err) {
792
+ return {
793
+ connectorId: options.connectorId,
794
+ status: "error",
795
+ message: `Invalid Hermes config: ${err instanceof Error ? err.message : String(err)}`,
796
+ };
797
+ }
798
+ }
799
+ if (hermesResolvedPort === undefined) {
800
+ hermesResolvedPort = hermesSavedPort ?? 4318;
801
+ }
802
+
803
+ // Also validate user-supplied profile and host up-front (Issue C coverage).
804
+ if (options.config?.profile !== undefined) {
805
+ try {
806
+ hermesResolvedProfile = sanitizeHermesProfile(String(options.config.profile));
807
+ } catch (err) {
808
+ return {
809
+ connectorId: options.connectorId,
810
+ status: "error",
811
+ message: `Invalid Hermes config: ${err instanceof Error ? err.message : String(err)}`,
812
+ };
813
+ }
814
+ }
815
+ if (options.config?.host !== undefined) {
816
+ try {
817
+ hermesResolvedHost = sanitizeHermesHost(String(options.config.host));
818
+ } catch (err) {
819
+ return {
820
+ connectorId: options.connectorId,
821
+ status: "error",
822
+ message: `Invalid Hermes config: ${err instanceof Error ? err.message : String(err)}`,
823
+ };
824
+ }
825
+ }
826
+ }
827
+
828
+ // Generate a per-connector auth token so the daemon can authenticate
829
+ // requests from this connector.
830
+ //
831
+ // Security gate (PRRT_kwDORJXyws56U9U0 round 6): tokens are ONLY generated
832
+ // and persisted to tokens.json for connectors that actually require bearer-token
833
+ // auth (manifest.requiresToken === true). MCP, CLI, embedded, and SDK connectors
834
+ // never need a token entry on disk — generating one unconditionally materialized
835
+ // credentials for connectors that have no auth requirement, a security regression.
836
+ //
837
+ // For hermes (Issue B fix): use buildTokenEntry() to generate a candidate
838
+ // token WITHOUT immediately persisting it to tokens.json. We commit the
839
+ // candidate to the store only AFTER upsertHermesConfig succeeds, so that
840
+ // a failed or skipped config.yaml write never leaves the daemon with a
841
+ // revoked token and no valid replacement written.
842
+ //
843
+ // For other connectors that requiresToken: generateToken() is idempotent —
844
+ // it filters the old entry and writes a fresh one atomically, so force-reinstall
845
+ // produces a new token automatically.
846
+ //
847
+ // Token write errors (e.g. read-only HOME with writable XDG_CONFIG_HOME)
848
+ // are non-fatal: we degrade gracefully and proceed with the connector
849
+ // config write rather than aborting the whole install.
850
+ //
851
+ // For non-Hermes connectors that requiresToken: snapshot the FULL token store
852
+ // BEFORE generateToken() so that if the connector JSON write later fails, we
853
+ // can restore the store to its pre-install state (UXJG fix — non-Hermes atomic
854
+ // rollback). Using a full-store snapshot (not a single-entry snapshot) ensures
855
+ // that a partial write of tokens.json during generateToken can be unwound
856
+ // atomically, covering both fresh-install and force-reinstall cases uniformly.
857
+ const nonHermesPriorTokenStore = (options.connectorId !== "hermes" && manifest.requiresToken)
858
+ ? loadTokenStore()
859
+ : null;
860
+
861
+ let tokenEntry: ReturnType<typeof generateToken> | null = null;
862
+ if (options.connectorId === "hermes") {
863
+ // Build a candidate token; do NOT save yet (Issue B).
864
+ try {
865
+ tokenEntry = buildTokenEntry(options.connectorId);
866
+ } catch {
867
+ // Non-fatal: fall through with tokenEntry === null.
868
+ }
869
+ } else if (manifest.requiresToken) {
870
+ // Only generate and persist a token entry for connectors that need token auth.
871
+ try {
872
+ tokenEntry = generateToken(options.connectorId);
873
+ } catch {
874
+ // Non-fatal: token store unavailable. Connector config will still be
875
+ // written; user can run `remnic token generate <id>` to create the token.
876
+ //
877
+ // Roll back the snapshot so that a partial write of tokens.json during
878
+ // generateToken (e.g. ENOSPC/EIO mid-write) does not leave other
879
+ // connectors' auth state corrupted. Best-effort: if the restore itself
880
+ // fails there is nothing more we can do here, but the error is swallowed
881
+ // so install continues in the same degraded (tokenEntry === null) path
882
+ // as before (PRRT_kwDORJXyws56UleN fix).
883
+ if (nonHermesPriorTokenStore !== null) {
884
+ try {
885
+ saveTokenStore(nonHermesPriorTokenStore);
886
+ } catch {
887
+ // Best-effort: snapshot restore failed; caller sees degraded install.
888
+ }
889
+ }
890
+ }
891
+ }
892
+ // else: connector does not require token auth — tokenEntry stays null and
893
+ // tokens.json is never touched for this connector.
894
+
895
+ // Thread 2 (PRRT_kwDORJXyws56VYwM): if the connector requires token auth but
896
+ // generateToken threw (tokenEntry is still null), abort now instead of
897
+ // continuing with a broken install that returns "success" without a valid token.
898
+ if (options.connectorId !== "hermes" && manifest.requiresToken && tokenEntry === null) {
899
+ return {
900
+ connectorId: options.connectorId,
901
+ status: "error",
902
+ message:
903
+ `${manifest.name} install aborted: token generation failed. ` +
904
+ `Run \`remnic token generate ${options.connectorId}\` to create the token, then reinstall.`,
905
+ };
906
+ }
907
+
908
+ // Build config from saved values + user overrides.
909
+ // Codex P1 (PRRT_kwDORJXyws56U9U0): tokens MUST NOT be written into
910
+ // connector.json. The authoritative store is tokens.json (0o600). Writing the
911
+ // token here created a second, unredacted copy that `remnic connectors list
912
+ // --json` printed verbatim, leaking live bearer tokens into shell history, CI
913
+ // logs, and telemetry. Callers needing the token for a specific connector
914
+ // must use loadTokenStore() and find the entry by connectorId directly.
915
+ //
916
+ // For hermes, include the resolved profile/host/port so that future
917
+ // force-reinstalls can read them back even if options.config is not supplied.
918
+ //
919
+ // Strip any stray `token` key the caller may have supplied via options.config
920
+ // so it cannot be persisted to disk even on legacy call paths.
921
+ const { token: _callerToken, ...rawUserConfig } = (options.config ?? {}) as Record<string, unknown>;
922
+ const savedConnectorConfigForMerge = removeClearedSavedConnectorConfig(savedConnectorConfig, rawUserConfig);
923
+ const safeUserConfig = compactConnectorConfigOverrides(rawUserConfig);
924
+ const resolvedConfig: Record<string, unknown> = {
925
+ ...savedConnectorConfigForMerge,
926
+ ...safeUserConfig,
927
+ connectorId: options.connectorId,
928
+ installedAt: new Date().toISOString(),
929
+ // For hermes, always overlay the sanitized/coerced resolved values so that
930
+ // the connector JSON always has a numeric port and validated profile/host.
931
+ // This also ensures options.config string values (from --config=port=5555)
932
+ // are replaced with their sanitized numeric equivalents (Fix 2 root cause).
933
+ ...(hermesResolvedProfile !== undefined ? {
934
+ profile: hermesResolvedProfile,
935
+ host: hermesResolvedHost,
936
+ port: hermesResolvedPort,
937
+ } : {}),
938
+ };
939
+
940
+ // ── Hermes atomic install flow ─────────────────────────────────────────────
941
+ // The Hermes install sequence must be atomic: connector.json must only be
942
+ // written if and only if both the YAML write AND the token-store commit
943
+ // succeed. Partial failures must leave the prior state intact so the daemon
944
+ // keeps working with the old token.
945
+ //
946
+ // Step order (all-or-nothing):
947
+ // a. Generate token candidate (buildTokenEntry, no store write yet).
948
+ // b. Validate profile (fail-fast).
949
+ // c. Write config.yaml via upsertHermesConfig — if skipped (missing dir)
950
+ // or throws, abort with status "error". Old token is NOT revoked.
951
+ // d. Commit new token to tokens.json — if this throws, rollback the YAML
952
+ // write (restore prior content or delete new file) and abort.
953
+ // e. Write connector.json only after both (c) and (d) succeed.
954
+ // f. Health check — gated on committed === true && tokenEntry != null.
955
+ //
956
+ // Non-Hermes connectors: connector.json is written immediately (no YAML
957
+ // dependency) and the health check is not performed.
958
+
959
+ if (options.connectorId === "hermes") {
960
+ // hermesResolvedProfile/Host/Port were computed above using the correct
961
+ // precedence (saved JSON → explicit options.config → defaults).
962
+ const rawProfile = hermesResolvedProfile!;
963
+ const hermesHost = hermesResolvedHost!;
964
+ const hermesPort = hermesResolvedPort!;
965
+
966
+ // (b) Validate profile name — fail-fast before touching any files.
967
+ let hermesProfile: string;
968
+ try {
969
+ hermesProfile = sanitizeHermesProfile(rawProfile);
970
+ } catch (err) {
971
+ return {
972
+ connectorId: options.connectorId,
973
+ status: "error",
974
+ message: `Hermes install aborted: ${err instanceof Error ? err.message : String(err)}`,
975
+ };
976
+ }
977
+
978
+ // Token generation is required for an atomic Hermes install.
979
+ if (!tokenEntry) {
980
+ return {
981
+ connectorId: options.connectorId,
982
+ status: "error",
983
+ message:
984
+ "Hermes install aborted: token store unavailable. " +
985
+ "Run `remnic token generate hermes` then reinstall to complete setup.",
986
+ };
987
+ }
988
+
989
+ // (c) Write config.yaml. If the profile dir does not exist (skipped) or
990
+ // the write throws, abort WITHOUT committing the token or writing connector.json.
991
+ let yamlResult: HermesConfigResult;
992
+ try {
993
+ yamlResult = upsertHermesConfig({
994
+ profile: hermesProfile,
995
+ host: hermesHost,
996
+ port: hermesPort,
997
+ token: tokenEntry.token,
998
+ });
999
+ } catch (err) {
1000
+ // upsertHermesConfig threw — old token preserved, connector.json unchanged.
1001
+ return {
1002
+ connectorId: options.connectorId,
1003
+ status: "error",
1004
+ message: `Hermes install aborted: config.yaml write failed — ${err instanceof Error ? err.message : String(err)}`,
1005
+ };
1006
+ }
1007
+
1008
+ if (!yamlResult.updated) {
1009
+ // Skipped (profile dir missing) — abort so connector.json is NOT written
1010
+ // with a token the daemon won't recognize (the profile doesn't exist).
1011
+ // Preserves any prior Hermes profile/connector.json untouched.
1012
+ return {
1013
+ connectorId: options.connectorId,
1014
+ status: "error",
1015
+ message: `Hermes install aborted: ${yamlResult.reason ?? "config.yaml not written"}. ` +
1016
+ `Create the Hermes profile directory first, then reinstall.`,
1017
+ };
1018
+ }
1019
+
1020
+ // (d) Commit token to tokens.json. If this fails, roll back the YAML write
1021
+ // and abort — the old token must remain valid and connector.json must stay
1022
+ // unchanged so the daemon keeps working.
1023
+ //
1024
+ // IMPORTANT (UXJI/UXJT): Snapshot the FULL token store BEFORE calling
1025
+ // commitTokenEntry(). A single-entry approach (capturing the return value
1026
+ // of commitTokenEntry) is insufficient: if commitTokenEntry throws mid-write
1027
+ // (e.g. ENOSPC truncating tokens.json), the assignment never completes and
1028
+ // the rollback becomes a no-op, leaving tokens.json potentially corrupt.
1029
+ // The full-store snapshot, captured before the write attempt, is guaranteed
1030
+ // clean and can be written back atomically by saveTokenStore.
1031
+ const priorTokenStore = loadTokenStore();
1032
+ let committed = false;
1033
+ try {
1034
+ commitTokenEntry(tokenEntry);
1035
+ committed = true;
1036
+ } catch (commitErr) {
1037
+ // Roll back the token store: restore the full snapshot so a partial write
1038
+ // (e.g. ENOSPC truncating tokens.json mid-write) cannot leave the store
1039
+ // corrupt or missing the prior hermes entry.
1040
+ let tokensRolledBack = true;
1041
+ let tokensRollbackErrMsg = "";
1042
+ try {
1043
+ saveTokenStore(priorTokenStore);
1044
+ } catch (tokenRestoreErr) {
1045
+ tokensRolledBack = false;
1046
+ tokensRollbackErrMsg = tokenRestoreErr instanceof Error ? tokenRestoreErr.message : String(tokenRestoreErr);
1047
+ }
1048
+ // Roll back the YAML write: restore prior content (or delete newly-created file).
1049
+ let yamlRolledBack = true;
1050
+ let yamlRollbackErrMsg = "";
1051
+ try {
1052
+ if (yamlResult.priorContent === null) {
1053
+ // File was created new — remove it entirely.
1054
+ fs.unlinkSync(yamlResult.configPath);
1055
+ } else if (typeof yamlResult.priorContent === "string") {
1056
+ // File existed before — restore original content.
1057
+ writeSecretFileSync(yamlResult.configPath, yamlResult.priorContent);
1058
+ }
1059
+ } catch (yamlRestoreErr) {
1060
+ yamlRolledBack = false;
1061
+ yamlRollbackErrMsg = yamlRestoreErr instanceof Error ? yamlRestoreErr.message : String(yamlRestoreErr);
1062
+ }
1063
+ // Build an error message that accurately reflects which rollbacks succeeded.
1064
+ const commitErrMsg = commitErr instanceof Error ? commitErr.message : String(commitErr);
1065
+ let message: string;
1066
+ if (tokensRolledBack && yamlRolledBack) {
1067
+ message =
1068
+ `Hermes install failed during token commit — ` +
1069
+ `${commitErrMsg}. ` +
1070
+ `config.yaml and tokens.json restored to prior state. ` +
1071
+ `Resolve the tokens.json access issue, then reinstall.`;
1072
+ } else if (!yamlRolledBack && tokensRolledBack) {
1073
+ message =
1074
+ `Hermes install failed during token commit — ` +
1075
+ `${commitErrMsg}. ` +
1076
+ `tokens.json restored but config.yaml rollback ALSO failed ` +
1077
+ `(${yamlRollbackErrMsg}). ` +
1078
+ `Hermes daemon may be in an inconsistent state: config references a stale token. ` +
1079
+ `Manually inspect ${yamlResult.configPath} and reinstall.`;
1080
+ } else if (yamlRolledBack && !tokensRolledBack) {
1081
+ message =
1082
+ `Hermes install failed during token commit — ` +
1083
+ `${commitErrMsg}. ` +
1084
+ `config.yaml restored but tokens.json rollback ALSO failed ` +
1085
+ `(${tokensRollbackErrMsg}). ` +
1086
+ `Hermes daemon may be in an inconsistent state: tokens.json is corrupt or incomplete. ` +
1087
+ `Manually inspect ~/.remnic/tokens.json and reinstall.`;
1088
+ } else {
1089
+ message =
1090
+ `Hermes install failed during token commit — ` +
1091
+ `${commitErrMsg}. ` +
1092
+ `BOTH rollbacks failed: config.yaml rollback failed (${yamlRollbackErrMsg}); ` +
1093
+ `tokens.json rollback failed (${tokensRollbackErrMsg}). ` +
1094
+ `Hermes daemon is likely in an inconsistent state. ` +
1095
+ `Manually inspect ${yamlResult.configPath} ` +
1096
+ `and ~/.remnic/tokens.json, then reinstall.`;
1097
+ }
1098
+ return {
1099
+ connectorId: options.connectorId,
1100
+ status: "error",
1101
+ message,
1102
+ };
1103
+ }
1104
+
1105
+ // (e) Both YAML write and token commit succeeded — now attempt to write connector.json.
1106
+ // If this write fails (e.g. connectors dir is not writable), roll back Phase D (token
1107
+ // commit) and Phase C (YAML upsert) so no partial-install state is left behind.
1108
+ // We restore the full token store snapshot captured before Phase D so that
1109
+ // tokens.json is guaranteed consistent with the rolled-back config.yaml.
1110
+ try {
1111
+ writeSecretFileSync(configPath, JSON.stringify(resolvedConfig, null, 2));
1112
+ } catch (writeErr) {
1113
+ // Roll back Phase D: restore the full token store snapshot so tokens.json
1114
+ // is consistent with the rolled-back config.yaml.
1115
+ let tokenRollbackFailed = false;
1116
+ let tokenRollbackMsg = "token store restored to pre-install snapshot";
1117
+ try {
1118
+ saveTokenStore(priorTokenStore);
1119
+ } catch (tokenRestoreErr) {
1120
+ tokenRollbackFailed = true;
1121
+ tokenRollbackMsg = `token rollback failed: ${tokenRestoreErr instanceof Error ? tokenRestoreErr.message : String(tokenRestoreErr)}`;
1122
+ }
1123
+ // Roll back Phase C: restore config.yaml to its prior content.
1124
+ let yamlRollbackMsg = "config.yaml restored";
1125
+ try {
1126
+ if (yamlResult.priorContent === null) {
1127
+ // File was created new — delete it. Track whether the unlink actually
1128
+ // succeeded so we report honestly rather than claiming removal when it
1129
+ // silently failed inside the inner catch.
1130
+ let unlinkSucceeded = false;
1131
+ let unlinkErr: unknown;
1132
+ try {
1133
+ fs.unlinkSync(yamlResult.configPath);
1134
+ unlinkSucceeded = true;
1135
+ } catch (err) {
1136
+ unlinkErr = err;
1137
+ }
1138
+ if (unlinkSucceeded) {
1139
+ yamlRollbackMsg = "config.yaml removed (was newly created)";
1140
+ } else {
1141
+ const unlinkMsg = unlinkErr instanceof Error ? unlinkErr.message : String(unlinkErr);
1142
+ yamlRollbackMsg = `config.yaml rollback failed: could not remove newly-created file — ${unlinkMsg}`;
1143
+ }
1144
+ } else if (typeof yamlResult.priorContent === "string") {
1145
+ writeSecretFileSync(yamlResult.configPath, yamlResult.priorContent);
1146
+ yamlRollbackMsg = "config.yaml restored to prior content";
1147
+ }
1148
+ } catch (yamlRollbackErr) {
1149
+ yamlRollbackMsg = `config.yaml rollback failed: ${yamlRollbackErr instanceof Error ? yamlRollbackErr.message : String(yamlRollbackErr)}`;
1150
+ }
1151
+ const urgentSuffix = tokenRollbackFailed
1152
+ ? ` tokens.json may be in an inconsistent state — manually restore hermes token with 'remnic token generate hermes'.`
1153
+ : "";
1154
+ return {
1155
+ connectorId: options.connectorId,
1156
+ status: "error",
1157
+ message:
1158
+ `Hermes install aborted: connector config write failed — ` +
1159
+ `connector directory may not be writable. ` +
1160
+ `Rollback: ${tokenRollbackMsg}; ${yamlRollbackMsg}.` +
1161
+ `${urgentSuffix} Resolve the permission issue, then reinstall.`,
1162
+ };
1163
+ }
1164
+
1165
+ const notes: string[] = [];
1166
+ notes.push(`Updated Hermes config: ${yamlResult.configPath}`);
1167
+
1168
+ // If a migrated default-profile install now writes to Hermes' root config,
1169
+ // remove stale Remnic credentials from the legacy default profile file too.
1170
+ if (hermesProfile === "default") {
1171
+ const legacyDefaultConfigPath = hermesDefaultProfileConfigPath();
1172
+ if (!sameHermesConfigTarget(yamlResult.configPath, legacyDefaultConfigPath)) {
1173
+ try {
1174
+ const legacyDefaultCleanResult = removeHermesConfigFile(legacyDefaultConfigPath);
1175
+ if (legacyDefaultCleanResult.updated) {
1176
+ notes.push(`Cleaned stale remnic: block from legacy default profile: ${legacyDefaultConfigPath}`);
1177
+ }
1178
+ } catch {
1179
+ notes.push("Note: could not clean stale remnic: block from legacy default profile");
1180
+ }
1181
+ }
1182
+ }
1183
+
1184
+ // Clean up the old profile's remnic: block if the profile changed.
1185
+ // Compare resolved config paths (not raw strings) so that case-insensitive
1186
+ // filesystems (macOS default) don't treat "Research" and "research" as
1187
+ // different profiles — resolving both would yield the same config.yaml,
1188
+ // and removing it would strip the block we just wrote (PRRT_kwDORJXyws56VQ76).
1189
+ let oldProfileResolvesToDifferentFile = false;
1190
+ if (hermesSavedProfile !== undefined) {
1191
+ try {
1192
+ oldProfileResolvesToDifferentFile =
1193
+ !sameHermesConfigTarget(hermesConfigPath(hermesSavedProfile), hermesConfigPath(hermesProfile));
1194
+ } catch {
1195
+ // If either profile fails sanitization the comparison is moot; skip cleanup.
1196
+ oldProfileResolvesToDifferentFile = false;
1197
+ }
1198
+ }
1199
+ if (oldProfileResolvesToDifferentFile) {
1200
+ try {
1201
+ const oldCleanResult = removeHermesConfig({ profile: hermesSavedProfile! });
1202
+ if (oldCleanResult.updated) {
1203
+ notes.push(`Cleaned stale remnic: block from previous profile: ${oldCleanResult.configPath}`);
1204
+ }
1205
+ } catch {
1206
+ // Non-fatal: if we can't clean the old profile, log a note but don't fail.
1207
+ notes.push(`Note: could not clean stale remnic: block from previous profile "${hermesSavedProfile}"`);
1208
+ }
1209
+ }
1210
+
1211
+ // (f) Health check — only when the token was actually committed to the store.
1212
+ // Without commitment, the daemon won't recognise the token → 401 → 6s sleep
1213
+ // → false-negative "Daemon not reachable". committed is always true here
1214
+ // (we returned early on failure above) but the explicit guard is kept for
1215
+ // clarity and future robustness.
1216
+ if (committed && tokenEntry) {
1217
+ const daemonOk = checkDaemonHealth(hermesHost, hermesPort, tokenEntry.token);
1218
+ if (daemonOk) {
1219
+ notes.push("Daemon health check: OK");
1220
+ } else {
1221
+ notes.push(
1222
+ `Daemon not reachable at ${hermesHost}:${hermesPort} — start with: remnic daemon start`,
1223
+ );
1224
+ }
1225
+ }
1226
+
1227
+ const suffix = notes.length > 0 ? `\n ${notes.join("\n ")}` : "";
1228
+ return {
1229
+ connectorId: options.connectorId,
1230
+ status: "installed",
1231
+ configPath,
1232
+ message: `Installed ${manifest.name} v${manifest.version}${suffix}`,
1233
+ };
1234
+ }
1235
+
1236
+ // ── Non-Hermes connectors: write connector.json ───────────────────────────
1237
+ // Write with owner-only permissions because the JSON may embed the
1238
+ // connector bearer token. Matches the 0o600 hardening on
1239
+ // ~/.remnic/tokens.json so the token is never world-readable via this
1240
+ // secondary location.
1241
+
1242
+ // Codex CLI: also drop the phase-2 memory extension unless the caller
1243
+ // explicitly opted out via `config.installExtension: false`.
1244
+ let extensionMessage = "";
1245
+ // Explicit structured flag for the config-write rollback gate. This MUST
1246
+ // stay decoupled from `extensionMessage` because that string embeds the
1247
+ // install path — substring-matching on "skipped" would misfire whenever
1248
+ // the codex home happens to contain the word "skipped".
1249
+ let extensionInstalled = false;
1250
+ // Holds the commit/rollback handle returned by installCodexMemoryExtension().
1251
+ // The backup of any prior extension is kept alive until commit() is called.
1252
+ let extensionHandle: { commit(): void; rollback(): void } | null = null;
1253
+ if (options.connectorId === "codex-cli") {
1254
+ // Finding 1: coerce string "false"/"true" from CLI config parsing to a real
1255
+ // boolean before the gate check, then persist the coerced value so it is
1256
+ // stored as a boolean in the config file.
1257
+ const coerced = coerceInstallExtension(resolvedConfig.installExtension);
1258
+ if (coerced !== undefined) {
1259
+ resolvedConfig.installExtension = coerced;
1260
+ }
1261
+ const shouldInstall = resolvedConfig.installExtension !== false;
1262
+ // Persist the effective installExtension boolean explicitly so that
1263
+ // removeConnector's provenance check (Finding 3) can match. When the caller
1264
+ // did not pass the key, the default is true — write it so later removal
1265
+ // knows Remnic owned the install.
1266
+ resolvedConfig.installExtension = shouldInstall;
1267
+ // Resolve the Codex home path NOW so we can persist the absolute path
1268
+ // into the saved config. This guarantees removeConnector can target the
1269
+ // exact same directory later even if $CODEX_HOME is unset or changed.
1270
+ const codexHomeOverride =
1271
+ typeof resolvedConfig.codexHome === "string" && resolvedConfig.codexHome.length > 0
1272
+ ? (resolvedConfig.codexHome as string)
1273
+ : null;
1274
+ const resolvedCodexHome = resolveCodexHome(codexHomeOverride);
1275
+ resolvedConfig.codexHome = resolvedCodexHome;
1276
+
1277
+ if (shouldInstall) {
1278
+ try {
1279
+ const extensionSourceOverride =
1280
+ typeof resolvedConfig.extensionSourceDir === "string" &&
1281
+ resolvedConfig.extensionSourceDir.length > 0
1282
+ ? (resolvedConfig.extensionSourceDir as string)
1283
+ : null;
1284
+ const extResult = installCodexMemoryExtension({
1285
+ codexHome: resolvedCodexHome,
1286
+ sourceDir: extensionSourceOverride,
1287
+ });
1288
+ extensionMessage = ` (memory extension: ${extResult.remnicExtensionDir})`;
1289
+ extensionInstalled = true;
1290
+ extensionHandle = extResult;
1291
+ } catch (err) {
1292
+ const errMsg = err instanceof Error ? err.message : "unknown error";
1293
+ // Codex P2 (PRRT_kwDORJXyws56Ur_G): generateToken already rotated
1294
+ // tokens.json before reaching this point. The extension threw, so no
1295
+ // connector.json was written — roll back the token store to the
1296
+ // pre-install snapshot so tokens.json and the absent connector.json
1297
+ // stay consistent (no orphaned/active token without a matching config).
1298
+ //
1299
+ // Initialize to false: only set true once saveTokenStore() succeeds.
1300
+ // For connectors without requiresToken the rollback block is skipped
1301
+ // entirely, so the suffix must remain absent — not "Token has been
1302
+ // rolled back." (which would be factually incorrect).
1303
+ let extensionErrTokenRolledBack = false;
1304
+ let extensionErrTokenRollbackMsg = "";
1305
+ if (tokenEntry !== null && nonHermesPriorTokenStore !== null) {
1306
+ try {
1307
+ saveTokenStore(nonHermesPriorTokenStore);
1308
+ extensionErrTokenRolledBack = true;
1309
+ } catch (tokenRestoreErr) {
1310
+ extensionErrTokenRolledBack = false;
1311
+ extensionErrTokenRollbackMsg =
1312
+ tokenRestoreErr instanceof Error ? tokenRestoreErr.message : String(tokenRestoreErr);
1313
+ }
1314
+ }
1315
+ // Only include a token-rollback suffix for connectors that have a token
1316
+ // to roll back. Non-token connectors (requiresToken !== true) never
1317
+ // generated a token entry, so no rollback occurred and the message must
1318
+ // not claim otherwise.
1319
+ const tokenRollbackSuffix = manifest.requiresToken
1320
+ ? extensionErrTokenRolledBack
1321
+ ? " Token has been rolled back."
1322
+ : ` Token rollback FAILED (${extensionErrTokenRollbackMsg}) — tokens.json may contain an orphaned entry. ` +
1323
+ `Manually inspect ~/.remnic/tokens.json and reinstall.`
1324
+ : "";
1325
+ return {
1326
+ connectorId: options.connectorId,
1327
+ status: "error",
1328
+ message: `Memory extension install failed — ${errMsg}.${tokenRollbackSuffix} Resolve the issue, then reinstall.`,
1329
+ };
1330
+ }
1331
+ } else {
1332
+ extensionMessage = " (memory extension: skipped via installExtension=false)";
1333
+ }
1334
+ }
1335
+
1336
+ // ── WeClone: write proxy config to ~/.remnic/connectors/weclone.json ─────
1337
+ //
1338
+ // The standalone `remnic-weclone-proxy` CLI (see packages/connector-weclone)
1339
+ // reads its config from ~/.remnic/connectors/weclone.json by default so the
1340
+ // proxy can start without depending on Remnic's XDG-scoped registry layout.
1341
+ // Compose and write that file here, BEFORE the registry connector.json is
1342
+ // written, so that a failure in either file's write path rolls back cleanly.
1343
+ //
1344
+ // Precedence for each field: user-supplied via --config → saved prior proxy
1345
+ // config (on --force) → manifest defaults. The generated bearer token (if
1346
+ // any) is persisted into remnicAuthToken so the proxy can authenticate with
1347
+ // the daemon without a second token lookup at runtime.
1348
+ let weCloneProxyHandleRollback: (() => void) | null = null;
1349
+ if (options.connectorId === "weclone") {
1350
+ try {
1351
+ // Force-reinstall (and any reinstall path) must keep using the exact
1352
+ // proxy config path that was persisted on the previous install. If we
1353
+ // re-derive from the current env each time, a user whose REMNIC_HOME /
1354
+ // ENGRAM_HOME changed between installs would end up with two proxy
1355
+ // config files — the old one stays with stale settings + a revoked
1356
+ // token, the new one gets the live token, and any running proxy still
1357
+ // reading the old file starts failing auth. Read the saved
1358
+ // `proxyConfigPath` from the existing registry config first, and only
1359
+ // fall back to env-derivation for genuine first-time installs.
1360
+ let proxyConfigPath: string | null = null;
1361
+ if (existing && fs.existsSync(configPath)) {
1362
+ try {
1363
+ const savedRegistryConfig = JSON.parse(fs.readFileSync(configPath, "utf8")) as Record<string, unknown>;
1364
+ if (
1365
+ typeof savedRegistryConfig.proxyConfigPath === "string" &&
1366
+ savedRegistryConfig.proxyConfigPath.length > 0
1367
+ ) {
1368
+ proxyConfigPath = savedRegistryConfig.proxyConfigPath;
1369
+ }
1370
+ } catch {
1371
+ // Saved registry config unreadable — fall through to env resolution.
1372
+ }
1373
+ }
1374
+ if (proxyConfigPath === null) {
1375
+ proxyConfigPath = resolveWeCloneProxyConfigPath();
1376
+ }
1377
+ const prior = readWeCloneProxyConfigIfExists(proxyConfigPath);
1378
+ const proxyConfig = buildWeCloneProxyConfig({
1379
+ userConfig: safeUserConfig,
1380
+ priorConfig: prior ? safeParseJson(prior) : null,
1381
+ authToken: tokenEntry?.token,
1382
+ });
1383
+ fs.mkdirSync(path.dirname(proxyConfigPath), { recursive: true });
1384
+ // Install the rollback closure BEFORE the write starts. `writeSecretFileSync`
1385
+ // opens the file in truncate mode, so a mid-write failure (ENOSPC, EPERM)
1386
+ // could leave `weclone.json` empty. Creating the rollback now guarantees
1387
+ // we can always restore prior content (or delete a newly-created file)
1388
+ // even if the write itself throws.
1389
+ weCloneProxyHandleRollback = () => {
1390
+ try {
1391
+ if (prior === null) {
1392
+ // File was created (or would have been created) by this install —
1393
+ // delete whatever is left behind, if anything.
1394
+ if (fs.existsSync(proxyConfigPath)) {
1395
+ fs.unlinkSync(proxyConfigPath);
1396
+ }
1397
+ } else {
1398
+ writeSecretFileSync(proxyConfigPath, prior);
1399
+ }
1400
+ } catch {
1401
+ // Best-effort rollback.
1402
+ }
1403
+ };
1404
+ try {
1405
+ writeSecretFileSync(
1406
+ proxyConfigPath,
1407
+ JSON.stringify(proxyConfig, null, 2),
1408
+ );
1409
+ } catch (writeErr) {
1410
+ // Truncate-and-write failed partway through — restore the file (or
1411
+ // remove the empty partial) and re-throw so the outer catch drives
1412
+ // the structured error response + token rollback.
1413
+ try {
1414
+ weCloneProxyHandleRollback();
1415
+ } catch {
1416
+ // Best-effort.
1417
+ }
1418
+ weCloneProxyHandleRollback = null;
1419
+ throw writeErr;
1420
+ }
1421
+ // Record the proxy-side config path on the registry JSON so operators
1422
+ // and `remnic connectors doctor weclone` can locate it later. Persist the
1423
+ // effective proxy port so `remnic connectors list` reflects the resolved
1424
+ // value rather than whatever (possibly missing) the user supplied.
1425
+ resolvedConfig.proxyConfigPath = proxyConfigPath;
1426
+ resolvedConfig.proxyPort = proxyConfig.proxyPort;
1427
+ resolvedConfig.wecloneApiUrl = proxyConfig.wecloneApiUrl;
1428
+ resolvedConfig.remnicDaemonUrl = proxyConfig.remnicDaemonUrl;
1429
+ resolvedConfig.sessionStrategy = proxyConfig.sessionStrategy;
1430
+ } catch (weCloneErr) {
1431
+ // Track token rollback success/failure explicitly so the error message
1432
+ // can truthfully report whether tokens.json was restored or is in a
1433
+ // potentially-inconsistent state. Mirrors the care taken in the
1434
+ // registry-config-write failure handler below.
1435
+ let tokenRolledBack = false;
1436
+ let tokenRollbackMsg = "";
1437
+ if (tokenEntry !== null && nonHermesPriorTokenStore !== null) {
1438
+ try {
1439
+ saveTokenStore(nonHermesPriorTokenStore);
1440
+ tokenRolledBack = true;
1441
+ } catch (tokenRestoreErr) {
1442
+ tokenRolledBack = false;
1443
+ tokenRollbackMsg =
1444
+ tokenRestoreErr instanceof Error ? tokenRestoreErr.message : String(tokenRestoreErr);
1445
+ }
1446
+ }
1447
+ const tokenSuffix = manifest.requiresToken && tokenEntry !== null
1448
+ ? tokenRolledBack
1449
+ ? " Token has been rolled back."
1450
+ : ` Token rollback FAILED (${tokenRollbackMsg}) — tokens.json may contain an orphaned entry. ` +
1451
+ `Manually inspect ~/.remnic/tokens.json and reinstall.`
1452
+ : "";
1453
+ return {
1454
+ connectorId: options.connectorId,
1455
+ status: "error",
1456
+ message:
1457
+ `WeClone install aborted: proxy config write failed — ` +
1458
+ `${weCloneErr instanceof Error ? weCloneErr.message : String(weCloneErr)}.` +
1459
+ `${tokenSuffix} Resolve the write permission issue on ~/.remnic/connectors/, then reinstall.`,
1460
+ };
1461
+ }
1462
+ }
1463
+
1464
+ // Finding 5: strip internal/test-only keys that must never be persisted to
1465
+ // the config file. These keys are used at install time only (e.g. to inject
1466
+ // a synthetic extension source dir in tests) and have no meaning on disk.
1467
+ // Denylist — add any future test-only keys here with a comment.
1468
+ const INTERNAL_KEYS_DENYLIST = [
1469
+ "extensionSourceDir", // test-only override for the plugin-codex source path
1470
+ ];
1471
+ for (const key of INTERNAL_KEYS_DENYLIST) {
1472
+ delete resolvedConfig[key];
1473
+ }
1474
+
1475
+ // Atomic rollback (UXJG / Codex P1): if the JSON write fails (e.g., permission
1476
+ // denied on XDG_CONFIG_HOME), generateToken() above already rotated the token in
1477
+ // tokens.json. Roll back via the full-store snapshot captured before generateToken
1478
+ // so tokens.json and the absent connector.json stay consistent — no stale token
1479
+ // lingers without a matching config file. Full-store restore (vs. single-entry
1480
+ // restore/revoke) handles partial writes atomically for both fresh-install and
1481
+ // force-reinstall paths uniformly.
1482
+ //
1483
+ // Also roll back any codex-cli memory extension if the config write fails so
1484
+ // that no dangling memories_extensions/remnic directory is left with no config
1485
+ // provenance for removeConnector to find and clean up later.
1486
+ try {
1487
+ writeSecretFileSync(configPath, JSON.stringify(resolvedConfig, null, 2));
1488
+ } catch (writeErr) {
1489
+ // Roll back non-hermes token store if needed. Track success so we can
1490
+ // report accurately — unconditionally claiming rollback succeeded when it
1491
+ // silently failed would leave operators unable to diagnose inconsistent state.
1492
+ //
1493
+ // Initialize to false: only set true once saveTokenStore() succeeds.
1494
+ // Non-token connectors skip this block entirely, so we must not emit a
1495
+ // "Token has been rolled back." suffix for them.
1496
+ let configWriteTokenRolledBack = false;
1497
+ let configWriteTokenRollbackMsg = "";
1498
+ if (tokenEntry !== null && nonHermesPriorTokenStore !== null) {
1499
+ try {
1500
+ saveTokenStore(nonHermesPriorTokenStore);
1501
+ configWriteTokenRolledBack = true;
1502
+ } catch (tokenRestoreErr) {
1503
+ configWriteTokenRolledBack = false;
1504
+ configWriteTokenRollbackMsg =
1505
+ tokenRestoreErr instanceof Error ? tokenRestoreErr.message : String(tokenRestoreErr);
1506
+ }
1507
+ }
1508
+ // Roll back the codex-cli extension if it was installed.
1509
+ // Use extensionHandle.rollback() so that a pre-existing (possibly
1510
+ // customised) extension is restored from the backup kept by
1511
+ // installCodexMemoryExtension(), rather than unconditionally deleted.
1512
+ if (extensionInstalled && extensionHandle !== null) {
1513
+ try {
1514
+ extensionHandle.rollback();
1515
+ } catch {
1516
+ // Best-effort rollback: log but don't mask the original write error.
1517
+ console.warn(
1518
+ "[remnic/connectors] installConnector: config write failed and extension rollback also failed — " +
1519
+ "manual cleanup of memories_extensions/remnic may be required.",
1520
+ );
1521
+ }
1522
+ }
1523
+ // Roll back the WeClone proxy config if it was written.
1524
+ if (weCloneProxyHandleRollback !== null) {
1525
+ try {
1526
+ weCloneProxyHandleRollback();
1527
+ } catch {
1528
+ // Best-effort rollback.
1529
+ }
1530
+ }
1531
+ // Only include a token-rollback suffix for connectors that actually had a
1532
+ // token to roll back. Non-token connectors (requiresToken !== true) never
1533
+ // generated a token entry. For requiresToken connectors where generateToken
1534
+ // threw (tokenEntry === null), no token was written to tokens.json so no
1535
+ // rollback occurred — avoid a misleading "Token rollback FAILED" message
1536
+ // (Thread 1, PRRT_kwDORJXyws56VVnB).
1537
+ const configWriteTokenSuffix = manifest.requiresToken && tokenEntry !== null
1538
+ ? configWriteTokenRolledBack
1539
+ ? " Token has been rolled back."
1540
+ : ` Token rollback FAILED (${configWriteTokenRollbackMsg}) — tokens.json may contain an orphaned entry. ` +
1541
+ `Manually inspect ~/.remnic/tokens.json and reinstall.`
1542
+ : "";
1543
+ return {
1544
+ connectorId: options.connectorId,
1545
+ status: "error",
1546
+ message:
1547
+ `${manifest.name} install aborted: connector config write failed — ` +
1548
+ `${writeErr instanceof Error ? writeErr.message : String(writeErr)}.` +
1549
+ `${configWriteTokenSuffix} Resolve the write permission issue, then reinstall.`,
1550
+ };
1551
+ }
1552
+
1553
+ // Config write succeeded — permanently drop the backup of the prior extension.
1554
+ if (extensionInstalled && extensionHandle !== null) {
1555
+ extensionHandle.commit();
1556
+ }
1557
+
1558
+ return {
1559
+ connectorId: options.connectorId,
1560
+ status: "installed",
1561
+ configPath,
1562
+ message: `Installed ${manifest.name} v${manifest.version}${extensionMessage}`,
1563
+ };
1564
+ }
1565
+
1566
+ // ── Remove connector ───────────────────────────────────────────────────────
1567
+
1568
+ export function removeConnector(connectorId: string): RemoveResult {
1569
+ const configDir = getConnectorsDir();
1570
+ const configPath = path.join(configDir, `${connectorId}.json`);
1571
+
1572
+ // For codex-cli, read the saved config BEFORE touching anything so we have
1573
+ // both the persisted codexHome and the installExtension flag available for
1574
+ // later use in extension removal (Findings 1, 3, 4, 5).
1575
+ let codexHomeOverride: string | null = null;
1576
+ let savedInstallExtension: boolean | undefined = undefined;
1577
+ // Finding 1: track whether config parsing succeeded. If parsing throws, we
1578
+ // cannot trust any metadata and must fail closed (skip extension removal).
1579
+ let configParsed = false;
1580
+ if (connectorId === "codex-cli" && fs.existsSync(configPath)) {
1581
+ try {
1582
+ const parsed = JSON.parse(fs.readFileSync(configPath, "utf8")) as Record<string, unknown>;
1583
+ configParsed = true;
1584
+ if (typeof parsed.codexHome === "string" && parsed.codexHome.length > 0) {
1585
+ codexHomeOverride = parsed.codexHome;
1586
+ }
1587
+ // Finding 4: coerce saved installExtension so string "false" still works.
1588
+ const coerced = coerceInstallExtension(parsed.installExtension);
1589
+ if (coerced !== undefined) {
1590
+ savedInstallExtension = coerced;
1591
+ }
1592
+ } catch {
1593
+ // Finding 1: config is malformed — log debug and fail closed.
1594
+ // codexHomeOverride and savedInstallExtension remain unset; configParsed
1595
+ // stays false so extension removal is skipped below.
1596
+ console.debug(
1597
+ "[remnic/connectors] removeConnector: codex-cli.json parse failed — skipping extension removal to avoid touching unverified paths",
1598
+ );
1599
+ }
1600
+ }
1601
+
1602
+ if (!fs.existsSync(configPath)) {
1603
+ // Best-effort: revoke any orphan token that may have survived a prior partial
1604
+ // cleanup (e.g. connector JSON deleted manually or XDG_CONFIG_HOME change).
1605
+ // This prevents a stale bearer token from remaining valid in tokens.json while
1606
+ // the connector appears "not installed" to the caller.
1607
+ // Config file is missing — we have no evidence that this installation ever
1608
+ // managed the extension directory, so it is unsafe to remove it (the user
1609
+ // may have self-managed it or installed with installExtension=false).
1610
+ // Skip removeCodexMemoryExtension entirely in this recovery path.
1611
+ let staleTokenRevoked = false;
1612
+ try {
1613
+ staleTokenRevoked = revokeToken(connectorId);
1614
+ } catch {
1615
+ // Best-effort: token store may be missing or read-only; do not mask the
1616
+ // not_found signal to the caller.
1617
+ }
1618
+ const message = staleTokenRevoked
1619
+ ? `${connectorId} is not installed. Removed stale token entry for ${connectorId}.`
1620
+ : "Not installed";
1621
+ return {
1622
+ connectorId,
1623
+ configPath,
1624
+ status: "not_found",
1625
+ message,
1626
+ };
1627
+ }
1628
+
1629
+ // Read connector config before deleting it (needed for hermes profile lookup)
1630
+ let storedProfile = "default";
1631
+ if (connectorId === "hermes") {
1632
+ try {
1633
+ const stored = JSON.parse(fs.readFileSync(configPath, "utf8"));
1634
+ if (typeof stored?.profile === "string") storedProfile = stored.profile;
1635
+ } catch {
1636
+ // use default profile
1637
+ }
1638
+ }
1639
+
1640
+ // For weclone, read the persisted proxy config path from the saved registry
1641
+ // config BEFORE deleting it. Using the persisted absolute path (rather than
1642
+ // recomputing from current REMNIC_HOME / ENGRAM_HOME / $HOME) guarantees
1643
+ // that a remove still targets the original file even if the environment
1644
+ // has changed between install and remove.
1645
+ //
1646
+ // Parse failure handling: if the registry config exists but is malformed,
1647
+ // we MUST abort the whole removal (mirror of the codex-cli provenance
1648
+ // gate). Silently falling back to an env-derived path would delete the
1649
+ // registry entry first and then miss the real proxy config if the
1650
+ // environment had since changed, orphaning the file (which may still hold
1651
+ // a live bearer token). Only install-time WRITES persist the path; if we
1652
+ // lost it on read, the only safe action is to stop and let the operator
1653
+ // fix the config or clean up manually.
1654
+ let weCloneProxyConfigPath: string | null = null;
1655
+ let weCloneRegistryParseFailed = false;
1656
+ if (connectorId === "weclone") {
1657
+ try {
1658
+ const stored = JSON.parse(fs.readFileSync(configPath, "utf8")) as Record<string, unknown>;
1659
+ if (typeof stored.proxyConfigPath === "string" && stored.proxyConfigPath.length > 0) {
1660
+ weCloneProxyConfigPath = stored.proxyConfigPath;
1661
+ }
1662
+ } catch {
1663
+ weCloneRegistryParseFailed = true;
1664
+ }
1665
+ // No persisted path AND parse succeeded means this is a legacy install
1666
+ // pre-dating proxyConfigPath provenance. Fall back to env resolution
1667
+ // only in that specific case so we still make a best-effort cleanup.
1668
+ if (weCloneProxyConfigPath === null && !weCloneRegistryParseFailed) {
1669
+ try {
1670
+ weCloneProxyConfigPath = resolveWeCloneProxyConfigPath();
1671
+ } catch {
1672
+ // Resolution failed (e.g. no HOME) — leave null; cleanup block skips.
1673
+ }
1674
+ }
1675
+ }
1676
+ if (connectorId === "weclone" && weCloneRegistryParseFailed) {
1677
+ console.warn(
1678
+ "[remnic/connectors] removeConnector: weclone.json is malformed — " +
1679
+ "aborting removal to preserve provenance. Fix or delete " +
1680
+ configPath +
1681
+ " manually and retry.",
1682
+ );
1683
+ return {
1684
+ connectorId,
1685
+ configPath,
1686
+ message:
1687
+ "Removal aborted: weclone.json is malformed. Registry config left in place for inspection; " +
1688
+ "proxy config NOT removed.",
1689
+ status: "skipped",
1690
+ reason: "config-parse-failed",
1691
+ };
1692
+ }
1693
+
1694
+ // Finding 4: if the codex-cli config exists but failed to parse, abort the
1695
+ // entire removal. Leave both the config file AND the extension directory
1696
+ // untouched so the operator can inspect/fix the config file and retry.
1697
+ // Unlinking the config here would destroy the only provenance record and make
1698
+ // deterministic retry impossible.
1699
+ if (connectorId === "codex-cli" && fs.existsSync(configPath) && !configParsed) {
1700
+ console.warn(
1701
+ "[remnic/connectors] removeConnector: codex-cli.json is malformed — " +
1702
+ "aborting removal to preserve provenance. Fix or delete " +
1703
+ configPath +
1704
+ " manually and retry.",
1705
+ );
1706
+ return {
1707
+ connectorId,
1708
+ configPath,
1709
+ message: "Removal aborted: codex-cli.json is malformed. Config file left in place for inspection.",
1710
+ status: "skipped",
1711
+ reason: "config-parse-failed",
1712
+ };
1713
+ }
1714
+
1715
+ // Finding 5: remove extension BEFORE deleting the config file. If extension
1716
+ // removal throws (e.g. EPERM/EBUSY), we re-throw WITHOUT deleting the config
1717
+ // so the user can retry — the config still has the persisted codexHome needed
1718
+ // to locate the extension directory.
1719
+ let extensionMessage = "";
1720
+ if (connectorId === "codex-cli") {
1721
+ // Finding 4: skip extension deletion when installExtension was explicitly disabled.
1722
+ if (savedInstallExtension === false) {
1723
+ extensionMessage = " (memory extension: skipped — installExtension=false)";
1724
+ // Finding 3: require EXPLICIT provenance (installExtension===true AND a saved
1725
+ // codexHome) before removing the extension. Legacy configs that pre-date this
1726
+ // feature have no installExtension key, so savedInstallExtension is undefined;
1727
+ // without provenance we cannot be sure Remnic ever owned the directory.
1728
+ } else if (savedInstallExtension !== true || codexHomeOverride === null) {
1729
+ extensionMessage = " (memory extension: skipped — no install provenance in saved config)";
1730
+ } else {
1731
+ const extResult = removeCodexMemoryExtension({ codexHome: codexHomeOverride });
1732
+ extensionMessage = extResult.removed
1733
+ ? ` (memory extension removed: ${extResult.remnicExtensionDir})`
1734
+ : " (no memory extension present)";
1735
+ }
1736
+ }
1737
+
1738
+ // Delete the connector config file AFTER extension removal (Finding 5): if
1739
+ // extension removal throws, we do not reach here and the config is preserved.
1740
+ // Token revocation and YAML cleanup only happen after the file is gone so
1741
+ // that a failed unlink (e.g., read-only directory) does not leave a
1742
+ // token-less orphan install on disk.
1743
+ try {
1744
+ fs.unlinkSync(configPath);
1745
+ } catch (unlinkErr) {
1746
+ const sanitizedErr = unlinkErr instanceof Error ? unlinkErr.message : String(unlinkErr);
1747
+ return {
1748
+ connectorId,
1749
+ configPath,
1750
+ status: "error",
1751
+ message:
1752
+ `${connectorId} remove aborted: could not delete connector file (${sanitizedErr}). ` +
1753
+ `Token and any connector-specific state were not modified.`,
1754
+ };
1755
+ }
1756
+
1757
+ // File removed — now safe to revoke the auth token.
1758
+ // Non-fatal: if the token store is read-only or missing, connector removal
1759
+ // should still succeed. Stale tokens will be rejected by the daemon when the
1760
+ // token file is later accessible.
1761
+ const notes: string[] = [];
1762
+ // Track revocation success so downstream error branches (e.g. weclone
1763
+ // proxy-delete failure) can accurately report whether the token was
1764
+ // cleaned up rather than hardcoding "Token has been rolled back".
1765
+ let tokenRevoked = true;
1766
+ try {
1767
+ revokeToken(connectorId);
1768
+ } catch (revokeErr) {
1769
+ // Surface the failure so callers know the token was not cleaned up.
1770
+ // The connector config has already been removed at this point.
1771
+ tokenRevoked = false;
1772
+ const revokeMsg = revokeErr instanceof Error ? revokeErr.message : String(revokeErr);
1773
+ notes.push(`Warning: token revocation failed — ${revokeMsg}. The token for ${connectorId} may still be present in tokens.json.`);
1774
+ }
1775
+
1776
+ // WeClone-specific: remove the proxy config file at the path persisted in
1777
+ // the registry config (read above before the registry file was deleted).
1778
+ // Using the persisted absolute path — not a re-derivation from the current
1779
+ // environment — is load-bearing: if REMNIC_HOME / ENGRAM_HOME changes (or
1780
+ // is unset) between install and remove, recomputing here would leave the
1781
+ // original proxy config (with a live bearer token) on disk while reporting
1782
+ // success. If the file is present but unlink fails (e.g. EPERM), we MUST
1783
+ // surface an error status rather than pretending success — a later retry
1784
+ // via `remnic connectors remove weclone` would go down the `not_found`
1785
+ // path because the registry config was already unlinked, leaving the
1786
+ // proxy config orphaned (potentially with a still-valid token).
1787
+ let weCloneProxyDeleteFailed: string | null = null;
1788
+ if (connectorId === "weclone") {
1789
+ if (weCloneProxyConfigPath === null) {
1790
+ notes.push(
1791
+ "WeClone proxy config cleanup skipped: no persisted path found in saved config " +
1792
+ "(likely a legacy install predating proxyConfigPath provenance).",
1793
+ );
1794
+ } else {
1795
+ // Safety gate: validate the persisted path before unlinking. Because
1796
+ // `weCloneProxyConfigPath` is loaded from user-controlled JSON, a
1797
+ // malformed or tampered weclone.json could make `removeConnector` delete
1798
+ // an arbitrary file. Restrict deletion to paths that are:
1799
+ // 1. Absolute (relative paths are CWD-dependent and were never written
1800
+ // by the installer).
1801
+ // 2. End with the known suffix "connectors/weclone.json" — the only
1802
+ // filename the installer ever writes, regardless of base directory.
1803
+ // If either check fails, skip the unlink and surface an error so the
1804
+ // operator can clean up manually. Failing closed is safer than silently
1805
+ // deleting an unexpected path.
1806
+ const expectedSuffix = path.join("connectors", "weclone.json");
1807
+ const isSafePath =
1808
+ path.isAbsolute(weCloneProxyConfigPath) &&
1809
+ weCloneProxyConfigPath.endsWith(expectedSuffix);
1810
+ if (!isSafePath) {
1811
+ weCloneProxyDeleteFailed =
1812
+ `Proxy config path ${JSON.stringify(weCloneProxyConfigPath)} failed safety validation ` +
1813
+ `(must be absolute and end with "${expectedSuffix}"). ` +
1814
+ `Refusing to delete — remove the file manually if it exists.`;
1815
+ } else {
1816
+ try {
1817
+ if (fs.existsSync(weCloneProxyConfigPath)) {
1818
+ fs.unlinkSync(weCloneProxyConfigPath);
1819
+ notes.push(`Removed WeClone proxy config: ${weCloneProxyConfigPath}`);
1820
+ }
1821
+ } catch (err) {
1822
+ // Hard failure: leaving the file behind with a live token is a
1823
+ // security issue. Capture the error so we return status:"error".
1824
+ weCloneProxyDeleteFailed = err instanceof Error ? err.message : String(err);
1825
+ }
1826
+ }
1827
+ }
1828
+ }
1829
+ if (weCloneProxyDeleteFailed !== null && weCloneProxyConfigPath !== null) {
1830
+ // Report the token-revocation status truthfully. If revocation already
1831
+ // failed above, claiming the token was "cleaned up" here would mislead
1832
+ // the operator into thinking the only action left is deleting the
1833
+ // orphan file — when in reality the bearer token is also still live.
1834
+ const tokenStatus = tokenRevoked
1835
+ ? "the registry config was deleted and the token was revoked"
1836
+ : "the registry config was deleted but TOKEN REVOCATION ALSO FAILED — " +
1837
+ "inspect ~/.remnic/tokens.json and revoke manually";
1838
+ return {
1839
+ connectorId,
1840
+ configPath,
1841
+ status: "error",
1842
+ message:
1843
+ `WeClone remove partially succeeded: ${tokenStatus}, ` +
1844
+ `but the proxy config at ${weCloneProxyConfigPath} could not be deleted ` +
1845
+ `(${weCloneProxyDeleteFailed}). Manually remove that file — it may still contain ` +
1846
+ `a Remnic daemon bearer token.`,
1847
+ };
1848
+ }
1849
+
1850
+ // Hermes-specific: strip the remnic: block from config.yaml.
1851
+ // Only attempted after successful file removal so that config.yaml cleanup
1852
+ // is consistent with the connector JSON state.
1853
+ if (connectorId === "hermes") {
1854
+ try {
1855
+ const yamlResult = removeHermesConfig({ profile: storedProfile });
1856
+ if (yamlResult.updated) {
1857
+ notes.push(`Removed remnic: block from Hermes config: ${yamlResult.configPath}`);
1858
+ } else if (yamlResult.reason?.startsWith("Hermes config cleanup partially failed:")) {
1859
+ const tokenStatus = tokenRevoked
1860
+ ? "the connector registry config was deleted and the token was revoked"
1861
+ : "the connector registry config was deleted but TOKEN REVOCATION ALSO FAILED — " +
1862
+ "inspect ~/.remnic/tokens.json and revoke manually";
1863
+ return {
1864
+ connectorId,
1865
+ configPath,
1866
+ status: "error",
1867
+ message:
1868
+ `Hermes remove partially succeeded: ${tokenStatus}, but ${yamlResult.reason}. ` +
1869
+ `Updated paths: ${yamlResult.configPath}. Manually remove any stale remnic: ` +
1870
+ `block and token material from the failed Hermes config path.`,
1871
+ };
1872
+ } else if (yamlResult.skipped) {
1873
+ notes.push(`Hermes config cleanup skipped: ${yamlResult.reason}`);
1874
+ }
1875
+ } catch (err) {
1876
+ notes.push(
1877
+ `Hermes config cleanup skipped: ${err instanceof Error ? err.message : String(err)}`,
1878
+ );
1879
+ }
1880
+ }
1881
+
1882
+ const suffix = notes.length > 0 ? `\n ${notes.join("\n ")}` : "";
1883
+ return {
1884
+ connectorId,
1885
+ configPath,
1886
+ status: "removed",
1887
+ message: `Removed${extensionMessage}${suffix}`,
1888
+ };
1889
+ }
1890
+
1891
+ // ── Hermes config.yaml helpers ─────────────────────────────────────────────────
1892
+
1893
+ interface HermesConfigResult {
1894
+ updated: boolean;
1895
+ skipped: boolean;
1896
+ reason?: string;
1897
+ configPath: string;
1898
+ /**
1899
+ * The exact byte-for-byte content of the config.yaml that existed BEFORE
1900
+ * this upsert ran. `null` when the file did not exist (new file was created).
1901
+ * `undefined` when the write was skipped (priorContent is irrelevant).
1902
+ * Used by installConnector to roll back the YAML write if commitTokenEntry
1903
+ * subsequently throws.
1904
+ */
1905
+ priorContent?: string | null;
1906
+ }
1907
+
1908
+ /**
1909
+ * Validate and sanitize a Hermes profile name.
1910
+ *
1911
+ * Profile names appear as a path segment under `~/.hermes/profiles/`, so we
1912
+ * must reject any value that could traverse outside that directory. Hermes
1913
+ * itself restricts profile names to filesystem-safe identifiers; we mirror
1914
+ * that convention and additionally require the resolved config path to stay
1915
+ * under the profiles root.
1916
+ *
1917
+ * Throws on invalid input rather than silently normalizing — the caller
1918
+ * should surface the error so the user can supply a valid profile.
1919
+ */
1920
+ function sanitizeHermesProfile(profile: string): string {
1921
+ if (typeof profile !== "string" || profile.length === 0) {
1922
+ throw new Error("Hermes profile name must be a non-empty string");
1923
+ }
1924
+ // Disallow anything that isn't a plain profile identifier. We accept
1925
+ // letters, digits, hyphen, underscore, and dot — but reject leading dots
1926
+ // (hidden dirs) and any path separator or parent-dir reference.
1927
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(profile)) {
1928
+ throw new Error(
1929
+ `Invalid Hermes profile name: ${JSON.stringify(profile)} — must match [A-Za-z0-9][A-Za-z0-9._-]*`,
1930
+ );
1931
+ }
1932
+ if (profile.includes("..")) {
1933
+ throw new Error(`Invalid Hermes profile name: ${JSON.stringify(profile)} — must not contain ".."`);
1934
+ }
1935
+ return profile;
1936
+ }
1937
+
1938
+ function hermesConfigPath(profile: string): string {
1939
+ const safeProfile = sanitizeHermesProfile(profile);
1940
+ const hermesRoot = path.resolve(resolveHomeDir(), ".hermes");
1941
+ const rootConfigPath = path.join(hermesRoot, "config.yaml");
1942
+ const profilesRoot = path.join(hermesRoot, "profiles");
1943
+ if (safeProfile === "default") {
1944
+ const defaultProfileDir = path.join(profilesRoot, safeProfile);
1945
+ if (isFile(rootConfigPath) || (!fs.existsSync(rootConfigPath) && !isDirectory(defaultProfileDir))) {
1946
+ return rootConfigPath;
1947
+ }
1948
+ }
1949
+ const cfgPath = path.resolve(profilesRoot, safeProfile, "config.yaml");
1950
+ // Defense in depth: ensure the resolved path is still under profilesRoot.
1951
+ const rel = path.relative(profilesRoot, cfgPath);
1952
+ if (rel.startsWith("..") || path.isAbsolute(rel)) {
1953
+ throw new Error(
1954
+ `Invalid Hermes profile path: resolved outside ${profilesRoot}`,
1955
+ );
1956
+ }
1957
+ return cfgPath;
1958
+ }
1959
+
1960
+ function isDirectory(filePath: string): boolean {
1961
+ try {
1962
+ return fs.statSync(filePath).isDirectory();
1963
+ } catch {
1964
+ return false;
1965
+ }
1966
+ }
1967
+
1968
+ function isFile(filePath: string): boolean {
1969
+ try {
1970
+ return fs.statSync(filePath).isFile();
1971
+ } catch {
1972
+ return false;
1973
+ }
1974
+ }
1975
+
1976
+ function hermesConfigTarget(filePath: string): string {
1977
+ try {
1978
+ return fs.realpathSync.native(filePath);
1979
+ } catch {
1980
+ return path.resolve(filePath);
1981
+ }
1982
+ }
1983
+
1984
+ function sameHermesConfigTarget(leftPath: string, rightPath: string): boolean {
1985
+ return hermesConfigTarget(leftPath) === hermesConfigTarget(rightPath);
1986
+ }
1987
+
1988
+ function hermesDefaultProfileConfigPath(): string {
1989
+ const hermesRoot = path.resolve(resolveHomeDir(), ".hermes");
1990
+ return path.join(hermesRoot, "profiles", "default", "config.yaml");
1991
+ }
1992
+
1993
+ function hermesConfigCleanupPaths(profile: string): string[] {
1994
+ const cfgPath = hermesConfigPath(profile);
1995
+ const safeProfile = sanitizeHermesProfile(profile);
1996
+ if (safeProfile !== "default") {
1997
+ return [cfgPath];
1998
+ }
1999
+ return [...new Set([cfgPath, hermesDefaultProfileConfigPath()])];
2000
+ }
2001
+
2002
+ /**
2003
+ * Validate a Hermes host string before interpolating it into YAML.
2004
+ *
2005
+ * YAML-injection guard: connector config values come from raw CLI input
2006
+ * (`--config host=...`) or config-file JSON, both of which are untrusted.
2007
+ * Without validation, a value like `127.0.0.1"\n session_key: "evil`
2008
+ * would emit additional YAML keys into the `remnic:` block and silently
2009
+ * override Hermes settings.
2010
+ *
2011
+ * Accepted forms:
2012
+ * - Plain IPv4: 127.0.0.1, 10.0.0.5
2013
+ * - Plain DNS hostname: localhost, foo.example.com
2014
+ * - Bracketed IPv6 literal: [::1], [2001:db8::1]
2015
+ *
2016
+ * Rejected forms:
2017
+ * - host:port combos: 127.0.0.1:4318 (colons not allowed outside brackets)
2018
+ * - Unbalanced brackets: [::1
2019
+ * - Any whitespace, quotes, or control characters
2020
+ *
2021
+ * Hermes builds its base URL as `http://{host}:{port}`, so supplying a
2022
+ * host that already embeds a port (e.g. "127.0.0.1:4318") would produce
2023
+ * the double-port URL "http://127.0.0.1:4318:4318/..." and fail at runtime
2024
+ * even though install reports success. We reject that form here.
2025
+ */
2026
+ function sanitizeHermesHost(host: string): string {
2027
+ if (typeof host !== "string" || host.length === 0) {
2028
+ throw new Error("Hermes host must be a non-empty string");
2029
+ }
2030
+ if (host.length > 253) {
2031
+ throw new Error(`Hermes host too long (max 253 chars): ${JSON.stringify(host.slice(0, 32))}…`);
2032
+ }
2033
+
2034
+ // Bracketed IPv6 literal: must start with "[", end with "]", and contain
2035
+ // only hex digits and colons inside the brackets.
2036
+ if (host.startsWith("[")) {
2037
+ if (!host.endsWith("]")) {
2038
+ throw new Error(
2039
+ `Invalid Hermes host: ${JSON.stringify(host)} — unbalanced brackets in IPv6 literal`,
2040
+ );
2041
+ }
2042
+ const inner = host.slice(1, -1);
2043
+ if (inner.length === 0 || !/^[0-9A-Fa-f:]+$/.test(inner)) {
2044
+ throw new Error(
2045
+ `Invalid Hermes host: ${JSON.stringify(host)} — bracketed IPv6 literal must contain only hex digits and colons`,
2046
+ );
2047
+ }
2048
+ return host;
2049
+ }
2050
+
2051
+ // Unbracketed value: colons are not allowed (would indicate an embedded port
2052
+ // or an unbracketed IPv6 address, both of which must be rejected here).
2053
+ if (host.includes(":")) {
2054
+ throw new Error(
2055
+ `Invalid Hermes host: ${JSON.stringify(host)} — host must not include a port; supply the port separately with --config port=<n>`,
2056
+ );
2057
+ }
2058
+
2059
+ // Plain IPv4 or DNS hostname: allow letters, digits, dots, and hyphens only.
2060
+ // No whitespace, quotes, or control characters.
2061
+ if (!/^[A-Za-z0-9._\-]+$/.test(host)) {
2062
+ throw new Error(
2063
+ `Invalid Hermes host: ${JSON.stringify(host)} — must be a plain hostname or IP literal`,
2064
+ );
2065
+ }
2066
+ return host;
2067
+ }
2068
+
2069
+ /**
2070
+ * Validate a Hermes port value. Accepts positive integers in [1, 65535].
2071
+ *
2072
+ * Rejects non-integer numeric strings (e.g. "4318.9") rather than silently
2073
+ * truncating them — a fractional port is almost certainly a typo and writing
2074
+ * the truncated value to config.yaml would be misleading.
2075
+ */
2076
+ function sanitizeHermesPort(port: number | string): number {
2077
+ const numeric = Number(port);
2078
+ // Reject NaN, Infinity, -Infinity, and any non-integer (e.g. 4318.9)
2079
+ if (!Number.isInteger(numeric)) {
2080
+ throw new Error(
2081
+ `Invalid Hermes port "${port}": must be a positive integer`,
2082
+ );
2083
+ }
2084
+ if (numeric < 1 || numeric > 65535) {
2085
+ throw new Error(`Invalid Hermes port: ${JSON.stringify(port)} — must be an integer in [1, 65535]`);
2086
+ }
2087
+ return numeric;
2088
+ }
2089
+
2090
+ /**
2091
+ * Write a file with owner-only (0o600) permissions.
2092
+ *
2093
+ * Used for any file that may contain a bearer token. writeFileSync's `mode`
2094
+ * option only applies when the file is newly created, so we also chmod
2095
+ * afterwards to tighten permissions on pre-existing files. The chmod is
2096
+ * best-effort on platforms that don't support POSIX modes.
2097
+ */
2098
+ function writeSecretFileSync(filePath: string, data: string): void {
2099
+ fs.writeFileSync(filePath, data, { mode: 0o600 });
2100
+ try {
2101
+ fs.chmodSync(filePath, 0o600);
2102
+ } catch {
2103
+ /* best-effort on non-POSIX filesystems */
2104
+ }
2105
+ }
2106
+
2107
+ /**
2108
+ * Upsert the `remnic:` block in a Hermes profile config.yaml.
2109
+ *
2110
+ * Rules:
2111
+ * - If the profile directory does not exist, skip with a warning (we do not
2112
+ * create arbitrary Hermes state).
2113
+ * - If config.yaml does not exist, create it with only the remnic: block.
2114
+ * - If config.yaml exists and already contains a `remnic:` block, update the
2115
+ * host/port/token lines in-place (line-based, preserves comments elsewhere).
2116
+ * - If config.yaml exists with no `remnic:` block, append one.
2117
+ * - Idempotent on repeated calls.
2118
+ */
2119
+ export function upsertHermesConfig(opts: {
2120
+ profile: string;
2121
+ host: string;
2122
+ port: number;
2123
+ token: string;
2124
+ }): HermesConfigResult {
2125
+ const cfgPath = hermesConfigPath(opts.profile);
2126
+ const profileDir = path.dirname(cfgPath);
2127
+
2128
+ // YAML-injection guard: validate scalar values before interpolating them
2129
+ // into the `remnic:` block. sanitizeHermesHost/Port throw on anything
2130
+ // that could break out of the scalar context.
2131
+ const safeHost = sanitizeHermesHost(opts.host);
2132
+ const safePort = sanitizeHermesPort(opts.port);
2133
+ // Token is generated by randomBytes + a fixed alphabetic prefix, so it's
2134
+ // already safe for YAML scalar interpolation. We still guard against an
2135
+ // unexpectedly malformed token reaching this function.
2136
+ if (!/^[A-Za-z0-9_]+$/.test(opts.token)) {
2137
+ throw new Error("Invalid Hermes token: contains non-alphanumeric characters");
2138
+ }
2139
+
2140
+ if (!isDirectory(profileDir)) {
2141
+ return {
2142
+ updated: false,
2143
+ skipped: true,
2144
+ reason: `Hermes profile directory not found: ${profileDir}`,
2145
+ configPath: cfgPath,
2146
+ };
2147
+ }
2148
+
2149
+ const block = [
2150
+ "remnic:",
2151
+ ` host: "${safeHost}"`,
2152
+ ` port: ${safePort}`,
2153
+ ` token: "${opts.token}"`,
2154
+ ].join("\n");
2155
+
2156
+ if (!fs.existsSync(cfgPath)) {
2157
+ // Create with just the remnic block. 0o600 because the file now holds
2158
+ // a bearer token — matching the permissions on ~/.remnic/tokens.json.
2159
+ writeSecretFileSync(cfgPath, block + "\n");
2160
+ // priorContent: null signals "file was created new" — rollback means delete.
2161
+ return { updated: true, skipped: false, configPath: cfgPath, priorContent: null };
2162
+ }
2163
+
2164
+ const raw = fs.readFileSync(cfgPath, "utf8");
2165
+
2166
+ // Check whether there's an existing remnic: block
2167
+ const hasRemnicBlock = /^remnic:/m.test(raw);
2168
+
2169
+ if (!hasRemnicBlock) {
2170
+ // Append the block (preserve existing content)
2171
+ const separator = raw.endsWith("\n") ? "\n" : "\n\n";
2172
+ writeSecretFileSync(cfgPath, raw + separator + block + "\n");
2173
+ // priorContent: raw preserves the original file so it can be restored on rollback.
2174
+ return { updated: true, skipped: false, configPath: cfgPath, priorContent: raw };
2175
+ }
2176
+
2177
+ // Update the existing block. Strategy: replace the content of the remnic:
2178
+ // section by matching from `^remnic:` to the next top-level key or end-of-file.
2179
+ // We rewrite only the host/port/token sub-keys inside the block; other keys
2180
+ // under remnic: (e.g. session_key, timeout) are preserved.
2181
+ //
2182
+ // Trailing-newline handling: split("\n") on a file that ends with "\n" produces
2183
+ // a final empty-string element. If that element is still inside the remnic block
2184
+ // when we hit it, it gets pushed to newLines via the else branch — placing a
2185
+ // blank line between existing sub-keys and any newly-appended missing sub-keys.
2186
+ // We strip the trailing empty element before the loop and re-add a single "\n"
2187
+ // at write time, normalising the file to always end with exactly one newline.
2188
+ const splitLines = raw.split("\n");
2189
+ // Remove trailing empty element produced by a file that ends with "\n"
2190
+ if (splitLines.length > 0 && splitLines[splitLines.length - 1] === "") {
2191
+ splitLines.pop();
2192
+ }
2193
+ const lines = splitLines;
2194
+ const newLines: string[] = [];
2195
+ let inRemnicBlock = false;
2196
+ let blockWritten = false;
2197
+
2198
+ // Track which sub-keys we've emitted
2199
+ const written = { host: false, port: false, token: false };
2200
+
2201
+ for (let i = 0; i < lines.length; i++) {
2202
+ const line = lines[i];
2203
+
2204
+ if (/^remnic:/.test(line)) {
2205
+ inRemnicBlock = true;
2206
+ newLines.push(line);
2207
+ continue;
2208
+ }
2209
+
2210
+ if (inRemnicBlock) {
2211
+ // A line that starts with a non-space character and is not empty signals
2212
+ // the start of the next top-level YAML key — we've left the remnic block.
2213
+ if (line.length > 0 && !/^\s/.test(line)) {
2214
+ // Emit any un-written keys before closing the block. Uses the
2215
+ // already-validated safeHost/safePort values.
2216
+ if (!written.host) newLines.push(` host: "${safeHost}"`);
2217
+ if (!written.port) newLines.push(` port: ${safePort}`);
2218
+ if (!written.token) newLines.push(` token: "${opts.token}"`);
2219
+ blockWritten = true;
2220
+ inRemnicBlock = false;
2221
+ newLines.push(line);
2222
+ continue;
2223
+ }
2224
+
2225
+ // Replace host/port/token lines; preserve other sub-keys
2226
+ if (/^\s+host:/.test(line)) {
2227
+ newLines.push(` host: "${safeHost}"`);
2228
+ written.host = true;
2229
+ } else if (/^\s+port:/.test(line)) {
2230
+ newLines.push(` port: ${safePort}`);
2231
+ written.port = true;
2232
+ } else if (/^\s+token:/.test(line)) {
2233
+ newLines.push(` token: "${opts.token}"`);
2234
+ written.token = true;
2235
+ } else {
2236
+ newLines.push(line);
2237
+ }
2238
+ continue;
2239
+ }
2240
+
2241
+ newLines.push(line);
2242
+ }
2243
+
2244
+ if (inRemnicBlock && !blockWritten) {
2245
+ // File ended while still inside the remnic block
2246
+ if (!written.host) newLines.push(` host: "${safeHost}"`);
2247
+ if (!written.port) newLines.push(` port: ${safePort}`);
2248
+ if (!written.token) newLines.push(` token: "${opts.token}"`);
2249
+ }
2250
+
2251
+ // Always write exactly one trailing newline, matching the create and append paths.
2252
+ writeSecretFileSync(cfgPath, newLines.join("\n") + "\n");
2253
+ // priorContent: raw is the original file content for rollback if needed.
2254
+ return { updated: true, skipped: false, configPath: cfgPath, priorContent: raw };
2255
+ }
2256
+
2257
+ /**
2258
+ * Remove the `remnic:` block from a Hermes profile config.yaml.
2259
+ * Idempotent — if the block is absent, returns skipped.
2260
+ */
2261
+ export function removeHermesConfig(opts: { profile: string }): HermesConfigResult {
2262
+ const cfgPaths = hermesConfigCleanupPaths(opts.profile);
2263
+ const results = cfgPaths.map((cfgPath) => {
2264
+ try {
2265
+ return removeHermesConfigFile(cfgPath);
2266
+ } catch (err) {
2267
+ return {
2268
+ updated: false,
2269
+ skipped: true,
2270
+ reason: `Hermes config cleanup failed: ${err instanceof Error ? err.message : String(err)}`,
2271
+ configPath: cfgPath,
2272
+ };
2273
+ }
2274
+ });
2275
+ const updated = results.filter((result) => result.updated);
2276
+ const cleanupFailures = results.filter((result) => result.reason?.startsWith("Hermes config cleanup failed:"));
2277
+
2278
+ if (updated.length > 0) {
2279
+ const updatedPaths = updated.map((result) => result.configPath).join(", ");
2280
+ if (cleanupFailures.length > 0) {
2281
+ const failedPaths = cleanupFailures.map((result) => result.configPath).join(", ");
2282
+ return {
2283
+ updated: false,
2284
+ skipped: true,
2285
+ reason: `Hermes config cleanup partially failed: updated ${updatedPaths}; failed ${failedPaths}`,
2286
+ configPath: `${updatedPaths}; failed: ${failedPaths}`,
2287
+ };
2288
+ }
2289
+ return {
2290
+ updated: true,
2291
+ skipped: false,
2292
+ configPath: updatedPaths,
2293
+ };
2294
+ }
2295
+
2296
+ const cleanupFailure = cleanupFailures[0];
2297
+ if (cleanupFailure) {
2298
+ return cleanupFailure;
2299
+ }
2300
+
2301
+ const existingWithoutBlock = results.find((result) => result.reason !== "Hermes config.yaml not found");
2302
+ return existingWithoutBlock ?? results[0] ?? {
2303
+ updated: false,
2304
+ skipped: true,
2305
+ reason: "Hermes config.yaml not found",
2306
+ configPath: hermesConfigPath(opts.profile),
2307
+ };
2308
+ }
2309
+
2310
+ function removeHermesConfigFile(cfgPath: string): HermesConfigResult {
2311
+ if (!fs.existsSync(cfgPath)) {
2312
+ return {
2313
+ updated: false,
2314
+ skipped: true,
2315
+ reason: "Hermes config.yaml not found",
2316
+ configPath: cfgPath,
2317
+ };
2318
+ }
2319
+
2320
+ const raw = fs.readFileSync(cfgPath, "utf8");
2321
+ if (!/^remnic:/m.test(raw)) {
2322
+ return {
2323
+ updated: false,
2324
+ skipped: true,
2325
+ reason: "No remnic: block found in config.yaml",
2326
+ configPath: cfgPath,
2327
+ };
2328
+ }
2329
+
2330
+ // Strip the remnic: block and its indented children
2331
+ const lines = raw.split("\n");
2332
+ const newLines: string[] = [];
2333
+ let inRemnicBlock = false;
2334
+
2335
+ for (const line of lines) {
2336
+ if (/^remnic:/.test(line)) {
2337
+ inRemnicBlock = true;
2338
+ continue;
2339
+ }
2340
+ if (inRemnicBlock) {
2341
+ if (line.length > 0 && !/^\s/.test(line)) {
2342
+ inRemnicBlock = false;
2343
+ newLines.push(line);
2344
+ }
2345
+ // else: still in the block — skip the line
2346
+ continue;
2347
+ }
2348
+ newLines.push(line);
2349
+ }
2350
+
2351
+ // Trim trailing blank lines left behind after the block removal
2352
+ while (newLines.length > 0 && newLines[newLines.length - 1]?.trim() === "") {
2353
+ newLines.pop();
2354
+ }
2355
+
2356
+ // Use writeSecretFileSync to keep the file at 0o600 even after the token
2357
+ // has been removed. The file previously held a bearer token (so it was
2358
+ // written with 0o600 originally); preserving that mode prevents a window
2359
+ // where a rewrite with default umask temporarily widens permissions.
2360
+ writeSecretFileSync(cfgPath, newLines.length > 0 ? newLines.join("\n") + "\n" : "");
2361
+ return { updated: true, skipped: false, configPath: cfgPath };
2362
+ }
2363
+
2364
+ // ── Daemon health check (synchronous, non-fatal) ────────────────────────────
2365
+
2366
+ /**
2367
+ * Probe exit-code contract (used by checkDaemonHealth):
2368
+ * 0 — HTTP 200 (healthy)
2369
+ * 2 — HTTP 401 (token cache miss: retry after TTL)
2370
+ * 1 — any other HTTP status or network error
2371
+ */
2372
+ const HEALTH_EXIT_OK = 0;
2373
+ const HEALTH_EXIT_UNAUTHORIZED = 2;
2374
+
2375
+ /**
2376
+ * Ping /engram/v1/health synchronously.
2377
+ * Returns true if the daemon responds with HTTP 200, false otherwise.
2378
+ * Uses a synchronous helper to run a one-liner Node script so that the existing
2379
+ * installConnector() flow does not need to become async.
2380
+ *
2381
+ * Data (host, port, token) are passed via environment variables — NOT
2382
+ * interpolated into the script string — to prevent injection from
2383
+ * user-supplied config values.
2384
+ *
2385
+ * /engram/v1/health is protected by bearer auth in the access HTTP server,
2386
+ * so the caller must pass the connector token (or the configured server
2387
+ * token) or the probe will always return 401 and report the daemon as
2388
+ * unreachable even when it is running.
2389
+ *
2390
+ * 401 handling: the daemon caches valid tokens with a 5-second TTL
2391
+ * (getAllValidTokensCached). A freshly-rotated token may not appear in the
2392
+ * cache for up to 5 s after rotation. We tolerate a single 401 by sleeping
2393
+ * one cache TTL (6000 ms = 5 s TTL + 1 s buffer) and retrying exactly once.
2394
+ */
2395
+ function checkDaemonHealth(host: string, port: number, authToken?: string): boolean {
2396
+ try {
2397
+ // Validate port: must be an integer in [1, 65535].
2398
+ // This guards against user config supplying a non-numeric string.
2399
+ const safePort = Math.trunc(Number(port));
2400
+ if (!Number.isFinite(safePort) || safePort < 1 || safePort > 65535) {
2401
+ return false;
2402
+ }
2403
+ // Finding 7 fix: Node's http.get({ host }) expects an unbracketed IPv6
2404
+ // literal (e.g. "::1"), but sanitizeHermesHost permits bracketed form
2405
+ // "[::1]" (required for URL contexts). Strip the brackets here so that
2406
+ // http.get receives the bare address and doesn't fail to connect.
2407
+ // IPv4 and hostname strings are unaffected (no brackets to strip).
2408
+ const bareHost = host.startsWith("[") && host.endsWith("]")
2409
+ ? host.slice(1, -1)
2410
+ : host;
2411
+
2412
+ // Data (host, port, token) are passed via env vars, never interpolated
2413
+ // into the script string, preventing any code-injection from malformed
2414
+ // config values.
2415
+ // Exit codes: 0 = 200 OK, 2 = 401 Unauthorized, 1 = other error.
2416
+ const script = [
2417
+ "const http = require('http');",
2418
+ "const env = process['env'];",
2419
+ "const headers = {};",
2420
+ "if (env.REMNIC_HEALTH_TOKEN) {",
2421
+ " headers['authorization'] = 'Bearer ' + env.REMNIC_HEALTH_TOKEN;",
2422
+ "}",
2423
+ "const req = http.get({",
2424
+ " host: env.REMNIC_HEALTH_HOST,",
2425
+ " port: parseInt(env.REMNIC_HEALTH_PORT, 10),",
2426
+ " path: '/engram/v1/health',",
2427
+ " headers,",
2428
+ " timeout: 3000,",
2429
+ "}, (res) => { process.exit(res.statusCode === 200 ? 0 : res.statusCode === 401 ? 2 : 1); });",
2430
+ "req.on('error', () => process.exit(1));",
2431
+ "req.on('timeout', () => { req.destroy(); process.exit(1); });",
2432
+ ].join("\n");
2433
+ const env: NodeJS.ProcessEnv = mergeEnv({
2434
+ REMNIC_HEALTH_HOST: bareHost,
2435
+ REMNIC_HEALTH_PORT: String(safePort),
2436
+ });
2437
+ if (authToken) {
2438
+ env.REMNIC_HEALTH_TOKEN = authToken;
2439
+ }
2440
+ const processPath = process.execPath;
2441
+ const launchOptions = { timeout: 4000, env };
2442
+ const result = launchProcessSync(processPath, ["-e", script], launchOptions);
2443
+
2444
+ if (result.status === HEALTH_EXIT_OK) {
2445
+ return true;
2446
+ }
2447
+
2448
+ if (result.status === HEALTH_EXIT_UNAUTHORIZED) {
2449
+ // The daemon's token cache (5 s TTL) has not yet picked up the freshly
2450
+ // rotated token. Sleep one TTL + buffer and retry exactly once.
2451
+ console.error(
2452
+ "[remnic/connectors] health probe got 401 — retrying after token cache TTL...",
2453
+ );
2454
+ // Synchronous sleep without making the caller async.
2455
+ launchProcessSync(processPath, ["-e", "setTimeout(() => {}, 6000)"], {
2456
+ timeout: 7000,
2457
+ env: {},
2458
+ });
2459
+ const retry = launchProcessSync(processPath, ["-e", script], launchOptions);
2460
+ return retry.status === HEALTH_EXIT_OK;
2461
+ }
2462
+
2463
+ return false;
2464
+ } catch {
2465
+ return false;
2466
+ }
2467
+ }
2468
+
2469
+ // ── Doctor ────────────────────────────────────────────────────────────────────
2470
+
2471
+ export async function doctorConnector(connectorId: string): Promise<DoctorResult> {
2472
+ const installed = listConnectors().installed;
2473
+ const instance = installed.find((c) => c.connectorId === connectorId);
2474
+
2475
+ if (!instance) {
2476
+ return {
2477
+ connectorId,
2478
+ checks: [{ name: "Installed", ok: false, detail: "Not installed" }],
2479
+ healthy: false,
2480
+ };
2481
+ }
2482
+
2483
+ const configPath = path.join(getConnectorsDir(), `${connectorId}.json`);
2484
+ const checks: DoctorCheck[] = [];
2485
+
2486
+ // Check config exists
2487
+ checks.push({
2488
+ name: "Config file",
2489
+ ok: fs.existsSync(configPath),
2490
+ detail: configPath,
2491
+ });
2492
+
2493
+ // Check config is valid JSON
2494
+ try {
2495
+ const raw = fs.readFileSync(configPath, "utf8");
2496
+ JSON.parse(raw);
2497
+ checks.push({ name: "Config valid", ok: true, detail: "OK" });
2498
+ } catch (e) {
2499
+ checks.push({ name: "Config valid", ok: false, detail: String(e) });
2500
+ }
2501
+
2502
+ // Check MCP server reachable (if applicable)
2503
+ const mcpUrl = instance.config.mcpServerUrl as string | undefined;
2504
+ if (mcpUrl) {
2505
+ try {
2506
+ const controller = new AbortController();
2507
+ const timeoutId = setTimeout(() => controller.abort(), 3000);
2508
+ const response = await fetch(mcpUrl, { signal: controller.signal });
2509
+ clearTimeout(timeoutId);
2510
+ checks.push({ name: "MCP server", ok: response.ok, detail: mcpUrl });
2511
+ } catch (e) {
2512
+ checks.push({
2513
+ name: "MCP server",
2514
+ ok: false,
2515
+ detail: `Cannot reach ${mcpUrl}: ${e instanceof Error ? e.message : "unknown"}`,
2516
+ });
2517
+ }
2518
+ }
2519
+
2520
+ // Check memory dir (if applicable)
2521
+ const memoryDir = instance.config.memoryDir as string | undefined;
2522
+ if (memoryDir) {
2523
+ if (fs.existsSync(memoryDir)) {
2524
+ checks.push({ name: "Memory directory", ok: true, detail: memoryDir });
2525
+ } else {
2526
+ checks.push({ name: "Memory directory", ok: false, detail: `Not found: ${memoryDir}` });
2527
+ }
2528
+ }
2529
+
2530
+ const healthy = checks.every((c) => c.ok);
2531
+ return { connectorId, checks, healthy };
2532
+ }
2533
+
2534
+ // ── Codex memory extension install ────────────────────────────────────────
2535
+
2536
+ /**
2537
+ * Name of the Codex memories folder. Matches Codex's
2538
+ * `MEMORIES_SUBDIR = "memories"`.
2539
+ */
2540
+ const CODEX_MEMORIES_SUBDIR = "memories";
2541
+
2542
+ /**
2543
+ * Name of the Codex memory-extensions folder. Matches Codex's
2544
+ * `EXTENSIONS_SUBDIR = "memories_extensions"`.
2545
+ *
2546
+ * Codex computes the extensions root as a **sibling** of the memories dir via
2547
+ * Rust's `Path::with_file_name("memories_extensions")` — so for the default
2548
+ * Codex home the layout is:
2549
+ *
2550
+ * ~/.codex/memories/
2551
+ * ~/.codex/memories_extensions/
2552
+ *
2553
+ * Extension files live **outside** of `memories/`, never inside it.
2554
+ */
2555
+ const CODEX_EXTENSIONS_SUBDIR = "memories_extensions";
2556
+
2557
+ /** Folder name Remnic installs its extension under. */
2558
+ const REMNIC_EXTENSION_DIR_NAME = "remnic";
2559
+
2560
+ export interface CodexMemoryExtensionPaths {
2561
+ /** Resolved Codex home directory (e.g. `~/.codex`). */
2562
+ codexHome: string;
2563
+ /** Resolved Codex memories directory (`<codex_home>/memories`). */
2564
+ memoriesDir: string;
2565
+ /** Sibling extensions root (`<codex_home>/memories_extensions`). */
2566
+ extensionsRoot: string;
2567
+ /** The specific Remnic extension directory inside the extensions root. */
2568
+ remnicExtensionDir: string;
2569
+ }
2570
+
2571
+ export interface InstallCodexMemoryExtensionOptions {
2572
+ /** Optional override for `$CODEX_HOME`. Highest priority. */
2573
+ codexHome?: string | null;
2574
+ /** Optional override for the plugin-codex extension source directory. */
2575
+ sourceDir?: string | null;
2576
+ }
2577
+
2578
+ export interface InstallCodexMemoryExtensionResult extends CodexMemoryExtensionPaths {
2579
+ /** Absolute path to the installed `instructions.md`. */
2580
+ instructionsPath: string;
2581
+ /** Number of files copied. */
2582
+ filesCopied: number;
2583
+ /**
2584
+ * Commit the install: permanently remove the backup of the prior extension
2585
+ * (if one existed). Call this once the config write has succeeded.
2586
+ */
2587
+ commit(): void;
2588
+ /**
2589
+ * Roll back the install: restore the prior extension if one existed, or
2590
+ * remove the newly-installed directory for a fresh install. Call this when
2591
+ * a subsequent step (e.g. config write) has failed.
2592
+ */
2593
+ rollback(): void;
2594
+ }
2595
+
2596
+ export interface RemoveCodexMemoryExtensionOptions {
2597
+ codexHome?: string | null;
2598
+ }
2599
+
2600
+ export interface RemoveCodexMemoryExtensionResult extends CodexMemoryExtensionPaths {
2601
+ /** True if an existing `remnic` extension directory was removed. */
2602
+ removed: boolean;
2603
+ }
2604
+
2605
+ /**
2606
+ * Resolve the Codex home directory. Precedence:
2607
+ * 1. explicit `override` argument (from config)
2608
+ * 2. `$CODEX_HOME` env var
2609
+ * 3. `$HOME/.codex`, `$USERPROFILE/.codex`, or the OS home directory
2610
+ */
2611
+ export function resolveCodexHome(override?: string | null): string {
2612
+ if (override && typeof override === "string" && override.trim().length > 0) {
2613
+ return path.resolve(override.trim());
2614
+ }
2615
+ const envHome = readEnvVar("CODEX_HOME");
2616
+ if (envHome && envHome.trim().length > 0) {
2617
+ return path.resolve(envHome.trim());
2618
+ }
2619
+ const home = readEnvVar("HOME") || readEnvVar("USERPROFILE") || resolveHomeDir();
2620
+ return path.resolve(home, ".codex");
2621
+ }
2622
+
2623
+ /**
2624
+ * Compute the Codex memories + memory-extensions layout for a given Codex home.
2625
+ *
2626
+ * The extensions root is computed as a **sibling** of the memories dir by
2627
+ * taking `path.dirname(memoriesDir)` and joining `memories_extensions`. This
2628
+ * mirrors Rust's `with_file_name("memories_extensions")` semantics used by
2629
+ * Codex's `memory_extensions_root()`. Do NOT place the extension inside
2630
+ * `<codex_home>/memories/`.
2631
+ */
2632
+ export function resolveCodexMemoryExtensionPaths(
2633
+ codexHomeOverride?: string | null,
2634
+ ): CodexMemoryExtensionPaths {
2635
+ const codexHome = resolveCodexHome(codexHomeOverride);
2636
+ const memoriesDir = path.join(codexHome, CODEX_MEMORIES_SUBDIR);
2637
+ // Sibling computation: with_file_name(EXTENSIONS_SUBDIR)
2638
+ const extensionsRoot = path.join(path.dirname(memoriesDir), CODEX_EXTENSIONS_SUBDIR);
2639
+ const remnicExtensionDir = path.join(extensionsRoot, REMNIC_EXTENSION_DIR_NAME);
2640
+ return { codexHome, memoriesDir, extensionsRoot, remnicExtensionDir };
2641
+ }
2642
+
2643
+ /**
2644
+ * Locate the plugin-codex `memories_extensions/remnic/` source directory on
2645
+ * disk. Search order:
2646
+ * 1. explicit `override`
2647
+ * 2. resolve via `@remnic/plugin-codex` package (handles global npm installs)
2648
+ * 3. sibling `node_modules/@remnic/plugin-codex` relative to this module
2649
+ * 4. walk upward from this file's location (monorepo development)
2650
+ * 5. walk upward from `process.cwd()` (monorepo fallback)
2651
+ *
2652
+ * Returns the absolute path or throws a descriptive error listing all paths
2653
+ * searched when none exist.
2654
+ */
2655
+ export function locatePluginCodexExtensionSource(override?: string | null): string {
2656
+ if (override && typeof override === "string" && override.trim().length > 0) {
2657
+ const resolved = path.resolve(override.trim());
2658
+ if (fs.existsSync(resolved) && fs.statSync(resolved).isDirectory()) {
2659
+ return resolved;
2660
+ }
2661
+ throw new Error(`Codex extension source directory not found: ${resolved}`);
2662
+ }
2663
+
2664
+ const EXTENSION_SUBPATH = path.join("memories_extensions", "remnic");
2665
+ const WORKSPACE_RELATIVE_PATH = path.join(
2666
+ "packages",
2667
+ "plugin-codex",
2668
+ "memories_extensions",
2669
+ "remnic",
2670
+ );
2671
+
2672
+ const searched: string[] = [];
2673
+
2674
+ // Primary path: the bundled payload shipped with @remnic/core itself.
2675
+ // tsup copies src/connectors/codex/ → dist/connectors/codex/ (see tsup.config.ts
2676
+ // onSuccess hook). However, tsup bundles all source into dist/ as flat files
2677
+ // (dist/index.js, dist/chunk-*.js), so at runtime import.meta.url points to
2678
+ // dist/index.js or a dist/chunk-*.js — NOT dist/connectors/index.js.
2679
+ // Therefore we probe two sibling-relative candidates:
2680
+ // 1. moduleDir/codex — matches tsx/ts-node on src/connectors/index.ts
2681
+ // 2. moduleDir/connectors/codex — matches the tsup dist layout where this code
2682
+ // lands in dist/index.js or dist/chunk-*.js
2683
+ try {
2684
+ const moduleDir = path.dirname(fileURLToPath(import.meta.url));
2685
+
2686
+ // Candidate 1: adjacent codex/ (tsx/ts-node from src/connectors/)
2687
+ const bundledCandidate = path.join(moduleDir, "codex");
2688
+ searched.push(bundledCandidate);
2689
+ if (fs.existsSync(bundledCandidate) && fs.statSync(bundledCandidate).isDirectory()) {
2690
+ return bundledCandidate;
2691
+ }
2692
+
2693
+ // Candidate 2: dist/connectors/codex/ — the tsup output path.
2694
+ // When this module is bundled into dist/index.js or dist/chunk-*.js,
2695
+ // moduleDir is dist/ and tsup copies the payload to dist/connectors/codex/.
2696
+ const distConnectorsCandidate = path.join(moduleDir, "connectors", "codex");
2697
+ searched.push(distConnectorsCandidate);
2698
+ if (
2699
+ fs.existsSync(distConnectorsCandidate) &&
2700
+ fs.statSync(distConnectorsCandidate).isDirectory()
2701
+ ) {
2702
+ return distConnectorsCandidate;
2703
+ }
2704
+ } catch {
2705
+ // import.meta.url unavailable — not running as ESM, skip bundled path.
2706
+ }
2707
+
2708
+ // Finding 2 — path 1: resolve via `@remnic/plugin-codex` package.json.
2709
+ // This covers global `npm install -g @remnic/remnic-core` or pnpm global installs
2710
+ // where the package lives under the global node_modules tree.
2711
+ try {
2712
+ const requireFromHere = createRequire(import.meta.url);
2713
+ const pluginPkgJsonPath = requireFromHere.resolve("@remnic/plugin-codex/package.json");
2714
+ const pluginPkgRoot = path.dirname(pluginPkgJsonPath);
2715
+ const candidate = path.join(pluginPkgRoot, EXTENSION_SUBPATH);
2716
+ searched.push(candidate);
2717
+ if (fs.existsSync(candidate) && fs.statSync(candidate).isDirectory()) {
2718
+ return candidate;
2719
+ }
2720
+ } catch {
2721
+ // @remnic/plugin-codex not installed — fall through to next strategy.
2722
+ }
2723
+
2724
+ // Finding 2 — path 2: sibling node_modules under the module's own directory.
2725
+ // Handles cases like:
2726
+ // .../node_modules/@remnic/remnic-core/src/connectors/index.js
2727
+ // .../node_modules/@remnic/plugin-codex/memories_extensions/remnic
2728
+ try {
2729
+ const moduleDir = path.dirname(fileURLToPath(import.meta.url));
2730
+ let dir = moduleDir;
2731
+ for (let depth = 0; depth < 8; depth += 1) {
2732
+ const candidate = path.join(
2733
+ dir,
2734
+ "node_modules",
2735
+ "@remnic",
2736
+ "plugin-codex",
2737
+ EXTENSION_SUBPATH,
2738
+ );
2739
+ searched.push(candidate);
2740
+ if (fs.existsSync(candidate) && fs.statSync(candidate).isDirectory()) {
2741
+ return candidate;
2742
+ }
2743
+ const parent = path.dirname(dir);
2744
+ if (parent === dir) break;
2745
+ dir = parent;
2746
+ }
2747
+ } catch {
2748
+ // import.meta.url unavailable — not running as ESM.
2749
+ }
2750
+
2751
+ // Finding 2 — path 3 & 4: walk upward from this file's location and from
2752
+ // process.cwd() looking for the monorepo layout (`packages/plugin-codex/…`).
2753
+ const anchors: string[] = [];
2754
+ try {
2755
+ anchors.push(path.dirname(fileURLToPath(import.meta.url)));
2756
+ } catch {
2757
+ // Not running under ESM with import.meta — skip.
2758
+ }
2759
+ anchors.push(process.cwd());
2760
+
2761
+ for (const anchor of anchors) {
2762
+ let dir = anchor;
2763
+ for (let depth = 0; depth < 12; depth += 1) {
2764
+ const candidate = path.join(dir, WORKSPACE_RELATIVE_PATH);
2765
+ searched.push(candidate);
2766
+ if (fs.existsSync(candidate) && fs.statSync(candidate).isDirectory()) {
2767
+ return candidate;
2768
+ }
2769
+ const parent = path.dirname(dir);
2770
+ if (parent === dir) break;
2771
+ dir = parent;
2772
+ }
2773
+ }
2774
+
2775
+ throw new Error(
2776
+ "Could not locate the plugin-codex memories_extensions/remnic source directory.\n" +
2777
+ "Paths searched:\n" +
2778
+ searched.map((p) => ` - ${p}`).join("\n") +
2779
+ "\nInstall @remnic/plugin-codex or pass sourceDir explicitly.",
2780
+ );
2781
+ }
2782
+
2783
+ /** Recursive synchronous directory copy. */
2784
+ function copyDirRecursiveSync(src: string, dest: string): number {
2785
+ let count = 0;
2786
+ fs.mkdirSync(dest, { recursive: true });
2787
+ const entries = fs.readdirSync(src, { withFileTypes: true });
2788
+ for (const entry of entries) {
2789
+ const from = path.join(src, entry.name);
2790
+ const to = path.join(dest, entry.name);
2791
+ if (entry.isDirectory()) {
2792
+ count += copyDirRecursiveSync(from, to);
2793
+ } else if (entry.isFile()) {
2794
+ fs.copyFileSync(from, to);
2795
+ count += 1;
2796
+ }
2797
+ // Skip symlinks, sockets, etc. — extension content is plain files.
2798
+ }
2799
+ return count;
2800
+ }
2801
+
2802
+ /**
2803
+ * Install the Remnic memory extension into `<codex_home>/memories_extensions/remnic/`
2804
+ * atomically. The copy is written to a sibling `.remnic.tmp-<pid>-<ts>` directory
2805
+ * and then renamed into place, so a concurrent Codex phase-2 run never sees a
2806
+ * half-written extension.
2807
+ *
2808
+ * This function is **idempotent and scoped**: it only touches the `remnic`
2809
+ * subfolder inside `memories_extensions/`. Adjacent extensions (other
2810
+ * vendors) are never read, written, or removed.
2811
+ */
2812
+ export function installCodexMemoryExtension(
2813
+ options: InstallCodexMemoryExtensionOptions = {},
2814
+ ): InstallCodexMemoryExtensionResult {
2815
+ const paths = resolveCodexMemoryExtensionPaths(options.codexHome ?? null);
2816
+ const sourceDir = locatePluginCodexExtensionSource(options.sourceDir ?? null);
2817
+
2818
+ fs.mkdirSync(paths.extensionsRoot, { recursive: true });
2819
+
2820
+ // Clean any stale tmp from a previous crashed run by scanning the
2821
+ // extensions root for any `.remnic.tmp-*` prefixed entry. We must do this
2822
+ // BEFORE creating the new tmp directory. Per-entry errors are swallowed so
2823
+ // one bad entry doesn't abort cleanup of the rest.
2824
+ //
2825
+ // Finding 2: only remove tmp dirs that are provably stale (older than
2826
+ // STALE_TMP_THRESHOLD_MS). Dirs younger than the threshold belong to a
2827
+ // concurrent install that is still in progress; deleting them would corrupt
2828
+ // the other process's atomic rename.
2829
+ const tmpPrefix = `.${REMNIC_EXTENSION_DIR_NAME}.tmp-`;
2830
+ const STALE_TMP_THRESHOLD_MS = 10 * 60 * 1000; // 10 minutes
2831
+ const now = Date.now();
2832
+ try {
2833
+ const existingEntries = fs.readdirSync(paths.extensionsRoot);
2834
+ for (const entry of existingEntries) {
2835
+ if (!entry.startsWith(tmpPrefix)) continue;
2836
+ const stalePath = path.join(paths.extensionsRoot, entry);
2837
+ try {
2838
+ const stat = fs.statSync(stalePath);
2839
+ const ageMs = now - stat.mtimeMs;
2840
+ if (ageMs < STALE_TMP_THRESHOLD_MS) {
2841
+ // Too recent — leave it alone; another install is likely still running.
2842
+ continue;
2843
+ }
2844
+ fs.rmSync(stalePath, { recursive: true, force: true });
2845
+ } catch {
2846
+ // swallow — one bad entry should not abort the others
2847
+ }
2848
+ }
2849
+ } catch {
2850
+ // extensions root just-created / unreadable — nothing to clean
2851
+ }
2852
+
2853
+ const tmpName = `${tmpPrefix}${process.pid}-${Date.now()}`;
2854
+ const tmpDir = path.join(paths.extensionsRoot, tmpName);
2855
+
2856
+ let filesCopied = 0;
2857
+ let commitFn: () => void = () => { /* no-op: set below on success */ };
2858
+ let rollbackFn: () => void = () => { /* no-op: set below on success */ };
2859
+ try {
2860
+ filesCopied = copyDirRecursiveSync(sourceDir, tmpDir);
2861
+
2862
+ // Atomic replace: rename old remnic/ to a timestamped backup, then rename
2863
+ // the tmp dir into place. If the second rename fails, restore from backup
2864
+ // so the old extension is never permanently lost.
2865
+ const backupDir = `${paths.remnicExtensionDir}.bak-${Date.now()}`;
2866
+ const hadExisting = fs.existsSync(paths.remnicExtensionDir);
2867
+ if (hadExisting) {
2868
+ fs.renameSync(paths.remnicExtensionDir, backupDir);
2869
+ }
2870
+ try {
2871
+ fs.renameSync(tmpDir, paths.remnicExtensionDir);
2872
+ } catch (renameErr) {
2873
+ // New rename failed — restore backup so the old extension survives.
2874
+ if (hadExisting) {
2875
+ try {
2876
+ fs.renameSync(backupDir, paths.remnicExtensionDir);
2877
+ } catch {
2878
+ // swallow — backup restore best-effort
2879
+ }
2880
+ }
2881
+ throw renameErr;
2882
+ }
2883
+ // The new extension is in place. We intentionally keep the backup alive
2884
+ // until the caller calls commit(). This gives the caller a chance to roll
2885
+ // back to the prior state if a subsequent operation (e.g. config write) fails.
2886
+ //
2887
+ // commit() — remove the backup (called on success)
2888
+ // rollback() — restore the prior extension from backup, or remove the newly
2889
+ // installed directory if this was a fresh install
2890
+ commitFn = (): void => {
2891
+ if (hadExisting) {
2892
+ try {
2893
+ fs.rmSync(backupDir, { recursive: true, force: true });
2894
+ } catch {
2895
+ // swallow — stale backup is harmless
2896
+ }
2897
+ }
2898
+ };
2899
+ rollbackFn = (): void => {
2900
+ if (hadExisting) {
2901
+ // Restore the prior extension from backup.
2902
+ try {
2903
+ // Remove the newly-installed dir first so rename can succeed.
2904
+ if (fs.existsSync(paths.remnicExtensionDir)) {
2905
+ fs.rmSync(paths.remnicExtensionDir, { recursive: true, force: true });
2906
+ }
2907
+ fs.renameSync(backupDir, paths.remnicExtensionDir);
2908
+ } catch {
2909
+ // swallow — best-effort restore; backup remains on disk
2910
+ }
2911
+ } else {
2912
+ // Fresh install — just remove the directory we created.
2913
+ try {
2914
+ if (fs.existsSync(paths.remnicExtensionDir)) {
2915
+ fs.rmSync(paths.remnicExtensionDir, { recursive: true, force: true });
2916
+ }
2917
+ } catch {
2918
+ // swallow
2919
+ }
2920
+ }
2921
+ };
2922
+ } catch (err) {
2923
+ // Best-effort cleanup so we never leave .tmp garbage behind.
2924
+ if (fs.existsSync(tmpDir)) {
2925
+ try {
2926
+ fs.rmSync(tmpDir, { recursive: true, force: true });
2927
+ } catch {
2928
+ // swallow
2929
+ }
2930
+ }
2931
+ throw err;
2932
+ }
2933
+
2934
+ const instructionsPath = path.join(paths.remnicExtensionDir, "instructions.md");
2935
+
2936
+ return {
2937
+ ...paths,
2938
+ instructionsPath,
2939
+ filesCopied,
2940
+ commit: commitFn,
2941
+ rollback: rollbackFn,
2942
+ };
2943
+ }
2944
+
2945
+ /**
2946
+ * Remove the Remnic memory extension. Only touches
2947
+ * `<codex_home>/memories_extensions/remnic/` — never adjacent extensions.
2948
+ */
2949
+ export function removeCodexMemoryExtension(
2950
+ options: RemoveCodexMemoryExtensionOptions = {},
2951
+ ): RemoveCodexMemoryExtensionResult {
2952
+ const paths = resolveCodexMemoryExtensionPaths(options.codexHome ?? null);
2953
+ let removed = false;
2954
+ if (fs.existsSync(paths.remnicExtensionDir)) {
2955
+ fs.rmSync(paths.remnicExtensionDir, { recursive: true, force: true });
2956
+ removed = true;
2957
+ }
2958
+ return { ...paths, removed };
2959
+ }
2960
+
2961
+ // ── Helpers ───────────────────────────────────────────────────────────────────
2962
+
2963
+ function getConnectorsDir(): string {
2964
+ const xdgConfigHome = readEnvVar("XDG_CONFIG_HOME");
2965
+ const configDir = xdgConfigHome
2966
+ ? path.join(xdgConfigHome, "engram")
2967
+ : path.join(resolveHomeDir(), ".config", "engram");
2968
+ return path.join(configDir, REGISTRY_DIR_NAME, "connectors");
2969
+ }
2970
+
2971
+ // ── WeClone proxy config helpers ───────────────────────────────────────────
2972
+ //
2973
+ // The standalone `remnic-weclone-proxy` CLI reads its config from
2974
+ // ~/.remnic/connectors/weclone.json by default. `remnic connectors install
2975
+ // weclone` composes and persists that file so the proxy can start without
2976
+ // additional setup. The file is also tracked by the connector registry (at
2977
+ // getConnectorsDir()/weclone.json) so `remnic connectors list/remove/doctor`
2978
+ // work uniformly across all connectors.
2979
+
2980
+ const WECLONE_PROXY_CONFIG_DIRNAME = ".remnic";
2981
+ const WECLONE_PROXY_CONFIG_FILENAME = "weclone.json";
2982
+
2983
+ /**
2984
+ * Resolve the path to ~/.remnic/connectors/weclone.json for the current user.
2985
+ * Honours REMNIC_HOME / ENGRAM_HOME env overrides so tests can point the
2986
+ * install at a temp dir without leaking into the real home directory.
2987
+ *
2988
+ * Always returns an absolute path via `path.resolve` so install-time and
2989
+ * run-time resolution agree even when the override is a relative path like
2990
+ * `tmp/remnic` (which would otherwise be interpreted against the caller's
2991
+ * current working directory). Must stay in lockstep with the proxy CLI's
2992
+ * `defaultConfigPath()` in @remnic/connector-weclone/src/cli.ts.
2993
+ *
2994
+ * `HOME=""` edge case: a nullish fallback would keep the
2995
+ * empty string (empty is not nullish), which `path.resolve("", ...)` then
2996
+ * interprets as CWD. `os.homedir()` by contrast falls back to the OS
2997
+ * password database when HOME is empty, so the two code paths would
2998
+ * disagree. We therefore treat empty HOME as absent and delegate to
2999
+ * `os.homedir()` in both places — the same rule the proxy CLI follows.
3000
+ */
3001
+ export function resolveWeCloneProxyConfigPath(): string {
3002
+ const remnicHome = readEnvVar("REMNIC_HOME");
3003
+ const override = remnicHome && remnicHome.length > 0 ? remnicHome : readEnvVar("ENGRAM_HOME");
3004
+ if (override && override.length > 0) {
3005
+ return path.resolve(expandTildePath(override), "connectors", WECLONE_PROXY_CONFIG_FILENAME);
3006
+ }
3007
+ const envHome = readEnvVar("HOME");
3008
+ const home = envHome && envHome.length > 0 ? envHome : os.homedir();
3009
+ return path.resolve(
3010
+ home,
3011
+ WECLONE_PROXY_CONFIG_DIRNAME,
3012
+ "connectors",
3013
+ WECLONE_PROXY_CONFIG_FILENAME,
3014
+ );
3015
+ }
3016
+
3017
+ /**
3018
+ * Read the existing proxy config file, if any. Returns raw contents so the
3019
+ * caller can both parse it (for value precedence) and restore it verbatim on
3020
+ * rollback without touching byte-level formatting.
3021
+ */
3022
+ function readWeCloneProxyConfigIfExists(configPath: string): string | null {
3023
+ try {
3024
+ if (!fs.existsSync(configPath)) return null;
3025
+ return fs.readFileSync(configPath, "utf8");
3026
+ } catch {
3027
+ return null;
3028
+ }
3029
+ }
3030
+
3031
+ /** Safely parse a JSON string into a record; returns null on error. */
3032
+ function safeParseJson(raw: string): Record<string, unknown> | null {
3033
+ try {
3034
+ const parsed = JSON.parse(raw);
3035
+ if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
3036
+ return parsed as Record<string, unknown>;
3037
+ }
3038
+ return null;
3039
+ } catch {
3040
+ return null;
3041
+ }
3042
+ }
3043
+
3044
+ interface WeCloneProxyConfig {
3045
+ wecloneApiUrl: string;
3046
+ wecloneModelName: string;
3047
+ proxyPort: number;
3048
+ remnicDaemonUrl: string;
3049
+ remnicAuthToken?: string;
3050
+ sessionStrategy: "caller-id" | "single";
3051
+ memoryInjection: {
3052
+ maxTokens: number;
3053
+ position: "system-append" | "system-prepend";
3054
+ template: string;
3055
+ };
3056
+ }
3057
+
3058
+ const WECLONE_DEFAULTS = {
3059
+ wecloneApiUrl: "http://localhost:8000/v1",
3060
+ wecloneModelName: "weclone-avatar",
3061
+ proxyPort: 8100,
3062
+ remnicDaemonUrl: "http://localhost:4318",
3063
+ sessionStrategy: "single" as const,
3064
+ memoryInjection: {
3065
+ maxTokens: 1500,
3066
+ position: "system-append" as const,
3067
+ template: "[Memory Context]\n{memories}\n[End Memory Context]",
3068
+ },
3069
+ };
3070
+
3071
+ /**
3072
+ * Resolve a string field with precedence: userConfig → priorConfig → default.
3073
+ * Only non-empty strings are accepted from either source; invalid values fall
3074
+ * through so the user gets a working default rather than a broken install.
3075
+ */
3076
+ function resolveStringField(
3077
+ userConfig: Record<string, unknown>,
3078
+ priorConfig: Record<string, unknown> | null,
3079
+ key: string,
3080
+ fallback: string,
3081
+ ): string {
3082
+ const fromUser = userConfig[key];
3083
+ if (typeof fromUser === "string" && fromUser.length > 0) return fromUser;
3084
+ if (priorConfig) {
3085
+ const fromPrior = priorConfig[key];
3086
+ if (typeof fromPrior === "string" && fromPrior.length > 0) return fromPrior;
3087
+ }
3088
+ return fallback;
3089
+ }
3090
+
3091
+ /**
3092
+ * Coerce a config value to an integer port in [1, 65535]. Accepts number or
3093
+ * numeric string (parseConnectorConfig produces strings from `--config
3094
+ * proxyPort=8100`). Returns null if the value is missing or invalid so the
3095
+ * caller can fall through to the next precedence level.
3096
+ */
3097
+ function coercePort(value: unknown): number | null {
3098
+ if (typeof value === "number" && Number.isInteger(value) && value >= 1 && value <= 65535) {
3099
+ return value;
3100
+ }
3101
+ if (typeof value === "string" && value.length > 0) {
3102
+ const n = Number(value);
3103
+ if (Number.isInteger(n) && n >= 1 && n <= 65535) return n;
3104
+ }
3105
+ return null;
3106
+ }
3107
+
3108
+ function resolvePort(
3109
+ userConfig: Record<string, unknown>,
3110
+ priorConfig: Record<string, unknown> | null,
3111
+ fallback: number,
3112
+ ): number {
3113
+ const fromUser = coercePort(userConfig.proxyPort);
3114
+ if (fromUser !== null) return fromUser;
3115
+ if (priorConfig) {
3116
+ const fromPrior = coercePort(priorConfig.proxyPort);
3117
+ if (fromPrior !== null) return fromPrior;
3118
+ }
3119
+ return fallback;
3120
+ }
3121
+
3122
+ function resolveSessionStrategy(
3123
+ userConfig: Record<string, unknown>,
3124
+ priorConfig: Record<string, unknown> | null,
3125
+ ): "caller-id" | "single" {
3126
+ const valid = new Set(["caller-id", "single"]);
3127
+ const fromUser = userConfig.sessionStrategy;
3128
+ if (typeof fromUser === "string" && valid.has(fromUser)) {
3129
+ return fromUser as "caller-id" | "single";
3130
+ }
3131
+ if (priorConfig) {
3132
+ const fromPrior = priorConfig.sessionStrategy;
3133
+ if (typeof fromPrior === "string" && valid.has(fromPrior)) {
3134
+ return fromPrior as "caller-id" | "single";
3135
+ }
3136
+ }
3137
+ return WECLONE_DEFAULTS.sessionStrategy;
3138
+ }
3139
+
3140
+ /**
3141
+ * Compose a WeCloneProxyConfig from user-supplied overrides and any prior
3142
+ * saved config, filling in defaults for every required field. The returned
3143
+ * shape is exactly what the proxy's parseConfig() expects.
3144
+ */
3145
+ export function buildWeCloneProxyConfig(args: {
3146
+ userConfig: Record<string, unknown>;
3147
+ priorConfig: Record<string, unknown> | null;
3148
+ authToken?: string;
3149
+ }): WeCloneProxyConfig {
3150
+ const { userConfig, priorConfig, authToken } = args;
3151
+
3152
+ const wecloneApiUrl = resolveStringField(
3153
+ userConfig,
3154
+ priorConfig,
3155
+ "wecloneApiUrl",
3156
+ WECLONE_DEFAULTS.wecloneApiUrl,
3157
+ );
3158
+ const wecloneModelName = resolveStringField(
3159
+ userConfig,
3160
+ priorConfig,
3161
+ "wecloneModelName",
3162
+ WECLONE_DEFAULTS.wecloneModelName,
3163
+ );
3164
+ const remnicDaemonUrl = resolveStringField(
3165
+ userConfig,
3166
+ priorConfig,
3167
+ "remnicDaemonUrl",
3168
+ WECLONE_DEFAULTS.remnicDaemonUrl,
3169
+ );
3170
+ const proxyPort = resolvePort(
3171
+ userConfig,
3172
+ priorConfig,
3173
+ WECLONE_DEFAULTS.proxyPort,
3174
+ );
3175
+ const sessionStrategy = resolveSessionStrategy(userConfig, priorConfig);
3176
+
3177
+ // Memory injection: always start from defaults, then shallow-merge any
3178
+ // prior values, then user overrides. Individual field validation happens in
3179
+ // the proxy's parseConfig() at proxy startup — here we only assemble a
3180
+ // best-effort shape. A malformed user override would be rejected later with
3181
+ // a clean error message.
3182
+ //
3183
+ // `typeof [] === "object"` so a bare `typeof ... === "object" && ... !==
3184
+ // null` guard would let an array spread numeric-indexed properties into
3185
+ // the merged object, silently corrupting it. Explicitly reject arrays.
3186
+ const memoryInjection = {
3187
+ ...WECLONE_DEFAULTS.memoryInjection,
3188
+ ...(priorConfig &&
3189
+ typeof priorConfig.memoryInjection === "object" &&
3190
+ priorConfig.memoryInjection !== null &&
3191
+ !Array.isArray(priorConfig.memoryInjection)
3192
+ ? (priorConfig.memoryInjection as Record<string, unknown>)
3193
+ : {}),
3194
+ ...(typeof userConfig.memoryInjection === "object" &&
3195
+ userConfig.memoryInjection !== null &&
3196
+ !Array.isArray(userConfig.memoryInjection)
3197
+ ? (userConfig.memoryInjection as Record<string, unknown>)
3198
+ : {}),
3199
+ } as WeCloneProxyConfig["memoryInjection"];
3200
+
3201
+ const config: WeCloneProxyConfig = {
3202
+ wecloneApiUrl,
3203
+ wecloneModelName,
3204
+ proxyPort,
3205
+ remnicDaemonUrl,
3206
+ sessionStrategy,
3207
+ memoryInjection,
3208
+ };
3209
+
3210
+ // Token precedence: freshly minted token → user-supplied → prior saved.
3211
+ // Never write a token if none is available — the proxy tolerates missing
3212
+ // tokens (it just won't send Authorization headers to the daemon).
3213
+ if (authToken && authToken.length > 0) {
3214
+ config.remnicAuthToken = authToken;
3215
+ } else if (typeof userConfig.remnicAuthToken === "string" && userConfig.remnicAuthToken.length > 0) {
3216
+ config.remnicAuthToken = userConfig.remnicAuthToken;
3217
+ } else if (priorConfig && typeof priorConfig.remnicAuthToken === "string" && priorConfig.remnicAuthToken.length > 0) {
3218
+ config.remnicAuthToken = priorConfig.remnicAuthToken;
3219
+ }
3220
+
3221
+ return config;
3222
+ }