mirror-mind 0.0.1 → 0.33.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (474) hide show
  1. package/.claude/hooks/log-assistant-reply.sh +85 -0
  2. package/.claude/hooks/log-session-end.sh +85 -0
  3. package/.claude/hooks/log-user-prompt.sh +85 -0
  4. package/.claude/hooks/mirror-inject.sh +85 -0
  5. package/.claude/hooks/session-start.sh +85 -0
  6. package/.gemini/hooks/log-assistant.sh +85 -0
  7. package/.gemini/hooks/log-user.sh +85 -0
  8. package/.gemini/hooks/session-end.sh +85 -0
  9. package/.gemini/hooks/session-start.sh +85 -0
  10. package/.pi/extensions/mirror-logger.ts +646 -0
  11. package/.pi/skills/mm-backup/SKILL.md +21 -0
  12. package/.pi/skills/mm-build/SKILL.md +1206 -0
  13. package/.pi/skills/mm-consolidate/SKILL.md +112 -0
  14. package/.pi/skills/mm-consult/SKILL.md +70 -0
  15. package/.pi/skills/mm-conversations/SKILL.md +16 -0
  16. package/.pi/skills/mm-discard/SKILL.md +39 -0
  17. package/.pi/skills/mm-explore/SKILL.md +255 -0
  18. package/.pi/skills/mm-help/SKILL.md +74 -0
  19. package/.pi/skills/mm-identity/SKILL.md +96 -0
  20. package/.pi/skills/mm-journal/SKILL.md +16 -0
  21. package/.pi/skills/mm-journey/SKILL.md +23 -0
  22. package/.pi/skills/mm-journeys/SKILL.md +15 -0
  23. package/.pi/skills/mm-memories/SKILL.md +17 -0
  24. package/.pi/skills/mm-mirror/SKILL.md +98 -0
  25. package/.pi/skills/mm-mute/SKILL.md +18 -0
  26. package/.pi/skills/mm-new/SKILL.md +18 -0
  27. package/.pi/skills/mm-recall/SKILL.md +17 -0
  28. package/.pi/skills/mm-release-notes/SKILL.md +52 -0
  29. package/.pi/skills/mm-seed/SKILL.md +19 -0
  30. package/.pi/skills/mm-shadow/SKILL.md +117 -0
  31. package/.pi/skills/mm-soul/SKILL.md +502 -0
  32. package/.pi/skills/mm-tasks/SKILL.md +24 -0
  33. package/.pi/skills/mm-update/SKILL.md +32 -0
  34. package/.pi/skills/mm-week/SKILL.md +28 -0
  35. package/.pi/skills/mm-welcome/SKILL.md +24 -0
  36. package/AGENTS.md +188 -0
  37. package/README.md +126 -9
  38. package/bin/loader.js +67 -0
  39. package/bin/mirror-hook.js +7 -0
  40. package/bin/mirror.js +7 -0
  41. package/docs/releases/index.md +100 -0
  42. package/docs/releases/pending-cutoffs.md +510 -0
  43. package/docs/releases/v0.10.0.md +123 -0
  44. package/docs/releases/v0.10.1.md +66 -0
  45. package/docs/releases/v0.10.10.md +51 -0
  46. package/docs/releases/v0.10.11.md +39 -0
  47. package/docs/releases/v0.10.12.md +57 -0
  48. package/docs/releases/v0.10.13.md +35 -0
  49. package/docs/releases/v0.10.14.md +42 -0
  50. package/docs/releases/v0.10.15.md +53 -0
  51. package/docs/releases/v0.10.16.md +35 -0
  52. package/docs/releases/v0.10.17.md +43 -0
  53. package/docs/releases/v0.10.2.md +64 -0
  54. package/docs/releases/v0.10.3.md +78 -0
  55. package/docs/releases/v0.10.4.md +42 -0
  56. package/docs/releases/v0.10.5.md +57 -0
  57. package/docs/releases/v0.10.6.md +47 -0
  58. package/docs/releases/v0.10.7.md +53 -0
  59. package/docs/releases/v0.10.8.md +49 -0
  60. package/docs/releases/v0.10.9.md +37 -0
  61. package/docs/releases/v0.11.0.md +86 -0
  62. package/docs/releases/v0.12.0.md +67 -0
  63. package/docs/releases/v0.13.0.md +64 -0
  64. package/docs/releases/v0.14.0.md +68 -0
  65. package/docs/releases/v0.15.0.md +48 -0
  66. package/docs/releases/v0.16.0.md +50 -0
  67. package/docs/releases/v0.16.1.md +55 -0
  68. package/docs/releases/v0.16.2.md +36 -0
  69. package/docs/releases/v0.17.0.md +50 -0
  70. package/docs/releases/v0.17.1.md +58 -0
  71. package/docs/releases/v0.17.2.md +42 -0
  72. package/docs/releases/v0.17.3.md +58 -0
  73. package/docs/releases/v0.18.0.md +84 -0
  74. package/docs/releases/v0.18.1.md +51 -0
  75. package/docs/releases/v0.18.2.md +69 -0
  76. package/docs/releases/v0.19.0.md +85 -0
  77. package/docs/releases/v0.19.1.md +46 -0
  78. package/docs/releases/v0.20.0.md +85 -0
  79. package/docs/releases/v0.21.0.md +61 -0
  80. package/docs/releases/v0.21.1.md +47 -0
  81. package/docs/releases/v0.22.0.md +77 -0
  82. package/docs/releases/v0.22.1.md +42 -0
  83. package/docs/releases/v0.23.0.md +105 -0
  84. package/docs/releases/v0.24.0.md +125 -0
  85. package/docs/releases/v0.25.0.md +107 -0
  86. package/docs/releases/v0.26.0.md +139 -0
  87. package/docs/releases/v0.26.1.md +65 -0
  88. package/docs/releases/v0.26.2.md +43 -0
  89. package/docs/releases/v0.27.0.md +63 -0
  90. package/docs/releases/v0.28.0.md +59 -0
  91. package/docs/releases/v0.29.0.md +61 -0
  92. package/docs/releases/v0.29.1.md +59 -0
  93. package/docs/releases/v0.29.2.md +59 -0
  94. package/docs/releases/v0.29.3.md +93 -0
  95. package/docs/releases/v0.30.0.md +75 -0
  96. package/docs/releases/v0.30.1.md +93 -0
  97. package/docs/releases/v0.30.2.md +66 -0
  98. package/docs/releases/v0.31.0.md +122 -0
  99. package/docs/releases/v0.31.1.md +142 -0
  100. package/docs/releases/v0.31.10.md +118 -0
  101. package/docs/releases/v0.31.11.md +88 -0
  102. package/docs/releases/v0.31.12.md +105 -0
  103. package/docs/releases/v0.31.13.md +99 -0
  104. package/docs/releases/v0.31.14.md +97 -0
  105. package/docs/releases/v0.31.2.md +115 -0
  106. package/docs/releases/v0.31.3.md +142 -0
  107. package/docs/releases/v0.31.4.md +117 -0
  108. package/docs/releases/v0.31.5.md +155 -0
  109. package/docs/releases/v0.31.6.md +132 -0
  110. package/docs/releases/v0.31.7.md +132 -0
  111. package/docs/releases/v0.31.8.md +120 -0
  112. package/docs/releases/v0.31.9.md +120 -0
  113. package/docs/releases/v0.32.0.md +261 -0
  114. package/docs/releases/v0.32.1.md +59 -0
  115. package/docs/releases/v0.33.0.md +164 -0
  116. package/docs/releases/v0.8.0.md +118 -0
  117. package/docs/releases/v0.9.0.md +98 -0
  118. package/docs/releases/v0.9.1.md +83 -0
  119. package/package.json +102 -6
  120. package/plugins/mirror-mind/.claude-plugin/plugin.json +17 -0
  121. package/plugins/mirror-mind/hooks/hooks.json +62 -0
  122. package/plugins/mirror-mind/hooks/log-assistant-reply.sh +98 -0
  123. package/plugins/mirror-mind/hooks/log-session-end.sh +98 -0
  124. package/plugins/mirror-mind/hooks/log-user-prompt.sh +98 -0
  125. package/plugins/mirror-mind/hooks/mirror-inject.sh +98 -0
  126. package/plugins/mirror-mind/hooks/session-start.sh +98 -0
  127. package/plugins/mirror-mind/skills/mm-backup/SKILL.md +27 -0
  128. package/plugins/mirror-mind/skills/mm-build/SKILL.md +1206 -0
  129. package/plugins/mirror-mind/skills/mm-consolidate/SKILL.md +112 -0
  130. package/plugins/mirror-mind/skills/mm-consult/SKILL.md +83 -0
  131. package/plugins/mirror-mind/skills/mm-conversations/SKILL.md +17 -0
  132. package/plugins/mirror-mind/skills/mm-discard/SKILL.md +40 -0
  133. package/plugins/mirror-mind/skills/mm-explore/SKILL.md +241 -0
  134. package/plugins/mirror-mind/skills/mm-help/SKILL.md +104 -0
  135. package/plugins/mirror-mind/skills/mm-identity/SKILL.md +96 -0
  136. package/plugins/mirror-mind/skills/mm-journal/SKILL.md +21 -0
  137. package/plugins/mirror-mind/skills/mm-journey/SKILL.md +37 -0
  138. package/plugins/mirror-mind/skills/mm-journeys/SKILL.md +15 -0
  139. package/plugins/mirror-mind/skills/mm-memories/SKILL.md +20 -0
  140. package/plugins/mirror-mind/skills/mm-mirror/SKILL.md +67 -0
  141. package/plugins/mirror-mind/skills/mm-mute/SKILL.md +18 -0
  142. package/plugins/mirror-mind/skills/mm-new/SKILL.md +18 -0
  143. package/plugins/mirror-mind/skills/mm-recall/SKILL.md +17 -0
  144. package/plugins/mirror-mind/skills/mm-release-notes/SKILL.md +31 -0
  145. package/plugins/mirror-mind/skills/mm-seed/SKILL.md +15 -0
  146. package/plugins/mirror-mind/skills/mm-shadow/SKILL.md +117 -0
  147. package/plugins/mirror-mind/skills/mm-soul/SKILL.md +488 -0
  148. package/plugins/mirror-mind/skills/mm-tasks/SKILL.md +36 -0
  149. package/plugins/mirror-mind/skills/mm-update/SKILL.md +30 -0
  150. package/plugins/mirror-mind/skills/mm-week/SKILL.md +46 -0
  151. package/plugins/mirror-mind/skills/mm-welcome/SKILL.md +22 -0
  152. package/scripts/codex-hooks/session-end.sh +85 -0
  153. package/scripts/codex-hooks/session-start.sh +85 -0
  154. package/scripts/codex-mirror.sh +48 -0
  155. package/templates/identity/README.md +119 -0
  156. package/templates/identity/ego/behavior.yaml +52 -0
  157. package/templates/identity/ego/constraints.yaml +15 -0
  158. package/templates/identity/ego/identity.yaml +43 -0
  159. package/templates/identity/journeys/personal-growth.yaml +39 -0
  160. package/templates/identity/organization/identity.yaml +24 -0
  161. package/templates/identity/organization/principles.yaml +17 -0
  162. package/templates/identity/personas/coach.yaml +94 -0
  163. package/templates/identity/personas/designer.yaml +93 -0
  164. package/templates/identity/personas/doctor.yaml +96 -0
  165. package/templates/identity/personas/engineer.yaml +77 -0
  166. package/templates/identity/personas/financial.yaml +97 -0
  167. package/templates/identity/personas/prompt-engineer.yaml +122 -0
  168. package/templates/identity/personas/researcher.yaml +88 -0
  169. package/templates/identity/personas/strategist.yaml +91 -0
  170. package/templates/identity/personas/teacher.yaml +95 -0
  171. package/templates/identity/personas/therapist.yaml +91 -0
  172. package/templates/identity/personas/thinker.yaml +74 -0
  173. package/templates/identity/personas/writer.yaml +76 -0
  174. package/templates/identity/self/config.yaml +16 -0
  175. package/templates/identity/self/soul.yaml +105 -0
  176. package/templates/identity/user/identity.yaml +30 -0
  177. package/ts/src/backup/staging.ts +62 -0
  178. package/ts/src/backup/zipBackup.ts +267 -0
  179. package/ts/src/backup/zipReader.ts +109 -0
  180. package/ts/src/backup/zipWriter.ts +148 -0
  181. package/ts/src/builder/activeCheckpoint.ts +134 -0
  182. package/ts/src/builder/aggregateStatus.ts +34 -0
  183. package/ts/src/builder/approve.ts +149 -0
  184. package/ts/src/builder/argv.ts +292 -0
  185. package/ts/src/builder/ariadMethod.ts +536 -0
  186. package/ts/src/builder/artifacts/artifactSurfaces.ts +106 -0
  187. package/ts/src/builder/artifacts/artifactWriter.ts +113 -0
  188. package/ts/src/builder/artifacts/closureArtifacts.ts +199 -0
  189. package/ts/src/builder/artifacts/deliveryStoryArtifacts.ts +316 -0
  190. package/ts/src/builder/artifacts/planArtifacts.ts +242 -0
  191. package/ts/src/builder/artifacts/scaffoldSections.ts +273 -0
  192. package/ts/src/builder/artifacts/scaffoldState.ts +227 -0
  193. package/ts/src/builder/artifacts/storyFiles.ts +61 -0
  194. package/ts/src/builder/artifacts/storyIndex.ts +166 -0
  195. package/ts/src/builder/cadence.ts +59 -0
  196. package/ts/src/builder/card.ts +192 -0
  197. package/ts/src/builder/checkpointRefused.ts +66 -0
  198. package/ts/src/builder/closure.ts +964 -0
  199. package/ts/src/builder/commands.ts +2053 -0
  200. package/ts/src/builder/cursorPosition.ts +176 -0
  201. package/ts/src/builder/cursorTransitions.ts +77 -0
  202. package/ts/src/builder/deliveryCursor.ts +577 -0
  203. package/ts/src/builder/deliveryStoryClosure.ts +521 -0
  204. package/ts/src/builder/deliveryStoryPlan.ts +536 -0
  205. package/ts/src/builder/deliveryStoryReady.ts +160 -0
  206. package/ts/src/builder/deliveryStoryRoadmapClosure.ts +151 -0
  207. package/ts/src/builder/expand.ts +336 -0
  208. package/ts/src/builder/flowUnit.ts +339 -0
  209. package/ts/src/builder/homeSurface.ts +122 -0
  210. package/ts/src/builder/implementationGuard.ts +125 -0
  211. package/ts/src/builder/index.ts +11 -0
  212. package/ts/src/builder/lifecycleRefusal.ts +92 -0
  213. package/ts/src/builder/lifecycleRibbon.ts +159 -0
  214. package/ts/src/builder/load.ts +313 -0
  215. package/ts/src/builder/methodAdoption.ts +113 -0
  216. package/ts/src/builder/methodDefinition.ts +429 -0
  217. package/ts/src/builder/methodInspection.ts +371 -0
  218. package/ts/src/builder/plan.ts +408 -0
  219. package/ts/src/builder/planPreauthorization.ts +265 -0
  220. package/ts/src/builder/prepare.ts +225 -0
  221. package/ts/src/builder/projectPaths.ts +64 -0
  222. package/ts/src/builder/pull.ts +356 -0
  223. package/ts/src/builder/pullCandidates.ts +463 -0
  224. package/ts/src/builder/pullCandidatesRender.ts +238 -0
  225. package/ts/src/builder/refinementField.ts +93 -0
  226. package/ts/src/builder/releaseIntent.ts +173 -0
  227. package/ts/src/builder/resumeState.ts +84 -0
  228. package/ts/src/builder/resumeSurface.ts +149 -0
  229. package/ts/src/builder/roadmapGrammar.ts +300 -0
  230. package/ts/src/builder/roadmapScan.ts +97 -0
  231. package/ts/src/builder/roadmapScope.ts +210 -0
  232. package/ts/src/builder/scopePhrases.ts +146 -0
  233. package/ts/src/builder/storyPaths.ts +196 -0
  234. package/ts/src/builder/storyPlanPreauthorization.ts +287 -0
  235. package/ts/src/builder/surfaceProtocol.ts +42 -0
  236. package/ts/src/builder/templateGeneration.ts +148 -0
  237. package/ts/src/builder/transition.ts +178 -0
  238. package/ts/src/consult/args.ts +99 -0
  239. package/ts/src/consult/core.ts +122 -0
  240. package/ts/src/consult/modelCatalog.ts +55 -0
  241. package/ts/src/consult/render.ts +47 -0
  242. package/ts/src/conversation/append.ts +414 -0
  243. package/ts/src/conversation/backfill.ts +387 -0
  244. package/ts/src/conversation/closeTail.ts +467 -0
  245. package/ts/src/conversation/extraction.ts +333 -0
  246. package/ts/src/conversation/extractionDriver.ts +130 -0
  247. package/ts/src/conversation/extractionRun.ts +70 -0
  248. package/ts/src/conversation/hookPayload.ts +51 -0
  249. package/ts/src/conversation/journeyInference.ts +165 -0
  250. package/ts/src/conversation/journeyRepair.ts +114 -0
  251. package/ts/src/conversation/lifecycleFaces.ts +197 -0
  252. package/ts/src/conversation/lifecycleWrites.ts +313 -0
  253. package/ts/src/conversation/listing.ts +77 -0
  254. package/ts/src/conversation/logger.ts +851 -0
  255. package/ts/src/conversation/loggerCli.ts +321 -0
  256. package/ts/src/conversation/loggerRuntime.ts +259 -0
  257. package/ts/src/conversation/metadataLifecycle.ts +443 -0
  258. package/ts/src/conversation/recall.ts +66 -0
  259. package/ts/src/conversation/sessionComposites.ts +258 -0
  260. package/ts/src/conversation/sessionImport.ts +90 -0
  261. package/ts/src/conversation/transcriptBackfill.ts +141 -0
  262. package/ts/src/cultivation/applyActions.ts +241 -0
  263. package/ts/src/cultivation/cluster.ts +86 -0
  264. package/ts/src/cultivation/consolidationStore.ts +281 -0
  265. package/ts/src/cultivation/promptContext.ts +62 -0
  266. package/ts/src/cultivation/propose.ts +305 -0
  267. package/ts/src/cultivation/scan.ts +155 -0
  268. package/ts/src/db/backupGate.ts +47 -0
  269. package/ts/src/db/bootstrap.ts +108 -0
  270. package/ts/src/db/bootstrapLock.ts +223 -0
  271. package/ts/src/db/copyGuard.ts +60 -0
  272. package/ts/src/db/database.ts +397 -0
  273. package/ts/src/db/decode.ts +66 -0
  274. package/ts/src/db/ftsIntegrity.ts +41 -0
  275. package/ts/src/db/migrateOnOpen.ts +214 -0
  276. package/ts/src/db/migrations.ts +576 -0
  277. package/ts/src/db/readyOnOpen.ts +35 -0
  278. package/ts/src/db/rowDecode.ts +42 -0
  279. package/ts/src/db/schema.ts +432 -0
  280. package/ts/src/db/schemaInventory.ts +225 -0
  281. package/ts/src/db/schemaInventorySnapshot.ts +2107 -0
  282. package/ts/src/db/schemaState.ts +91 -0
  283. package/ts/src/db/schemaTsDivergence.ts +114 -0
  284. package/ts/src/descriptor/descriptorRead.ts +49 -0
  285. package/ts/src/docs/docsLint.ts +265 -0
  286. package/ts/src/explorer/handoff.ts +522 -0
  287. package/ts/src/explorer/render.ts +335 -0
  288. package/ts/src/explorer/story.ts +769 -0
  289. package/ts/src/explorer/transition.ts +72 -0
  290. package/ts/src/extensions/bindings.ts +170 -0
  291. package/ts/src/extensions/catalog.ts +249 -0
  292. package/ts/src/extensions/catalogCommands.ts +513 -0
  293. package/ts/src/extensions/catalogWrites.ts +652 -0
  294. package/ts/src/extensions/contextRuntime.ts +325 -0
  295. package/ts/src/extensions/dispatch.ts +374 -0
  296. package/ts/src/extensions/errors.ts +24 -0
  297. package/ts/src/extensions/manifest.ts +356 -0
  298. package/ts/src/extensions/migrations.ts +350 -0
  299. package/ts/src/extraction/conversation.ts +304 -0
  300. package/ts/src/extraction/conversationMetadata.ts +206 -0
  301. package/ts/src/extraction/fencing.ts +106 -0
  302. package/ts/src/extraction/json.ts +17 -0
  303. package/ts/src/extraction/prompts.ts +470 -0
  304. package/ts/src/frontDoor/args.ts +30 -0
  305. package/ts/src/frontDoor/argvShape.ts +106 -0
  306. package/ts/src/frontDoor/buildLoadRuntime.ts +139 -0
  307. package/ts/src/frontDoor/buildRoute.ts +398 -0
  308. package/ts/src/frontDoor/cli.ts +1872 -0
  309. package/ts/src/frontDoor/cloneRoleGuard.ts +98 -0
  310. package/ts/src/frontDoor/consultRoute.ts +50 -0
  311. package/ts/src/frontDoor/contentTailRoute.ts +239 -0
  312. package/ts/src/frontDoor/conversationLoggerRoute.ts +83 -0
  313. package/ts/src/frontDoor/cultivationRoute.ts +210 -0
  314. package/ts/src/frontDoor/dbPath.ts +131 -0
  315. package/ts/src/frontDoor/dbSafetyToolsRoute.ts +240 -0
  316. package/ts/src/frontDoor/exploreRoute.ts +572 -0
  317. package/ts/src/frontDoor/extensionCatalogRoute.ts +252 -0
  318. package/ts/src/frontDoor/frontDoorLog.ts +57 -0
  319. package/ts/src/frontDoor/identityWrite.ts +36 -0
  320. package/ts/src/frontDoor/journeyWriteRoute.ts +26 -0
  321. package/ts/src/frontDoor/lifecycleWriteRoute.ts +110 -0
  322. package/ts/src/frontDoor/liveBackup.ts +100 -0
  323. package/ts/src/frontDoor/mirrorModeRoute.ts +163 -0
  324. package/ts/src/frontDoor/namedRefusal.ts +35 -0
  325. package/ts/src/frontDoor/nodeSupport.ts +24 -0
  326. package/ts/src/frontDoor/render/consolidate.ts +199 -0
  327. package/ts/src/frontDoor/render/conversations.ts +33 -0
  328. package/ts/src/frontDoor/render/cultivationShared.ts +21 -0
  329. package/ts/src/frontDoor/render/descriptor.ts +20 -0
  330. package/ts/src/frontDoor/render/detectPersona.ts +50 -0
  331. package/ts/src/frontDoor/render/icons.ts +13 -0
  332. package/ts/src/frontDoor/render/identity.ts +59 -0
  333. package/ts/src/frontDoor/render/identityRows.ts +17 -0
  334. package/ts/src/frontDoor/render/inspectPersona.ts +53 -0
  335. package/ts/src/frontDoor/render/journeyStatus.ts +78 -0
  336. package/ts/src/frontDoor/render/journeys.ts +79 -0
  337. package/ts/src/frontDoor/render/list.ts +41 -0
  338. package/ts/src/frontDoor/render/memories.ts +67 -0
  339. package/ts/src/frontDoor/render/recall.ts +49 -0
  340. package/ts/src/frontDoor/render/shadow.ts +155 -0
  341. package/ts/src/frontDoor/render/tasks.ts +99 -0
  342. package/ts/src/frontDoor/render/tasksImportSync.ts +63 -0
  343. package/ts/src/frontDoor/render/week.ts +113 -0
  344. package/ts/src/frontDoor/routing.ts +1001 -0
  345. package/ts/src/frontDoor/runtimeRoute.ts +623 -0
  346. package/ts/src/frontDoor/searchRoute.ts +166 -0
  347. package/ts/src/frontDoor/seedPaths.ts +98 -0
  348. package/ts/src/frontDoor/sessionStartWelcomeRoute.ts +166 -0
  349. package/ts/src/frontDoor/soulRoute.ts +433 -0
  350. package/ts/src/frontDoor/stdinSource.ts +37 -0
  351. package/ts/src/frontDoor/tasksImportSyncRoute.ts +110 -0
  352. package/ts/src/frontDoor/tasksWriteRoute.ts +101 -0
  353. package/ts/src/frontDoor/usage.ts +176 -0
  354. package/ts/src/guards/claudePlugin.ts +298 -0
  355. package/ts/src/guards/packContents.ts +306 -0
  356. package/ts/src/guards/piSourcedSkills.ts +70 -0
  357. package/ts/src/guards/retiredRouteShadows.ts +57 -0
  358. package/ts/src/guards/retiredSurfaces.ts +564 -0
  359. package/ts/src/hooks/claude.ts +214 -0
  360. package/ts/src/hooks/codex.ts +45 -0
  361. package/ts/src/hooks/gemini.ts +126 -0
  362. package/ts/src/hooks/main.ts +109 -0
  363. package/ts/src/hooks/mirrorState.ts +71 -0
  364. package/ts/src/hooks/payload.ts +73 -0
  365. package/ts/src/hooks/runtime.ts +179 -0
  366. package/ts/src/hooks/runtimeDetection.ts +34 -0
  367. package/ts/src/identity/applyConsolidationIdentityUpdate.ts +67 -0
  368. package/ts/src/identity/descriptorGenerate.ts +162 -0
  369. package/ts/src/identity/identityEdit.ts +143 -0
  370. package/ts/src/identity/identityKey.ts +77 -0
  371. package/ts/src/identity/identityRead.ts +69 -0
  372. package/ts/src/identity/identityStore.ts +80 -0
  373. package/ts/src/identity/journeyListing.ts +53 -0
  374. package/ts/src/identity/personaInspect.ts +44 -0
  375. package/ts/src/identity/personaListing.ts +37 -0
  376. package/ts/src/identity/setIdentity.ts +64 -0
  377. package/ts/src/index.ts +231 -0
  378. package/ts/src/init/init.ts +111 -0
  379. package/ts/src/journey/journeyOptions.ts +131 -0
  380. package/ts/src/journey/journeyRemoval.ts +158 -0
  381. package/ts/src/journey/journeyStatus.ts +149 -0
  382. package/ts/src/journey/journeySyncFile.ts +87 -0
  383. package/ts/src/journey/journeyWrite.ts +203 -0
  384. package/ts/src/journey/parentJourney.ts +41 -0
  385. package/ts/src/journey/validateParentJourney.ts +55 -0
  386. package/ts/src/mcp/boundary.ts +151 -0
  387. package/ts/src/mcp/guards.ts +244 -0
  388. package/ts/src/mcp/main.ts +121 -0
  389. package/ts/src/mcp/payload.ts +141 -0
  390. package/ts/src/mcp/protocol.ts +141 -0
  391. package/ts/src/mcp/readModels.ts +241 -0
  392. package/ts/src/mcp/registry.ts +201 -0
  393. package/ts/src/mcp/serve.ts +120 -0
  394. package/ts/src/mcp/tools/deterministic.ts +137 -0
  395. package/ts/src/mcp/tools/providerCrossing.ts +187 -0
  396. package/ts/src/mcp/wire.ts +8 -0
  397. package/ts/src/memory/journal.ts +180 -0
  398. package/ts/src/memory/listing.ts +107 -0
  399. package/ts/src/memory/memoryWrite.ts +88 -0
  400. package/ts/src/memory/reinforcement.ts +35 -0
  401. package/ts/src/mirror/context.ts +161 -0
  402. package/ts/src/mirror/defaultResolution.ts +199 -0
  403. package/ts/src/mirror/mirrorForm.ts +43 -0
  404. package/ts/src/mirror/orchestration.ts +364 -0
  405. package/ts/src/mirror/reception.ts +194 -0
  406. package/ts/src/mirror/render.ts +75 -0
  407. package/ts/src/mirror/runtimeSession.ts +205 -0
  408. package/ts/src/mode/operatingMode.ts +148 -0
  409. package/ts/src/observability/callOutcome.ts +115 -0
  410. package/ts/src/observability/inspectLedger.ts +329 -0
  411. package/ts/src/observability/ledgerHooks.ts +126 -0
  412. package/ts/src/observability/llmCalls.ts +154 -0
  413. package/ts/src/persona/detectPersona.ts +81 -0
  414. package/ts/src/planning/promptAssembly.ts +111 -0
  415. package/ts/src/planning/weekPending.ts +81 -0
  416. package/ts/src/planning/weekPlan.ts +183 -0
  417. package/ts/src/planning/weekSave.ts +117 -0
  418. package/ts/src/providers/config.ts +189 -0
  419. package/ts/src/providers/cost.ts +64 -0
  420. package/ts/src/providers/credits.ts +204 -0
  421. package/ts/src/providers/embedding.ts +321 -0
  422. package/ts/src/providers/familyProviders.ts +147 -0
  423. package/ts/src/providers/llm.ts +334 -0
  424. package/ts/src/providers/openrouter.ts +244 -0
  425. package/ts/src/providers/redaction.ts +110 -0
  426. package/ts/src/providers/replay.ts +15 -0
  427. package/ts/src/providers/transport.ts +410 -0
  428. package/ts/src/repair/encodingRepair.ts +253 -0
  429. package/ts/src/runtime/backup.ts +151 -0
  430. package/ts/src/runtime/channel.ts +146 -0
  431. package/ts/src/runtime/config.ts +168 -0
  432. package/ts/src/runtime/diagnose.ts +686 -0
  433. package/ts/src/runtime/git.ts +510 -0
  434. package/ts/src/runtime/installKind.ts +220 -0
  435. package/ts/src/runtime/migrate.ts +154 -0
  436. package/ts/src/runtime/packageIdentity.ts +102 -0
  437. package/ts/src/runtime/releaseNotes.ts +244 -0
  438. package/ts/src/runtime/status.ts +557 -0
  439. package/ts/src/runtime/strategies/clone.ts +200 -0
  440. package/ts/src/runtime/strategies/package.ts +328 -0
  441. package/ts/src/runtime/treeRoot.ts +30 -0
  442. package/ts/src/runtime/update.ts +445 -0
  443. package/ts/src/runtime/updateGate.ts +40 -0
  444. package/ts/src/runtime/updatePipeline.ts +159 -0
  445. package/ts/src/runtime/version.ts +36 -0
  446. package/ts/src/runtime/wiring.ts +130 -0
  447. package/ts/src/search/memorySearch.ts +338 -0
  448. package/ts/src/search/ranker.ts +154 -0
  449. package/ts/src/seed/seed.ts +256 -0
  450. package/ts/src/seed/seedYaml.ts +133 -0
  451. package/ts/src/soul/apply.ts +218 -0
  452. package/ts/src/soul/harvest.ts +267 -0
  453. package/ts/src/soul/prompts/soul_beauty_voice.md +103 -0
  454. package/ts/src/soul/prompts/soul_self_voice.md +219 -0
  455. package/ts/src/soul/prompts/soul_wisdom_voice.md +121 -0
  456. package/ts/src/soul/prompts.ts +85 -0
  457. package/ts/src/soul/render.ts +358 -0
  458. package/ts/src/soul/state.ts +212 -0
  459. package/ts/src/soul/transition.ts +37 -0
  460. package/ts/src/tasks/journeyPathParse.ts +113 -0
  461. package/ts/src/tasks/taskImportSync.ts +142 -0
  462. package/ts/src/tasks/taskStore.ts +312 -0
  463. package/ts/src/tasks/weekView.ts +120 -0
  464. package/ts/src/util/clipCodePoints.ts +26 -0
  465. package/ts/src/util/paths.ts +63 -0
  466. package/ts/src/util/program.ts +9 -0
  467. package/ts/src/util/pyGenerators.ts +144 -0
  468. package/ts/src/util/pythonText.ts +317 -0
  469. package/ts/src/util/semver.ts +45 -0
  470. package/ts/src/util/shellWord.ts +25 -0
  471. package/ts/src/util/slug.ts +64 -0
  472. package/ts/src/welcome/card.ts +339 -0
  473. package/ts/src/welcome/statusLine.ts +101 -0
  474. package/ts/src/welcome/updateCache.ts +129 -0
@@ -0,0 +1,64 @@
1
+ /**
2
+ * The one cost authority for model-in-the-loop spend (AI-09 / CV9.E2.S13),
3
+ * ported from `memory/intelligence/cost.py` for CV22.DS8.US1.
4
+ *
5
+ * Token counts arrive with every provider response; prices are a static table;
6
+ * cost is a pure function of the two. Keeping this in one place is deliberate --
7
+ * scattering cost math across call sites is exactly the failure the audit warns
8
+ * about.
9
+ *
10
+ * CR040 deliberately did NOT port this: consult always logs the real fetched
11
+ * generation cost, so a static estimate would have been dead code. The live
12
+ * search embedding has no fetched figure -- Python prices that ledger row from
13
+ * this table -- so the port becomes load-bearing the moment the live provider
14
+ * lands. Graded against `ts/test/goldens/cost.golden.json`.
15
+ *
16
+ * Prices are **estimated** published list prices (USD per 1K tokens) for the
17
+ * pinned models, and drift over time. Update this table whenever the model pins
18
+ * change (AI-06 / CV9.E2.S12), on both engines in the same commit. An unknown
19
+ * model yields `null` -- never a silent `0` -- so unpriced spend stays visibly
20
+ * unpriced rather than looking free.
21
+ */
22
+
23
+ export interface ModelPrice {
24
+ /** USD per 1K prompt tokens. */
25
+ readonly promptPer1k: number;
26
+ /** USD per 1K completion tokens. */
27
+ readonly completionPer1k: number;
28
+ }
29
+
30
+ /** Estimated OpenRouter list prices (USD / 1K tokens). Verify on every pin change. */
31
+ export const MODEL_PRICES: Readonly<Record<string, ModelPrice>> = {
32
+ // Extraction pin -- google/gemini-2.5-flash-lite: ~$0.10/M in, ~$0.40/M out.
33
+ "google/gemini-2.5-flash-lite": { promptPer1k: 0.0001, completionPer1k: 0.0004 },
34
+ // Embedding pin -- openai/text-embedding-3-small: ~$0.02/M, no completion side.
35
+ "openai/text-embedding-3-small": { promptPer1k: 0.00002, completionPer1k: 0.0 },
36
+ };
37
+
38
+ /**
39
+ * Estimate the USD cost of one call from its model and token usage.
40
+ *
41
+ * Returns `null` when the model is not in the price table or when prompt usage
42
+ * is missing -- both mean the cost is genuinely unknown, and `null` keeps it out
43
+ * of any sum rather than understating spend as `0`. Missing completion tokens
44
+ * are treated as zero completion (some providers omit the field on empty
45
+ * completions, and every embedding call omits it by nature).
46
+ *
47
+ * `undefined` is folded into Python's `None`: TS call sites carry
48
+ * `number | null | undefined`, and both absences must mean "unknown", never
49
+ * "zero tokens".
50
+ *
51
+ * The arithmetic mirrors Python's operation order exactly -- `(prompt / 1000) *
52
+ * price + (completion / 1000) * price` -- because the golden asserts bit-equal
53
+ * doubles, not an epsilon.
54
+ */
55
+ export function computeCost(
56
+ model: string,
57
+ promptTokens: number | null | undefined,
58
+ completionTokens: number | null | undefined,
59
+ ): number | null {
60
+ const price = MODEL_PRICES[model];
61
+ if (price === undefined || promptTokens === null || promptTokens === undefined) return null;
62
+ const completion = completionTokens ?? 0;
63
+ return (promptTokens / 1000) * price.promptPer1k + (completion / 1000) * price.completionPer1k;
64
+ }
@@ -0,0 +1,204 @@
1
+ import { type ProviderConfig, resolveLlmTimeoutMs, resolveProviderConfig } from "./config.ts";
2
+ import { createOpenRouterClient, LlmTransportError, type OpenRouterClient } from "./openrouter.ts";
3
+ import { loadReplayFixture } from "./replay.ts";
4
+
5
+ export interface CreditInfo {
6
+ totalCredits: number;
7
+ totalUsage: number;
8
+ balance: number;
9
+ }
10
+
11
+ export interface CreditProvider {
12
+ getCredits(): Promise<CreditInfo>;
13
+ fetchGenerationCost(generationId: string): Promise<number | null>;
14
+ }
15
+
16
+ export interface ReplayCreditFixture {
17
+ kind: "credits";
18
+ credits: CreditInfo;
19
+ generationCosts?: Record<string, number | null>;
20
+ }
21
+
22
+ export class ReplayCreditProvider implements CreditProvider {
23
+ private readonly fixture: ReplayCreditFixture;
24
+
25
+ constructor(fixture: ReplayCreditFixture) {
26
+ this.fixture = fixture;
27
+ }
28
+
29
+ async getCredits(): Promise<CreditInfo> {
30
+ return this.fixture.credits;
31
+ }
32
+
33
+ async fetchGenerationCost(generationId: string): Promise<number | null> {
34
+ return this.fixture.generationCosts?.[generationId] ?? null;
35
+ }
36
+ }
37
+
38
+ export async function loadReplayCreditProvider(path: string): Promise<ReplayCreditProvider> {
39
+ const fixture = await loadReplayFixture(path);
40
+ assertReplayCreditFixture(fixture);
41
+ return new ReplayCreditProvider(fixture);
42
+ }
43
+
44
+ export function assertReplayCreditFixture(value: unknown): asserts value is ReplayCreditFixture {
45
+ if (!isRecord(value) || value.kind !== "credits") {
46
+ throw new Error("credits replay fixture must declare kind='credits'");
47
+ }
48
+ if (!isCreditInfo(value.credits)) {
49
+ throw new Error("credits replay fixture must include numeric credits");
50
+ }
51
+ if (value.generationCosts !== undefined && !isRecord(value.generationCosts)) {
52
+ throw new Error("credits replay fixture generationCosts must be an object");
53
+ }
54
+ }
55
+
56
+ function isCreditInfo(value: unknown): value is CreditInfo {
57
+ return (
58
+ isRecord(value) &&
59
+ typeof value.totalCredits === "number" &&
60
+ typeof value.totalUsage === "number" &&
61
+ typeof value.balance === "number"
62
+ );
63
+ }
64
+
65
+ function isRecord(value: unknown): value is Record<string, unknown> {
66
+ return typeof value === "object" && value !== null;
67
+ }
68
+
69
+ // --- Live credits and generation cost (CV22.DS8.US3) ------------------------
70
+
71
+ /** Python's `fetch_generation_cost(retries=4)`: attempts 0..4, five in all. */
72
+ const GENERATION_COST_ATTEMPTS = 5;
73
+
74
+ export interface LiveCreditProviderOptions {
75
+ env?: Record<string, string | undefined>;
76
+ /** Injectable for tests; no test in this repo may reach the network. */
77
+ createClient?: (config: ProviderConfig) => OpenRouterClient;
78
+ /** Injectable for tests so the generation poll does not really sleep. */
79
+ sleep?: (ms: number) => Promise<void>;
80
+ }
81
+
82
+ /**
83
+ * The live credit provider, mirroring Python's `get_credits` and
84
+ * `fetch_generation_cost`.
85
+ *
86
+ * Both are plain `urlopen` GETs in Python, and both are load-bearing for
87
+ * `consult`: the balance bar after every answer, and the real cost of the call
88
+ * that the ledger prefers over the static estimate.
89
+ *
90
+ * Two properties are worth stating because they are easy to get wrong:
91
+ *
92
+ * 1. **The generation poll is not a transport retry.** OpenRouter exposes
93
+ * usage a few seconds after the completion, so Python asks up to five
94
+ * times with 1/2/3/4s between attempts and swallows every error. Each
95
+ * attempt is therefore sent with `maxRetries: 0` -- letting the transport
96
+ * retry inside the poll would multiply the two budgets into ~15 requests
97
+ * for a number that is optional by design.
98
+ * 2. **Failure is `null`, never a throw.** Python returns None and the caller
99
+ * prints no cost line. A cost lookup must not be able to fail a command
100
+ * that already succeeded and was already paid for.
101
+ */
102
+ export class LiveCreditProvider implements CreditProvider {
103
+ private readonly env: Record<string, string | undefined>;
104
+ private readonly createClient: (config: ProviderConfig) => OpenRouterClient;
105
+ private readonly sleep: (ms: number) => Promise<void>;
106
+ private client: OpenRouterClient | null = null;
107
+
108
+ constructor(options: LiveCreditProviderOptions = {}) {
109
+ this.env = options.env ?? process.env;
110
+ this.createClient = options.createClient ?? ((config) => createOpenRouterClient(config));
111
+ this.sleep = options.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
112
+ }
113
+
114
+ async getCredits(): Promise<CreditInfo> {
115
+ const body = await this.resolveClient().getJson("/credits", {
116
+ timeoutMs: this.readTimeoutMs(),
117
+ // Python makes exactly one request here and lets the error surface.
118
+ maxRetries: 0,
119
+ });
120
+ const data = isRecord(body) && isRecord(body.data) ? body.data : null;
121
+ if (!data) {
122
+ // Python indexes `["data"]` unguarded: a response without it is a
123
+ // KeyError, not a zero balance. Reporting R$ 0.00 for a malformed
124
+ // response would read as "you are out of credits".
125
+ throw new LlmTransportError("malformed_output", "credits response carried no data object");
126
+ }
127
+ const totalCredits = readAmount(data.total_credits, "total_credits");
128
+ const totalUsage = readAmount(data.total_usage, "total_usage");
129
+ return { totalCredits, totalUsage, balance: totalCredits - totalUsage };
130
+ }
131
+
132
+ async fetchGenerationCost(generationId: string): Promise<number | null> {
133
+ // The id comes back from the provider and goes into a URL. Same provider,
134
+ // so the realistic risk is small -- but it is still untrusted input
135
+ // crossing into a request line, and Python would simply have built a
136
+ // broken URL and returned None (CV22.DS8.US3 plan review, security).
137
+ if (!isWellFormedGenerationId(generationId)) return null;
138
+ const path = `/generation?id=${encodeURIComponent(generationId)}`;
139
+
140
+ for (let attempt = 0; attempt < GENERATION_COST_ATTEMPTS; attempt += 1) {
141
+ // Python: `if attempt > 0: time.sleep(attempt)` -- 1s, 2s, 3s, 4s.
142
+ if (attempt > 0) await this.sleep(attempt * 1000);
143
+ const cost = await this.readGenerationCost(path);
144
+ if (cost !== null) return cost;
145
+ }
146
+ return null;
147
+ }
148
+
149
+ /** One poll attempt. Every failure is swallowed, exactly as Python's is. */
150
+ private async readGenerationCost(path: string): Promise<number | null> {
151
+ try {
152
+ const body = await this.resolveClient().getJson(path, {
153
+ timeoutMs: this.readTimeoutMs(),
154
+ maxRetries: 0,
155
+ });
156
+ const data = isRecord(body) && isRecord(body.data) ? body.data : {};
157
+ const cost = data.total_cost;
158
+ // Python: `if cost is not None: return float(cost)`. A genuine zero is a
159
+ // real answer, so truthiness would turn free calls into "unknown".
160
+ if (cost === null || cost === undefined) return null;
161
+ const parsed = Number(cost);
162
+ // `float(cost)` raises inside Python's try for a non-numeric string, so
163
+ // the loop simply continues.
164
+ return Number.isFinite(parsed) ? parsed : null;
165
+ } catch {
166
+ return null;
167
+ }
168
+ }
169
+
170
+ private resolveClient(): OpenRouterClient {
171
+ this.client ??= this.createClient(resolveProviderConfig("openrouter", { env: this.env }));
172
+ return this.client;
173
+ }
174
+
175
+ /**
176
+ * Python bounds neither GET. TypeScript bounds both at the embedding tier --
177
+ * the tier Python itself uses for its other cheap GET (`/models`) -- so a
178
+ * hung connection cannot park an interactive command. Recorded in
179
+ * `docs/project/decisions.md`.
180
+ */
181
+ private readTimeoutMs(): number {
182
+ return resolveLlmTimeoutMs("embedding", { env: this.env });
183
+ }
184
+ }
185
+
186
+ /**
187
+ * Python's `data.get("total_credits", 0)` followed by a subtraction: an absent
188
+ * field is zero, a present non-numeric field is a TypeError.
189
+ */
190
+ function readAmount(value: unknown, field: string): number {
191
+ if (value === undefined || value === null) return 0;
192
+ if (typeof value !== "number" || !Number.isFinite(value)) {
193
+ throw new LlmTransportError(
194
+ "malformed_output",
195
+ `credits response carried a non-numeric ${field}`,
196
+ );
197
+ }
198
+ return value;
199
+ }
200
+
201
+ /** Non-empty, and nothing that would have to be escaped out of a request line. */
202
+ function isWellFormedGenerationId(generationId: string): boolean {
203
+ return generationId.length > 0 && !/\s/.test(generationId);
204
+ }
@@ -0,0 +1,321 @@
1
+ import {
2
+ type ProviderConfig,
3
+ ProviderConfigError,
4
+ resolveEmbeddingModel,
5
+ resolveLlmMaxRetries,
6
+ resolveLlmTimeoutMs,
7
+ resolveProviderConfig,
8
+ } from "./config.ts";
9
+ import { createOpenRouterClient, LlmTransportError, type OpenRouterClient } from "./openrouter.ts";
10
+ import { loadReplayFixture } from "./replay.ts";
11
+
12
+ /**
13
+ * One embedding call's outcome: the vector, plus what it cost to get it.
14
+ *
15
+ * The usage travels with the vector rather than through a side channel so the
16
+ * ledger row can be priced the way Python prices it (`build_llm_logger` reads
17
+ * `response.usage.prompt_tokens`). `null` means the provider did not report
18
+ * usage -- genuinely unknown, never zero. This mirrors `LlmProvider.complete`,
19
+ * which has always returned tokens alongside content.
20
+ */
21
+ export interface EmbeddingResult {
22
+ vector: readonly number[];
23
+ promptTokens: number | null;
24
+ }
25
+
26
+ export interface EmbeddingProvider {
27
+ embed(text: string): Promise<EmbeddingResult>;
28
+ }
29
+
30
+ // Mirrors Python's EMBEDDING_DIMENSIONS / MEMORY_EMBEDDING_ATTEMPTS /
31
+ // MEMORY_EMBEDDING_RETRY_BACKOFF exactly (CV9.E2.S1, AI-06 pin family).
32
+ export const EMBEDDING_DIMENSIONS = 1536;
33
+ export const DEFAULT_EMBEDDING_ATTEMPTS = 3;
34
+ export const DEFAULT_EMBEDDING_RETRY_BACKOFF_MS = 500;
35
+
36
+ export class EmbeddingError extends Error {
37
+ readonly permanent: boolean;
38
+ constructor(message: string, options: { permanent?: boolean } = {}) {
39
+ super(message);
40
+ this.name = "EmbeddingError";
41
+ this.permanent = options.permanent ?? false;
42
+ }
43
+ }
44
+
45
+ export interface EmbeddingAttemptInfo {
46
+ text: string;
47
+ vector: readonly number[] | null;
48
+ latencyMs: number;
49
+ /** Usage for this round-trip, when the provider reported any (CV22.DS8.US1). */
50
+ promptTokens: number | null;
51
+ }
52
+
53
+ export interface GenerateEmbeddingSafelyOptions {
54
+ attempts?: number;
55
+ backoffMs?: number;
56
+ expectedDimensions?: number;
57
+ /** Injectable for fast, deterministic tests -- no real backoff wait. */
58
+ sleep?: (ms: number) => Promise<void>;
59
+ /** Fires once per real API round-trip (not for the empty-input guard,
60
+ * matching Python not logging before any call is attempted). Callers wire
61
+ * this to logLlmCall for ledger observability (AI-09/D-003). */
62
+ onAttempt?: (info: EmbeddingAttemptInfo) => void;
63
+ }
64
+
65
+ const defaultSleep = (ms: number): Promise<void> =>
66
+ new Promise((resolve) => setTimeout(resolve, ms));
67
+
68
+ /**
69
+ * Generate an embedding with Python's exact three-way failure taxonomy
70
+ * (`generate_embedding`, CV9.E2.S1):
71
+ *
72
+ * - empty/whitespace input -> permanent `EmbeddingError`, before any provider
73
+ * call (no doomed spend);
74
+ * - a well-formed call that resolves to an empty vector -> TRANSIENT, retried
75
+ * up to `attempts` with backoff;
76
+ * - a resolved vector of the wrong length -> PERMANENT, fails immediately
77
+ * with a `MEMORY_EMBEDDING_MODEL` diagnostic, never retried (a config error
78
+ * must never be mislabeled "failed after N attempts");
79
+ * - the provider call REJECTS -> TERMINAL, wrapped once, NOT retried at this
80
+ * layer. The underlying client/SDK is expected to have already retried
81
+ * transient transport failures itself (AI-01 philosophy); retrying here too
82
+ * would double the retry budget for exactly the failures already covered.
83
+ * This is the one branch most likely to be inverted by accident -- it is
84
+ * deliberately NOT the same as the empty-response transient case above.
85
+ *
86
+ * No fake or zero vector is ever returned: every path yields a validated
87
+ * embedding or throws.
88
+ */
89
+ export async function generateEmbeddingSafely(
90
+ provider: EmbeddingProvider,
91
+ text: string,
92
+ options: GenerateEmbeddingSafelyOptions = {},
93
+ ): Promise<readonly number[]> {
94
+ const attempts = options.attempts ?? DEFAULT_EMBEDDING_ATTEMPTS;
95
+ const backoffMs = options.backoffMs ?? DEFAULT_EMBEDDING_RETRY_BACKOFF_MS;
96
+ const expectedDimensions = options.expectedDimensions ?? EMBEDDING_DIMENSIONS;
97
+ const sleep = options.sleep ?? defaultSleep;
98
+
99
+ if (!text?.trim()) {
100
+ throw new EmbeddingError("Cannot embed empty text.", { permanent: true });
101
+ }
102
+
103
+ let lastError: EmbeddingError | undefined;
104
+ for (let attempt = 1; attempt <= attempts; attempt += 1) {
105
+ const t0 = Date.now();
106
+ let result: EmbeddingResult;
107
+ try {
108
+ result = await provider.embed(text);
109
+ } catch (error) {
110
+ // A missing/invalid key is a CONFIGURATION failure, not a call: Python
111
+ // raises before its attempt loop, so no `llm_calls` row exists for it.
112
+ // Firing onAttempt here would give TS an unpriced row where Python has
113
+ // none -- invisible in every replay test, because replay always has a
114
+ // "key" (CV22.DS8.US1 plan review, engineer).
115
+ if (error instanceof ProviderConfigError) throw error;
116
+ // Provider/transport exception: terminal at this layer, not retried.
117
+ options.onAttempt?.({ text, vector: null, latencyMs: Date.now() - t0, promptTokens: null });
118
+ const detail = error instanceof Error ? error.message : String(error);
119
+ throw new EmbeddingError(`Embedding provider call failed: ${detail}`);
120
+ }
121
+ const { vector, promptTokens } = result;
122
+ if (!vector || vector.length === 0) {
123
+ // Well-formed call, empty payload: transient, retry within budget.
124
+ options.onAttempt?.({ text, vector: null, latencyMs: Date.now() - t0, promptTokens });
125
+ lastError = new EmbeddingError("Empty embedding payload received");
126
+ if (attempt < attempts) await sleep(backoffMs * attempt);
127
+ continue;
128
+ }
129
+ if (vector.length !== expectedDimensions) {
130
+ options.onAttempt?.({ text, vector: null, latencyMs: Date.now() - t0, promptTokens });
131
+ throw new EmbeddingError(
132
+ `Embedding dimension mismatch: expected ${expectedDimensions}, got ${vector.length}. ` +
133
+ "Check MEMORY_EMBEDDING_MODEL -- a model whose vectors are not " +
134
+ `${expectedDimensions}-dim cannot be stored in this corpus.`,
135
+ { permanent: true },
136
+ );
137
+ }
138
+ options.onAttempt?.({ text, vector, latencyMs: Date.now() - t0, promptTokens });
139
+ return vector;
140
+ }
141
+ throw new EmbeddingError(
142
+ `No embedding generated after ${attempts} attempts: ${lastError?.message}`,
143
+ );
144
+ }
145
+
146
+ /** Provenance for a vector produced by the currently configured embedding pin
147
+ * (CV9.E2.S17, AI-07 shape-guard companion). Records the *configured* model,
148
+ * matching Python's own comment: equals the generation model unless the pin
149
+ * is hot-swapped mid-process. */
150
+ export function embeddingProvenance(): { embedding_model: string; embedding_dimensions: number } {
151
+ return { embedding_model: resolveEmbeddingModel(), embedding_dimensions: EMBEDDING_DIMENSIONS };
152
+ }
153
+
154
+ /**
155
+ * Merge current embedding provenance into a metadata JSON string. Foreign
156
+ * keys are preserved; provenance keys are authoritative (a re-embed must be
157
+ * able to overwrite a stale model). Never throws on malformed or non-object
158
+ * existing metadata -- falls back to a fresh object, so a bad metadata value
159
+ * can never fail a memory write (mirrors Python's add_embedding_provenance).
160
+ */
161
+ export function addEmbeddingProvenance(metadata: string | null | undefined): string {
162
+ let base: Record<string, unknown> = {};
163
+ if (metadata) {
164
+ try {
165
+ const parsed: unknown = JSON.parse(metadata);
166
+ if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
167
+ base = parsed as Record<string, unknown>;
168
+ }
169
+ } catch {
170
+ base = {};
171
+ }
172
+ }
173
+ return JSON.stringify({ ...base, ...embeddingProvenance() });
174
+ }
175
+
176
+ export interface ReplayEmbeddingFixture {
177
+ kind: "embedding";
178
+ request?: {
179
+ model?: string;
180
+ input_sha256?: string;
181
+ };
182
+ response: {
183
+ embedding: readonly number[];
184
+ /** Optional; DS5-era fixtures predate usage and stay valid without it. */
185
+ usage?: { prompt_tokens?: number };
186
+ };
187
+ }
188
+
189
+ export class ReplayEmbeddingProvider implements EmbeddingProvider {
190
+ private readonly fixture: ReplayEmbeddingFixture;
191
+
192
+ constructor(fixture: ReplayEmbeddingFixture) {
193
+ this.fixture = fixture;
194
+ }
195
+
196
+ async embed(_text: string): Promise<EmbeddingResult> {
197
+ // Fixtures may pin usage; most predate it. `null` is the honest answer --
198
+ // a replayed call cost nothing, so pricing it would be a fiction.
199
+ return {
200
+ vector: this.fixture.response.embedding,
201
+ promptTokens: this.fixture.response.usage?.prompt_tokens ?? null,
202
+ };
203
+ }
204
+ }
205
+
206
+ export interface LiveEmbeddingProviderOptions {
207
+ env?: Record<string, string | undefined>;
208
+ /** Injectable for tests; no test in this repo may reach the network. */
209
+ createClient?: (config: ProviderConfig) => OpenRouterClient;
210
+ }
211
+
212
+ /**
213
+ * The live embedding provider (CV22.DS8.US1).
214
+ *
215
+ * Two properties are load-bearing and easy to get backwards:
216
+ *
217
+ * 1. **Config resolves lazily**, on the first `embed()`. Resolving in the
218
+ * constructor would throw outside `searchMemoriesWithStatus`'s try block,
219
+ * crashing `memories --search` for an install with no key instead of
220
+ * degrading to lexical-only the way Python does.
221
+ * 2. **An empty payload returns an EMPTY VECTOR, it does not throw.** Python's
222
+ * `_extract_embedding` treats "no data"/"empty payload" as transient and
223
+ * lets `generate_embedding` retry; at the `generateEmbeddingSafely` layer a
224
+ * thrown error is TERMINAL. Throwing here would silently convert a
225
+ * retryable emptiness into a hard failure -- the exact taxonomy inversion
226
+ * the AI-18 contract exists to prevent.
227
+ *
228
+ * Dimension is deliberately NOT checked here: `generateEmbeddingSafely` owns
229
+ * the permanent mismatch diagnostic (AI-07), and duplicating it would give the
230
+ * corpus two places to disagree about what fits in it.
231
+ */
232
+ export class LiveEmbeddingProvider implements EmbeddingProvider {
233
+ private readonly env: Record<string, string | undefined>;
234
+ private readonly createClient: (config: ProviderConfig) => OpenRouterClient;
235
+ private client: OpenRouterClient | null = null;
236
+
237
+ constructor(options: LiveEmbeddingProviderOptions = {}) {
238
+ this.env = options.env ?? process.env;
239
+ this.createClient = options.createClient ?? ((config) => createOpenRouterClient(config));
240
+ }
241
+
242
+ async embed(text: string): Promise<EmbeddingResult> {
243
+ const client = this.resolveClient();
244
+ const body = await client.postJson(
245
+ "/embeddings",
246
+ { input: text, model: resolveEmbeddingModel({ env: this.env }) },
247
+ {
248
+ timeoutMs: resolveLlmTimeoutMs("embedding", { env: this.env }),
249
+ maxRetries: resolveLlmMaxRetries({ env: this.env }),
250
+ },
251
+ );
252
+ return interpretEmbeddingResponse(body);
253
+ }
254
+
255
+ private resolveClient(): OpenRouterClient {
256
+ // Throws ProviderConfigError when the key is absent -- inside embed(), so
257
+ // the caller's degraded path can catch it.
258
+ this.client ??= this.createClient(resolveProviderConfig("openrouter", { env: this.env }));
259
+ return this.client;
260
+ }
261
+ }
262
+
263
+ /** Mirrors Python's `_extract_embedding` branch for branch. */
264
+ function interpretEmbeddingResponse(body: unknown): EmbeddingResult {
265
+ if (!isRecord(body)) {
266
+ throw new LlmTransportError("malformed_output", "embedding response was not an object");
267
+ }
268
+ const promptTokens = readPromptTokens(body.usage);
269
+ const data = body.data;
270
+ // No `data` at all, or an empty list: transient (Python: "No embedding data
271
+ // received"). Reported as an empty vector so the retry budget applies.
272
+ if (data === undefined || data === null || (Array.isArray(data) && data.length === 0)) {
273
+ return { vector: [], promptTokens };
274
+ }
275
+ if (!Array.isArray(data) || !isRecord(data[0])) {
276
+ throw new LlmTransportError("malformed_output", "embedding response data was not a list");
277
+ }
278
+ const payload = data[0].embedding;
279
+ if (payload === undefined || payload === null || (Array.isArray(payload) && payload.length === 0))
280
+ return { vector: [], promptTokens };
281
+ if (!Array.isArray(payload)) {
282
+ throw new LlmTransportError("malformed_output", "embedding payload was not a list");
283
+ }
284
+ if (!payload.every((value) => typeof value === "number" && Number.isFinite(value))) {
285
+ // A NaN or a string in the vector would be written to the corpus as a BLOB
286
+ // and poison every future cosine against it.
287
+ throw new LlmTransportError("malformed_output", "embedding payload was not finite numbers");
288
+ }
289
+ return { vector: payload as readonly number[], promptTokens };
290
+ }
291
+
292
+ function readPromptTokens(usage: unknown): number | null {
293
+ if (!isRecord(usage)) return null;
294
+ const raw = usage.prompt_tokens;
295
+ return typeof raw === "number" && Number.isInteger(raw) ? raw : null;
296
+ }
297
+
298
+ export async function loadReplayEmbeddingProvider(path: string): Promise<ReplayEmbeddingProvider> {
299
+ const fixture = await loadReplayFixture(path);
300
+ assertReplayEmbeddingFixture(fixture);
301
+ return new ReplayEmbeddingProvider(fixture);
302
+ }
303
+
304
+ export function assertReplayEmbeddingFixture(
305
+ value: unknown,
306
+ ): asserts value is ReplayEmbeddingFixture {
307
+ if (!isRecord(value) || value.kind !== "embedding") {
308
+ throw new Error("embedding replay fixture must declare kind='embedding'");
309
+ }
310
+ const response = value.response;
311
+ if (!isRecord(response) || !Array.isArray(response.embedding)) {
312
+ throw new Error("embedding replay fixture must include response.embedding[]");
313
+ }
314
+ if (!response.embedding.every((item) => typeof item === "number" && Number.isFinite(item))) {
315
+ throw new Error("embedding replay fixture response.embedding must contain only finite numbers");
316
+ }
317
+ }
318
+
319
+ function isRecord(value: unknown): value is Record<string, unknown> {
320
+ return typeof value === "object" && value !== null;
321
+ }