mirror-mind 0.0.1 → 0.32.1

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 (467) hide show
  1. package/.claude/hooks/log-session-end.sh +85 -0
  2. package/.claude/hooks/log-user-prompt.sh +85 -0
  3. package/.claude/hooks/mirror-inject.sh +85 -0
  4. package/.claude/hooks/session-start.sh +85 -0
  5. package/.gemini/hooks/log-assistant.sh +85 -0
  6. package/.gemini/hooks/log-user.sh +85 -0
  7. package/.gemini/hooks/session-end.sh +85 -0
  8. package/.gemini/hooks/session-start.sh +85 -0
  9. package/.pi/extensions/mirror-logger.ts +561 -0
  10. package/.pi/skills/mm-backup/SKILL.md +21 -0
  11. package/.pi/skills/mm-build/SKILL.md +1206 -0
  12. package/.pi/skills/mm-consolidate/SKILL.md +112 -0
  13. package/.pi/skills/mm-consult/SKILL.md +70 -0
  14. package/.pi/skills/mm-conversations/SKILL.md +16 -0
  15. package/.pi/skills/mm-discard/SKILL.md +39 -0
  16. package/.pi/skills/mm-explore/SKILL.md +255 -0
  17. package/.pi/skills/mm-help/SKILL.md +74 -0
  18. package/.pi/skills/mm-identity/SKILL.md +96 -0
  19. package/.pi/skills/mm-journal/SKILL.md +16 -0
  20. package/.pi/skills/mm-journey/SKILL.md +23 -0
  21. package/.pi/skills/mm-journeys/SKILL.md +15 -0
  22. package/.pi/skills/mm-memories/SKILL.md +17 -0
  23. package/.pi/skills/mm-mirror/SKILL.md +98 -0
  24. package/.pi/skills/mm-mute/SKILL.md +18 -0
  25. package/.pi/skills/mm-new/SKILL.md +18 -0
  26. package/.pi/skills/mm-recall/SKILL.md +17 -0
  27. package/.pi/skills/mm-release-notes/SKILL.md +52 -0
  28. package/.pi/skills/mm-seed/SKILL.md +19 -0
  29. package/.pi/skills/mm-shadow/SKILL.md +117 -0
  30. package/.pi/skills/mm-soul/SKILL.md +502 -0
  31. package/.pi/skills/mm-tasks/SKILL.md +24 -0
  32. package/.pi/skills/mm-update/SKILL.md +32 -0
  33. package/.pi/skills/mm-week/SKILL.md +28 -0
  34. package/.pi/skills/mm-welcome/SKILL.md +24 -0
  35. package/AGENTS.md +188 -0
  36. package/README.md +126 -9
  37. package/bin/loader.js +67 -0
  38. package/bin/mirror-hook.js +7 -0
  39. package/bin/mirror.js +7 -0
  40. package/docs/releases/index.md +99 -0
  41. package/docs/releases/pending-cutoffs.md +510 -0
  42. package/docs/releases/v0.10.0.md +123 -0
  43. package/docs/releases/v0.10.1.md +66 -0
  44. package/docs/releases/v0.10.10.md +51 -0
  45. package/docs/releases/v0.10.11.md +39 -0
  46. package/docs/releases/v0.10.12.md +57 -0
  47. package/docs/releases/v0.10.13.md +35 -0
  48. package/docs/releases/v0.10.14.md +42 -0
  49. package/docs/releases/v0.10.15.md +53 -0
  50. package/docs/releases/v0.10.16.md +35 -0
  51. package/docs/releases/v0.10.17.md +43 -0
  52. package/docs/releases/v0.10.2.md +64 -0
  53. package/docs/releases/v0.10.3.md +78 -0
  54. package/docs/releases/v0.10.4.md +42 -0
  55. package/docs/releases/v0.10.5.md +57 -0
  56. package/docs/releases/v0.10.6.md +47 -0
  57. package/docs/releases/v0.10.7.md +53 -0
  58. package/docs/releases/v0.10.8.md +49 -0
  59. package/docs/releases/v0.10.9.md +37 -0
  60. package/docs/releases/v0.11.0.md +86 -0
  61. package/docs/releases/v0.12.0.md +67 -0
  62. package/docs/releases/v0.13.0.md +64 -0
  63. package/docs/releases/v0.14.0.md +68 -0
  64. package/docs/releases/v0.15.0.md +48 -0
  65. package/docs/releases/v0.16.0.md +50 -0
  66. package/docs/releases/v0.16.1.md +55 -0
  67. package/docs/releases/v0.16.2.md +36 -0
  68. package/docs/releases/v0.17.0.md +50 -0
  69. package/docs/releases/v0.17.1.md +58 -0
  70. package/docs/releases/v0.17.2.md +42 -0
  71. package/docs/releases/v0.17.3.md +58 -0
  72. package/docs/releases/v0.18.0.md +84 -0
  73. package/docs/releases/v0.18.1.md +51 -0
  74. package/docs/releases/v0.18.2.md +69 -0
  75. package/docs/releases/v0.19.0.md +85 -0
  76. package/docs/releases/v0.19.1.md +46 -0
  77. package/docs/releases/v0.20.0.md +85 -0
  78. package/docs/releases/v0.21.0.md +61 -0
  79. package/docs/releases/v0.21.1.md +47 -0
  80. package/docs/releases/v0.22.0.md +77 -0
  81. package/docs/releases/v0.22.1.md +42 -0
  82. package/docs/releases/v0.23.0.md +105 -0
  83. package/docs/releases/v0.24.0.md +125 -0
  84. package/docs/releases/v0.25.0.md +107 -0
  85. package/docs/releases/v0.26.0.md +139 -0
  86. package/docs/releases/v0.26.1.md +65 -0
  87. package/docs/releases/v0.26.2.md +43 -0
  88. package/docs/releases/v0.27.0.md +63 -0
  89. package/docs/releases/v0.28.0.md +59 -0
  90. package/docs/releases/v0.29.0.md +61 -0
  91. package/docs/releases/v0.29.1.md +59 -0
  92. package/docs/releases/v0.29.2.md +59 -0
  93. package/docs/releases/v0.29.3.md +93 -0
  94. package/docs/releases/v0.30.0.md +75 -0
  95. package/docs/releases/v0.30.1.md +93 -0
  96. package/docs/releases/v0.30.2.md +66 -0
  97. package/docs/releases/v0.31.0.md +122 -0
  98. package/docs/releases/v0.31.1.md +142 -0
  99. package/docs/releases/v0.31.10.md +118 -0
  100. package/docs/releases/v0.31.11.md +88 -0
  101. package/docs/releases/v0.31.12.md +105 -0
  102. package/docs/releases/v0.31.13.md +99 -0
  103. package/docs/releases/v0.31.14.md +97 -0
  104. package/docs/releases/v0.31.2.md +115 -0
  105. package/docs/releases/v0.31.3.md +142 -0
  106. package/docs/releases/v0.31.4.md +117 -0
  107. package/docs/releases/v0.31.5.md +155 -0
  108. package/docs/releases/v0.31.6.md +132 -0
  109. package/docs/releases/v0.31.7.md +132 -0
  110. package/docs/releases/v0.31.8.md +120 -0
  111. package/docs/releases/v0.31.9.md +120 -0
  112. package/docs/releases/v0.32.0.md +261 -0
  113. package/docs/releases/v0.32.1.md +59 -0
  114. package/docs/releases/v0.8.0.md +118 -0
  115. package/docs/releases/v0.9.0.md +98 -0
  116. package/docs/releases/v0.9.1.md +83 -0
  117. package/package.json +102 -6
  118. package/plugins/mirror-mind/.claude-plugin/plugin.json +13 -0
  119. package/plugins/mirror-mind/hooks/hooks.json +50 -0
  120. package/plugins/mirror-mind/hooks/log-session-end.sh +98 -0
  121. package/plugins/mirror-mind/hooks/log-user-prompt.sh +98 -0
  122. package/plugins/mirror-mind/hooks/mirror-inject.sh +98 -0
  123. package/plugins/mirror-mind/hooks/session-start.sh +98 -0
  124. package/plugins/mirror-mind/mcp/launch.sh +52 -0
  125. package/plugins/mirror-mind/skills/mm-backup/SKILL.md +27 -0
  126. package/plugins/mirror-mind/skills/mm-build/SKILL.md +1206 -0
  127. package/plugins/mirror-mind/skills/mm-consolidate/SKILL.md +112 -0
  128. package/plugins/mirror-mind/skills/mm-consult/SKILL.md +83 -0
  129. package/plugins/mirror-mind/skills/mm-conversations/SKILL.md +17 -0
  130. package/plugins/mirror-mind/skills/mm-discard/SKILL.md +40 -0
  131. package/plugins/mirror-mind/skills/mm-explore/SKILL.md +241 -0
  132. package/plugins/mirror-mind/skills/mm-help/SKILL.md +104 -0
  133. package/plugins/mirror-mind/skills/mm-identity/SKILL.md +96 -0
  134. package/plugins/mirror-mind/skills/mm-journal/SKILL.md +21 -0
  135. package/plugins/mirror-mind/skills/mm-journey/SKILL.md +37 -0
  136. package/plugins/mirror-mind/skills/mm-journeys/SKILL.md +15 -0
  137. package/plugins/mirror-mind/skills/mm-memories/SKILL.md +20 -0
  138. package/plugins/mirror-mind/skills/mm-mirror/SKILL.md +65 -0
  139. package/plugins/mirror-mind/skills/mm-mute/SKILL.md +18 -0
  140. package/plugins/mirror-mind/skills/mm-new/SKILL.md +18 -0
  141. package/plugins/mirror-mind/skills/mm-recall/SKILL.md +17 -0
  142. package/plugins/mirror-mind/skills/mm-release-notes/SKILL.md +31 -0
  143. package/plugins/mirror-mind/skills/mm-seed/SKILL.md +15 -0
  144. package/plugins/mirror-mind/skills/mm-shadow/SKILL.md +117 -0
  145. package/plugins/mirror-mind/skills/mm-soul/SKILL.md +488 -0
  146. package/plugins/mirror-mind/skills/mm-tasks/SKILL.md +36 -0
  147. package/plugins/mirror-mind/skills/mm-update/SKILL.md +30 -0
  148. package/plugins/mirror-mind/skills/mm-week/SKILL.md +46 -0
  149. package/plugins/mirror-mind/skills/mm-welcome/SKILL.md +22 -0
  150. package/scripts/codex-hooks/session-end.sh +85 -0
  151. package/scripts/codex-hooks/session-start.sh +85 -0
  152. package/scripts/codex-mirror.sh +48 -0
  153. package/templates/identity/README.md +119 -0
  154. package/templates/identity/ego/behavior.yaml +52 -0
  155. package/templates/identity/ego/constraints.yaml +15 -0
  156. package/templates/identity/ego/identity.yaml +43 -0
  157. package/templates/identity/journeys/personal-growth.yaml +39 -0
  158. package/templates/identity/organization/identity.yaml +24 -0
  159. package/templates/identity/organization/principles.yaml +17 -0
  160. package/templates/identity/personas/coach.yaml +94 -0
  161. package/templates/identity/personas/designer.yaml +93 -0
  162. package/templates/identity/personas/doctor.yaml +96 -0
  163. package/templates/identity/personas/engineer.yaml +77 -0
  164. package/templates/identity/personas/financial.yaml +97 -0
  165. package/templates/identity/personas/prompt-engineer.yaml +122 -0
  166. package/templates/identity/personas/researcher.yaml +88 -0
  167. package/templates/identity/personas/strategist.yaml +91 -0
  168. package/templates/identity/personas/teacher.yaml +95 -0
  169. package/templates/identity/personas/therapist.yaml +91 -0
  170. package/templates/identity/personas/thinker.yaml +74 -0
  171. package/templates/identity/personas/writer.yaml +76 -0
  172. package/templates/identity/self/config.yaml +16 -0
  173. package/templates/identity/self/soul.yaml +105 -0
  174. package/templates/identity/user/identity.yaml +30 -0
  175. package/ts/src/backup/staging.ts +62 -0
  176. package/ts/src/backup/zipBackup.ts +267 -0
  177. package/ts/src/backup/zipReader.ts +109 -0
  178. package/ts/src/backup/zipWriter.ts +148 -0
  179. package/ts/src/builder/activeCheckpoint.ts +134 -0
  180. package/ts/src/builder/aggregateStatus.ts +34 -0
  181. package/ts/src/builder/approve.ts +149 -0
  182. package/ts/src/builder/argv.ts +292 -0
  183. package/ts/src/builder/ariadMethod.ts +536 -0
  184. package/ts/src/builder/artifacts/artifactSurfaces.ts +106 -0
  185. package/ts/src/builder/artifacts/artifactWriter.ts +113 -0
  186. package/ts/src/builder/artifacts/closureArtifacts.ts +199 -0
  187. package/ts/src/builder/artifacts/deliveryStoryArtifacts.ts +316 -0
  188. package/ts/src/builder/artifacts/planArtifacts.ts +242 -0
  189. package/ts/src/builder/artifacts/scaffoldSections.ts +273 -0
  190. package/ts/src/builder/artifacts/scaffoldState.ts +227 -0
  191. package/ts/src/builder/artifacts/storyFiles.ts +61 -0
  192. package/ts/src/builder/artifacts/storyIndex.ts +166 -0
  193. package/ts/src/builder/cadence.ts +59 -0
  194. package/ts/src/builder/card.ts +192 -0
  195. package/ts/src/builder/checkpointRefused.ts +66 -0
  196. package/ts/src/builder/closure.ts +964 -0
  197. package/ts/src/builder/commands.ts +2053 -0
  198. package/ts/src/builder/cursorPosition.ts +176 -0
  199. package/ts/src/builder/cursorTransitions.ts +77 -0
  200. package/ts/src/builder/deliveryCursor.ts +577 -0
  201. package/ts/src/builder/deliveryStoryClosure.ts +521 -0
  202. package/ts/src/builder/deliveryStoryPlan.ts +536 -0
  203. package/ts/src/builder/deliveryStoryReady.ts +160 -0
  204. package/ts/src/builder/deliveryStoryRoadmapClosure.ts +151 -0
  205. package/ts/src/builder/expand.ts +336 -0
  206. package/ts/src/builder/flowUnit.ts +339 -0
  207. package/ts/src/builder/homeSurface.ts +122 -0
  208. package/ts/src/builder/implementationGuard.ts +125 -0
  209. package/ts/src/builder/index.ts +11 -0
  210. package/ts/src/builder/lifecycleRefusal.ts +92 -0
  211. package/ts/src/builder/lifecycleRibbon.ts +159 -0
  212. package/ts/src/builder/load.ts +313 -0
  213. package/ts/src/builder/methodAdoption.ts +113 -0
  214. package/ts/src/builder/methodDefinition.ts +429 -0
  215. package/ts/src/builder/methodInspection.ts +371 -0
  216. package/ts/src/builder/plan.ts +408 -0
  217. package/ts/src/builder/planPreauthorization.ts +265 -0
  218. package/ts/src/builder/prepare.ts +225 -0
  219. package/ts/src/builder/projectPaths.ts +64 -0
  220. package/ts/src/builder/pull.ts +356 -0
  221. package/ts/src/builder/pullCandidates.ts +463 -0
  222. package/ts/src/builder/pullCandidatesRender.ts +238 -0
  223. package/ts/src/builder/refinementField.ts +93 -0
  224. package/ts/src/builder/releaseIntent.ts +173 -0
  225. package/ts/src/builder/resumeState.ts +84 -0
  226. package/ts/src/builder/resumeSurface.ts +149 -0
  227. package/ts/src/builder/roadmapGrammar.ts +300 -0
  228. package/ts/src/builder/roadmapScan.ts +97 -0
  229. package/ts/src/builder/roadmapScope.ts +210 -0
  230. package/ts/src/builder/scopePhrases.ts +146 -0
  231. package/ts/src/builder/storyPaths.ts +196 -0
  232. package/ts/src/builder/storyPlanPreauthorization.ts +287 -0
  233. package/ts/src/builder/surfaceProtocol.ts +42 -0
  234. package/ts/src/builder/templateGeneration.ts +148 -0
  235. package/ts/src/builder/transition.ts +178 -0
  236. package/ts/src/consult/args.ts +99 -0
  237. package/ts/src/consult/core.ts +122 -0
  238. package/ts/src/consult/modelCatalog.ts +55 -0
  239. package/ts/src/consult/render.ts +47 -0
  240. package/ts/src/conversation/append.ts +414 -0
  241. package/ts/src/conversation/backfill.ts +387 -0
  242. package/ts/src/conversation/closeTail.ts +467 -0
  243. package/ts/src/conversation/extraction.ts +333 -0
  244. package/ts/src/conversation/extractionDriver.ts +130 -0
  245. package/ts/src/conversation/extractionRun.ts +70 -0
  246. package/ts/src/conversation/journeyInference.ts +165 -0
  247. package/ts/src/conversation/journeyRepair.ts +114 -0
  248. package/ts/src/conversation/lifecycleFaces.ts +197 -0
  249. package/ts/src/conversation/lifecycleWrites.ts +313 -0
  250. package/ts/src/conversation/listing.ts +77 -0
  251. package/ts/src/conversation/logger.ts +576 -0
  252. package/ts/src/conversation/loggerCli.ts +282 -0
  253. package/ts/src/conversation/loggerRuntime.ts +259 -0
  254. package/ts/src/conversation/metadataLifecycle.ts +443 -0
  255. package/ts/src/conversation/recall.ts +66 -0
  256. package/ts/src/conversation/sessionComposites.ts +258 -0
  257. package/ts/src/conversation/sessionImport.ts +90 -0
  258. package/ts/src/conversation/transcriptBackfill.ts +141 -0
  259. package/ts/src/cultivation/applyActions.ts +241 -0
  260. package/ts/src/cultivation/cluster.ts +86 -0
  261. package/ts/src/cultivation/consolidationStore.ts +281 -0
  262. package/ts/src/cultivation/promptContext.ts +62 -0
  263. package/ts/src/cultivation/propose.ts +305 -0
  264. package/ts/src/cultivation/scan.ts +155 -0
  265. package/ts/src/db/backupGate.ts +47 -0
  266. package/ts/src/db/bootstrap.ts +108 -0
  267. package/ts/src/db/bootstrapLock.ts +223 -0
  268. package/ts/src/db/copyGuard.ts +60 -0
  269. package/ts/src/db/database.ts +397 -0
  270. package/ts/src/db/decode.ts +66 -0
  271. package/ts/src/db/ftsIntegrity.ts +41 -0
  272. package/ts/src/db/migrateOnOpen.ts +214 -0
  273. package/ts/src/db/migrations.ts +576 -0
  274. package/ts/src/db/readyOnOpen.ts +35 -0
  275. package/ts/src/db/rowDecode.ts +42 -0
  276. package/ts/src/db/schema.ts +432 -0
  277. package/ts/src/db/schemaInventory.ts +225 -0
  278. package/ts/src/db/schemaInventorySnapshot.ts +2107 -0
  279. package/ts/src/db/schemaState.ts +91 -0
  280. package/ts/src/db/schemaTsDivergence.ts +114 -0
  281. package/ts/src/descriptor/descriptorRead.ts +49 -0
  282. package/ts/src/docs/docsLint.ts +265 -0
  283. package/ts/src/explorer/handoff.ts +522 -0
  284. package/ts/src/explorer/render.ts +335 -0
  285. package/ts/src/explorer/story.ts +769 -0
  286. package/ts/src/explorer/transition.ts +72 -0
  287. package/ts/src/extensions/bindings.ts +170 -0
  288. package/ts/src/extensions/catalog.ts +249 -0
  289. package/ts/src/extensions/catalogCommands.ts +513 -0
  290. package/ts/src/extensions/catalogWrites.ts +652 -0
  291. package/ts/src/extensions/contextRuntime.ts +325 -0
  292. package/ts/src/extensions/dispatch.ts +374 -0
  293. package/ts/src/extensions/errors.ts +24 -0
  294. package/ts/src/extensions/manifest.ts +356 -0
  295. package/ts/src/extensions/migrations.ts +350 -0
  296. package/ts/src/extraction/conversation.ts +304 -0
  297. package/ts/src/extraction/conversationMetadata.ts +206 -0
  298. package/ts/src/extraction/fencing.ts +106 -0
  299. package/ts/src/extraction/json.ts +17 -0
  300. package/ts/src/extraction/prompts.ts +470 -0
  301. package/ts/src/frontDoor/args.ts +30 -0
  302. package/ts/src/frontDoor/argvShape.ts +106 -0
  303. package/ts/src/frontDoor/buildLoadRuntime.ts +139 -0
  304. package/ts/src/frontDoor/buildRoute.ts +398 -0
  305. package/ts/src/frontDoor/cli.ts +1865 -0
  306. package/ts/src/frontDoor/cloneRoleGuard.ts +98 -0
  307. package/ts/src/frontDoor/consultRoute.ts +50 -0
  308. package/ts/src/frontDoor/contentTailRoute.ts +239 -0
  309. package/ts/src/frontDoor/conversationLoggerRoute.ts +73 -0
  310. package/ts/src/frontDoor/cultivationRoute.ts +210 -0
  311. package/ts/src/frontDoor/dbPath.ts +131 -0
  312. package/ts/src/frontDoor/dbSafetyToolsRoute.ts +240 -0
  313. package/ts/src/frontDoor/exploreRoute.ts +572 -0
  314. package/ts/src/frontDoor/extensionCatalogRoute.ts +252 -0
  315. package/ts/src/frontDoor/frontDoorLog.ts +57 -0
  316. package/ts/src/frontDoor/identityWrite.ts +36 -0
  317. package/ts/src/frontDoor/journeyWriteRoute.ts +26 -0
  318. package/ts/src/frontDoor/lifecycleWriteRoute.ts +110 -0
  319. package/ts/src/frontDoor/liveBackup.ts +100 -0
  320. package/ts/src/frontDoor/mirrorModeRoute.ts +163 -0
  321. package/ts/src/frontDoor/namedRefusal.ts +35 -0
  322. package/ts/src/frontDoor/nodeSupport.ts +24 -0
  323. package/ts/src/frontDoor/render/consolidate.ts +199 -0
  324. package/ts/src/frontDoor/render/conversations.ts +33 -0
  325. package/ts/src/frontDoor/render/cultivationShared.ts +21 -0
  326. package/ts/src/frontDoor/render/descriptor.ts +20 -0
  327. package/ts/src/frontDoor/render/detectPersona.ts +50 -0
  328. package/ts/src/frontDoor/render/icons.ts +13 -0
  329. package/ts/src/frontDoor/render/identity.ts +59 -0
  330. package/ts/src/frontDoor/render/identityRows.ts +17 -0
  331. package/ts/src/frontDoor/render/inspectPersona.ts +53 -0
  332. package/ts/src/frontDoor/render/journeyStatus.ts +78 -0
  333. package/ts/src/frontDoor/render/journeys.ts +79 -0
  334. package/ts/src/frontDoor/render/list.ts +41 -0
  335. package/ts/src/frontDoor/render/memories.ts +67 -0
  336. package/ts/src/frontDoor/render/recall.ts +49 -0
  337. package/ts/src/frontDoor/render/shadow.ts +155 -0
  338. package/ts/src/frontDoor/render/tasks.ts +99 -0
  339. package/ts/src/frontDoor/render/tasksImportSync.ts +63 -0
  340. package/ts/src/frontDoor/render/week.ts +113 -0
  341. package/ts/src/frontDoor/routing.ts +999 -0
  342. package/ts/src/frontDoor/runtimeRoute.ts +623 -0
  343. package/ts/src/frontDoor/searchRoute.ts +166 -0
  344. package/ts/src/frontDoor/seedPaths.ts +98 -0
  345. package/ts/src/frontDoor/soulRoute.ts +433 -0
  346. package/ts/src/frontDoor/tasksImportSyncRoute.ts +110 -0
  347. package/ts/src/frontDoor/tasksWriteRoute.ts +101 -0
  348. package/ts/src/frontDoor/usage.ts +176 -0
  349. package/ts/src/guards/claudePlugin.ts +252 -0
  350. package/ts/src/guards/packContents.ts +306 -0
  351. package/ts/src/guards/piSourcedSkills.ts +70 -0
  352. package/ts/src/guards/retiredRouteShadows.ts +57 -0
  353. package/ts/src/guards/retiredSurfaces.ts +564 -0
  354. package/ts/src/hooks/claude.ts +176 -0
  355. package/ts/src/hooks/codex.ts +45 -0
  356. package/ts/src/hooks/gemini.ts +126 -0
  357. package/ts/src/hooks/main.ts +109 -0
  358. package/ts/src/hooks/mirrorState.ts +71 -0
  359. package/ts/src/hooks/payload.ts +68 -0
  360. package/ts/src/hooks/runtime.ts +159 -0
  361. package/ts/src/identity/applyConsolidationIdentityUpdate.ts +67 -0
  362. package/ts/src/identity/descriptorGenerate.ts +162 -0
  363. package/ts/src/identity/identityEdit.ts +143 -0
  364. package/ts/src/identity/identityKey.ts +77 -0
  365. package/ts/src/identity/identityRead.ts +69 -0
  366. package/ts/src/identity/identityStore.ts +80 -0
  367. package/ts/src/identity/journeyListing.ts +53 -0
  368. package/ts/src/identity/personaInspect.ts +44 -0
  369. package/ts/src/identity/personaListing.ts +37 -0
  370. package/ts/src/identity/setIdentity.ts +64 -0
  371. package/ts/src/index.ts +231 -0
  372. package/ts/src/init/init.ts +111 -0
  373. package/ts/src/journey/journeyOptions.ts +131 -0
  374. package/ts/src/journey/journeyRemoval.ts +158 -0
  375. package/ts/src/journey/journeyStatus.ts +149 -0
  376. package/ts/src/journey/journeySyncFile.ts +87 -0
  377. package/ts/src/journey/journeyWrite.ts +203 -0
  378. package/ts/src/journey/parentJourney.ts +41 -0
  379. package/ts/src/journey/validateParentJourney.ts +55 -0
  380. package/ts/src/mcp/boundary.ts +151 -0
  381. package/ts/src/mcp/guards.ts +244 -0
  382. package/ts/src/mcp/main.ts +121 -0
  383. package/ts/src/mcp/payload.ts +141 -0
  384. package/ts/src/mcp/protocol.ts +141 -0
  385. package/ts/src/mcp/readModels.ts +241 -0
  386. package/ts/src/mcp/registry.ts +201 -0
  387. package/ts/src/mcp/serve.ts +120 -0
  388. package/ts/src/mcp/tools/deterministic.ts +137 -0
  389. package/ts/src/mcp/tools/providerCrossing.ts +187 -0
  390. package/ts/src/mcp/wire.ts +8 -0
  391. package/ts/src/memory/journal.ts +180 -0
  392. package/ts/src/memory/listing.ts +107 -0
  393. package/ts/src/memory/memoryWrite.ts +88 -0
  394. package/ts/src/memory/reinforcement.ts +35 -0
  395. package/ts/src/mirror/context.ts +161 -0
  396. package/ts/src/mirror/defaultResolution.ts +199 -0
  397. package/ts/src/mirror/orchestration.ts +364 -0
  398. package/ts/src/mirror/reception.ts +194 -0
  399. package/ts/src/mirror/render.ts +75 -0
  400. package/ts/src/mirror/runtimeSession.ts +205 -0
  401. package/ts/src/mode/operatingMode.ts +148 -0
  402. package/ts/src/observability/callOutcome.ts +115 -0
  403. package/ts/src/observability/inspectLedger.ts +329 -0
  404. package/ts/src/observability/ledgerHooks.ts +126 -0
  405. package/ts/src/observability/llmCalls.ts +154 -0
  406. package/ts/src/persona/detectPersona.ts +81 -0
  407. package/ts/src/planning/promptAssembly.ts +111 -0
  408. package/ts/src/planning/weekPending.ts +81 -0
  409. package/ts/src/planning/weekPlan.ts +183 -0
  410. package/ts/src/planning/weekSave.ts +117 -0
  411. package/ts/src/providers/config.ts +189 -0
  412. package/ts/src/providers/cost.ts +64 -0
  413. package/ts/src/providers/credits.ts +204 -0
  414. package/ts/src/providers/embedding.ts +321 -0
  415. package/ts/src/providers/familyProviders.ts +147 -0
  416. package/ts/src/providers/llm.ts +334 -0
  417. package/ts/src/providers/openrouter.ts +244 -0
  418. package/ts/src/providers/redaction.ts +110 -0
  419. package/ts/src/providers/replay.ts +15 -0
  420. package/ts/src/providers/transport.ts +410 -0
  421. package/ts/src/repair/encodingRepair.ts +253 -0
  422. package/ts/src/runtime/backup.ts +151 -0
  423. package/ts/src/runtime/channel.ts +146 -0
  424. package/ts/src/runtime/config.ts +168 -0
  425. package/ts/src/runtime/diagnose.ts +686 -0
  426. package/ts/src/runtime/git.ts +510 -0
  427. package/ts/src/runtime/installKind.ts +220 -0
  428. package/ts/src/runtime/migrate.ts +154 -0
  429. package/ts/src/runtime/packageIdentity.ts +102 -0
  430. package/ts/src/runtime/releaseNotes.ts +244 -0
  431. package/ts/src/runtime/status.ts +557 -0
  432. package/ts/src/runtime/strategies/clone.ts +200 -0
  433. package/ts/src/runtime/strategies/package.ts +328 -0
  434. package/ts/src/runtime/treeRoot.ts +30 -0
  435. package/ts/src/runtime/update.ts +445 -0
  436. package/ts/src/runtime/updateGate.ts +40 -0
  437. package/ts/src/runtime/updatePipeline.ts +159 -0
  438. package/ts/src/runtime/version.ts +36 -0
  439. package/ts/src/runtime/wiring.ts +130 -0
  440. package/ts/src/search/memorySearch.ts +338 -0
  441. package/ts/src/search/ranker.ts +154 -0
  442. package/ts/src/seed/seed.ts +256 -0
  443. package/ts/src/seed/seedYaml.ts +133 -0
  444. package/ts/src/soul/apply.ts +218 -0
  445. package/ts/src/soul/harvest.ts +267 -0
  446. package/ts/src/soul/prompts/soul_beauty_voice.md +103 -0
  447. package/ts/src/soul/prompts/soul_self_voice.md +219 -0
  448. package/ts/src/soul/prompts/soul_wisdom_voice.md +121 -0
  449. package/ts/src/soul/prompts.ts +85 -0
  450. package/ts/src/soul/render.ts +358 -0
  451. package/ts/src/soul/state.ts +212 -0
  452. package/ts/src/soul/transition.ts +37 -0
  453. package/ts/src/tasks/journeyPathParse.ts +113 -0
  454. package/ts/src/tasks/taskImportSync.ts +142 -0
  455. package/ts/src/tasks/taskStore.ts +312 -0
  456. package/ts/src/tasks/weekView.ts +120 -0
  457. package/ts/src/util/clipCodePoints.ts +26 -0
  458. package/ts/src/util/paths.ts +63 -0
  459. package/ts/src/util/program.ts +9 -0
  460. package/ts/src/util/pyGenerators.ts +144 -0
  461. package/ts/src/util/pythonText.ts +317 -0
  462. package/ts/src/util/semver.ts +45 -0
  463. package/ts/src/util/shellWord.ts +25 -0
  464. package/ts/src/util/slug.ts +64 -0
  465. package/ts/src/welcome/card.ts +339 -0
  466. package/ts/src/welcome/statusLine.ts +101 -0
  467. package/ts/src/welcome/updateCache.ts +129 -0
@@ -0,0 +1,576 @@
1
+ // CV22.DS7.US5 slice A — deterministic conversation-logger core.
2
+ //
3
+ // Ports the no-LLM paths of `src/memory/cli/conversation_logger.py`
4
+ // (log_user_message, log_assistant_message, _generate_title, mute flag) and
5
+ // the transactional `runtime_session.get_or_create_conversation` service.
6
+ // Byte-shapes follow Python exactly: title truncation, whitespace collapse,
7
+ // and `json.dumps`-style metadata separators (see `pythonJsonDumps`).
8
+ //
9
+ // The LLM-backed tails of this family (extraction, close-time metadata
10
+ // finalization) are slice C/D scope behind the replay transport; nothing in
11
+ // this module may call a provider.
12
+
13
+ import { closeSync, existsSync, mkdirSync, openSync, rmSync } from "node:fs";
14
+ import { join } from "node:path";
15
+ import { backfillAssistantMessages } from "#conversation/transcriptBackfill.ts";
16
+ import { type WritableDatabase, withTransaction } from "#db/database.ts";
17
+ import {
18
+ decodeMetadata,
19
+ getRuntimeSession,
20
+ type RuntimeSessionRow,
21
+ resolveRuntimeSessionId,
22
+ upsertRuntimeSession,
23
+ } from "#mirror/runtimeSession.ts";
24
+ import { expandHome } from "#util/paths.ts";
25
+ import { pythonJsonDumps } from "#util/pyGenerators.ts";
26
+ import { codePointLength, sliceCodePoints } from "#util/pythonText.ts";
27
+
28
+ export interface LoggerDeps {
29
+ newId: () => string;
30
+ nowIso: () => string;
31
+ }
32
+
33
+ /**
34
+ * The DS7/DS8 close seam. Python's `end_conversation` writes `ended_at`
35
+ * deterministically, then runs two LLM-backed tails: extraction (conditional)
36
+ * and close-time metadata finalization (always, in a `finally`). Both are
37
+ * injected so slice A stays provider-free; slice C/D supply the real
38
+ * implementations behind the replay transport.
39
+ */
40
+ export interface CloseHooks {
41
+ runExtraction?: (db: WritableDatabase, conversationId: string) => Promise<void> | void;
42
+ finalizeMetadata?: (db: WritableDatabase, conversationId: string) => Promise<void> | void;
43
+ }
44
+
45
+ // PARITY NOTE for every call site below: Python's store upsert is asymmetric.
46
+ // `closed_at`/`metadata` use an `_UNSET` sentinel, so passing None means SET
47
+ // NULL. `conversation_id`/`interface`/`persona`/`journey`/`active` default to
48
+ // None meaning PRESERVE. The TS helper uses undefined=preserve, null=clear, so
49
+ // a Python `persona=None` maps to `persona: undefined` here — never `null`.
50
+
51
+ export interface SessionConversationOptions {
52
+ interface: string;
53
+ persona?: string | null;
54
+ journey?: string | null;
55
+ title?: string | null;
56
+ }
57
+
58
+ /**
59
+ * Python `_generate_title`: first line, hard 80 cap, word-boundary 60 cut.
60
+ * Both cuts are by code point (Python slicing), so an emoji counts once.
61
+ */
62
+ export function generateTitle(content: string): string {
63
+ const text = sliceCodePoints(content.trim().split("\n")[0] ?? "", 80);
64
+ if (codePointLength(text) <= 60) return text;
65
+ const cut = sliceCodePoints(text, 60);
66
+ const boundary = cut.lastIndexOf(" ");
67
+ return `${boundary === -1 ? cut : cut.slice(0, boundary)}...`;
68
+ }
69
+
70
+ /** Python `ConversationService._clean_title`: collapse whitespace, bound length. */
71
+ export function cleanTitle(title: string): string {
72
+ const clean = title.trim().split(/\s+/).filter(Boolean).join(" ");
73
+ if (!clean) throw new Error("title is required");
74
+ if (clean.length > 160) throw new Error("title must be at most 160 characters");
75
+ return clean;
76
+ }
77
+
78
+ function titleMetadata(
79
+ rawMetadata: string | null,
80
+ source: string,
81
+ status: string,
82
+ previousTitle: string | null,
83
+ ): Record<string, unknown> {
84
+ const metadata = decodeMetadata(rawMetadata);
85
+ if (previousTitle && previousTitle !== metadata.previous_title) {
86
+ metadata.previous_title = previousTitle;
87
+ }
88
+ metadata.title_source = source;
89
+ metadata.title_status = status;
90
+ return metadata;
91
+ }
92
+
93
+ /**
94
+ * Python `ConversationService.set_provisional_title` for an exact id.
95
+ * (The Python service also resolves id prefixes; that fallback belongs to the
96
+ * title CLI operations and is not reachable from the logger paths, so it is
97
+ * deliberately not ported here.)
98
+ */
99
+ export function setProvisionalTitle(
100
+ db: WritableDatabase,
101
+ conversationId: string,
102
+ title: string,
103
+ ): void {
104
+ const clean = cleanTitle(title);
105
+ const row = db
106
+ .prepare("SELECT title, metadata FROM conversations WHERE id = ?")
107
+ .get(conversationId);
108
+ if (!row) throw new Error(`Conversation '${conversationId}' not found`);
109
+ const metadata = titleMetadata(
110
+ typeof row.metadata === "string" ? row.metadata : null,
111
+ "first_user",
112
+ "provisional",
113
+ typeof row.title === "string" ? row.title : null,
114
+ );
115
+ db.prepare("UPDATE conversations SET title = ?, metadata = ? WHERE id = ?").run(
116
+ clean,
117
+ pythonJsonDumps(metadata),
118
+ conversationId,
119
+ );
120
+ }
121
+
122
+ /**
123
+ * Python `RuntimeSessionService.get_or_create_conversation`: return the bound
124
+ * live conversation or create-and-bind atomically under BEGIN IMMEDIATE.
125
+ * Persona/journey come from the caller only — never from the session row.
126
+ */
127
+ export function getOrCreateSessionConversation(
128
+ db: WritableDatabase,
129
+ sessionId: string,
130
+ options: SessionConversationOptions,
131
+ deps: LoggerDeps,
132
+ ): string {
133
+ return withTransaction(db, () => {
134
+ const bound = db
135
+ .prepare("SELECT conversation_id FROM runtime_sessions WHERE session_id = ?")
136
+ .get(sessionId);
137
+ const boundId =
138
+ bound && typeof bound.conversation_id === "string" ? bound.conversation_id : null;
139
+ if (boundId) {
140
+ const existing = db.prepare("SELECT id FROM conversations WHERE id = ?").get(boundId);
141
+ if (existing) return boundId;
142
+ }
143
+
144
+ const id = deps.newId();
145
+ const startedAt = deps.nowIso();
146
+ db.prepare(
147
+ `INSERT INTO conversations
148
+ (id, title, started_at, ended_at, interface, persona, journey, summary, tags, metadata)
149
+ VALUES (?, ?, ?, NULL, ?, ?, ?, NULL, NULL, NULL)`,
150
+ ).run(
151
+ id,
152
+ options.title ?? null,
153
+ startedAt,
154
+ options.interface,
155
+ options.persona ?? null,
156
+ options.journey ?? null,
157
+ );
158
+ // Mirrors the service's conflict clause exactly: it does NOT touch
159
+ // mirror_active, hook_injected, started_at, or metadata on update.
160
+ db.prepare(
161
+ `INSERT INTO runtime_sessions
162
+ (session_id, conversation_id, interface, mirror_active, persona, journey,
163
+ hook_injected, active, started_at, updated_at, closed_at, metadata)
164
+ VALUES (?, ?, ?, 0, ?, ?, 0, 1, ?, ?, NULL, NULL)
165
+ ON CONFLICT(session_id) DO UPDATE SET
166
+ conversation_id = excluded.conversation_id,
167
+ interface = excluded.interface,
168
+ persona = excluded.persona,
169
+ journey = excluded.journey,
170
+ active = 1,
171
+ closed_at = NULL,
172
+ updated_at = excluded.updated_at`,
173
+ ).run(
174
+ sessionId,
175
+ id,
176
+ options.interface,
177
+ options.persona ?? null,
178
+ options.journey ?? null,
179
+ startedAt,
180
+ startedAt,
181
+ );
182
+ return id;
183
+ });
184
+ }
185
+
186
+ function discardMarkerActive(session: RuntimeSessionRow | null): boolean {
187
+ return Boolean(decodeMetadata(session?.metadata ?? null).discard_current_conversation);
188
+ }
189
+
190
+ function addMessage(
191
+ db: WritableDatabase,
192
+ conversationId: string,
193
+ role: "user" | "assistant",
194
+ content: string,
195
+ deps: LoggerDeps,
196
+ ): void {
197
+ db.prepare(
198
+ `INSERT INTO messages (id, conversation_id, role, content, created_at, token_count, metadata)
199
+ VALUES (?, ?, ?, ?, ?, NULL, NULL)`,
200
+ ).run(deps.newId(), conversationId, role, content, deps.nowIso());
201
+ }
202
+
203
+ /** Python `log_user_message`: discard-marker reset, bind, title-on-first, append. */
204
+ export function logUserMessage(
205
+ db: WritableDatabase,
206
+ sessionId: string,
207
+ content: string,
208
+ options: { interface: string },
209
+ deps: LoggerDeps,
210
+ ): void {
211
+ let existing = getRuntimeSession(db, sessionId);
212
+ if (discardMarkerActive(existing)) {
213
+ upsertRuntimeSession(
214
+ db,
215
+ sessionId,
216
+ { metadata: null, active: true, closedAt: null },
217
+ deps.nowIso(),
218
+ );
219
+ existing = getRuntimeSession(db, sessionId);
220
+ }
221
+ const isNew = existing === null || existing.conversationId === null;
222
+ const conversationId = getOrCreateSessionConversation(
223
+ db,
224
+ sessionId,
225
+ { interface: options.interface },
226
+ deps,
227
+ );
228
+ upsertRuntimeSession(
229
+ db,
230
+ sessionId,
231
+ { interface: options.interface, active: true, closedAt: null },
232
+ deps.nowIso(),
233
+ );
234
+ if (isNew) {
235
+ setProvisionalTitle(db, conversationId, generateTitle(content));
236
+ }
237
+ addMessage(db, conversationId, "user", content, deps);
238
+ }
239
+
240
+ /** Python `log_assistant_message`: silent no-op while the discard marker holds. */
241
+ export function logAssistantMessage(
242
+ db: WritableDatabase,
243
+ sessionId: string,
244
+ content: string,
245
+ options: { interface: string },
246
+ deps: LoggerDeps,
247
+ ): void {
248
+ const existing = getRuntimeSession(db, sessionId);
249
+ if (discardMarkerActive(existing)) return;
250
+ const conversationId = getOrCreateSessionConversation(
251
+ db,
252
+ sessionId,
253
+ { interface: options.interface },
254
+ deps,
255
+ );
256
+ addMessage(db, conversationId, "assistant", content, deps);
257
+ }
258
+
259
+ /** Python `ConversationService.start_conversation`: always a fresh row. */
260
+ function startConversation(
261
+ db: WritableDatabase,
262
+ options: SessionConversationOptions,
263
+ deps: LoggerDeps,
264
+ ): string {
265
+ const id = deps.newId();
266
+ db.prepare(
267
+ `INSERT INTO conversations
268
+ (id, title, started_at, ended_at, interface, persona, journey, summary, tags, metadata)
269
+ VALUES (?, ?, ?, NULL, ?, ?, ?, NULL, NULL, NULL)`,
270
+ ).run(
271
+ id,
272
+ options.title ?? null,
273
+ deps.nowIso(),
274
+ options.interface,
275
+ options.persona ?? null,
276
+ options.journey ?? null,
277
+ );
278
+ return id;
279
+ }
280
+
281
+ /**
282
+ * Python `ConversationService.end_conversation`: deterministic close + tails.
283
+ *
284
+ * Async since CV22.DS7.US10 slice C′: both tails are genuinely asynchronous in
285
+ * TypeScript (they reach the LLM transport), and awaiting them here keeps ONE
286
+ * authority for the ordering contract — `ended_at` first, extraction second,
287
+ * finalization in a `finally`. Duplicating that contract in a second async
288
+ * close path would let the two drift on exactly the property that must not.
289
+ */
290
+ export async function endConversation(
291
+ db: WritableDatabase,
292
+ conversationId: string,
293
+ options: { extract: boolean },
294
+ deps: LoggerDeps,
295
+ hooks: CloseHooks = {},
296
+ ): Promise<void> {
297
+ db.prepare("UPDATE conversations SET ended_at = ? WHERE id = ?").run(
298
+ deps.nowIso(),
299
+ conversationId,
300
+ );
301
+ try {
302
+ if (options.extract) await hooks.runExtraction?.(db, conversationId);
303
+ } finally {
304
+ // Python finalizes in a `finally`: the orphan is closed and non-manual
305
+ // metadata finalized even when extraction fails.
306
+ await hooks.finalizeMetadata?.(db, conversationId);
307
+ }
308
+ }
309
+
310
+ /** Python `switch_conversation`: close the old conversation, bind a new one. */
311
+ export async function switchConversation(
312
+ db: WritableDatabase,
313
+ sessionId: string | null,
314
+ options: { persona?: string | null; journey?: string | null; envSessionId?: string | null },
315
+ deps: LoggerDeps,
316
+ hooks: CloseHooks = {},
317
+ ): Promise<string | null> {
318
+ const resolved = resolveRuntimeSessionId(db, sessionId, options.envSessionId ?? null);
319
+ if (!resolved) return null;
320
+
321
+ const runtimeSession = getRuntimeSession(db, resolved);
322
+ const oldConversationId = runtimeSession?.conversationId ?? null;
323
+ if (oldConversationId) {
324
+ await endConversation(db, oldConversationId, { extract: true }, deps, hooks);
325
+ }
326
+
327
+ // Python truthiness: an empty interface also falls back to claude_code.
328
+ const existingInterface = runtimeSession?.interface;
329
+ const interfaceName = existingInterface ? existingInterface : "claude_code";
330
+ const newConversationId = startConversation(
331
+ db,
332
+ {
333
+ interface: interfaceName,
334
+ persona: options.persona ?? null,
335
+ journey: options.journey ?? null,
336
+ },
337
+ deps,
338
+ );
339
+ upsertRuntimeSession(
340
+ db,
341
+ resolved,
342
+ {
343
+ conversationId: newConversationId,
344
+ interface: interfaceName,
345
+ persona: options.persona ?? undefined,
346
+ journey: options.journey ?? undefined,
347
+ active: true,
348
+ closedAt: null,
349
+ },
350
+ deps.nowIso(),
351
+ );
352
+ return newConversationId;
353
+ }
354
+
355
+ /** Python `end_session`: close the bound conversation and deactivate. */
356
+ export async function endSession(
357
+ db: WritableDatabase,
358
+ sessionId: string,
359
+ options: { extract: boolean },
360
+ deps: LoggerDeps,
361
+ hooks: CloseHooks = {},
362
+ ): Promise<void> {
363
+ const runtimeSession = getRuntimeSession(db, sessionId);
364
+ const conversationId = runtimeSession?.conversationId ?? null;
365
+ if (!conversationId) return;
366
+ await endConversation(db, conversationId, { extract: options.extract }, deps, hooks);
367
+ upsertRuntimeSession(db, sessionId, { active: false, closedAt: deps.nowIso() }, deps.nowIso());
368
+ }
369
+
370
+ /**
371
+ * Python `ConversationService.delete_conversations` for one resolved id:
372
+ * extracted memories survive with a nulled FK rather than cascading away.
373
+ */
374
+ function deleteConversation(db: WritableDatabase, conversationId: string): void {
375
+ db.prepare("DELETE FROM messages WHERE conversation_id = ?").run(conversationId);
376
+ db.prepare("DELETE FROM conversation_embeddings WHERE conversation_id = ?").run(conversationId);
377
+ db.prepare("UPDATE memories SET conversation_id = NULL WHERE conversation_id = ?").run(
378
+ conversationId,
379
+ );
380
+ db.prepare("UPDATE llm_calls SET conversation_id = NULL WHERE conversation_id = ?").run(
381
+ conversationId,
382
+ );
383
+ db.prepare("UPDATE runtime_sessions SET conversation_id = NULL WHERE conversation_id = ?").run(
384
+ conversationId,
385
+ );
386
+ db.prepare("DELETE FROM conversations WHERE id = ?").run(conversationId);
387
+ }
388
+
389
+ /** Python `discard_current_conversation`: delete and mark the session discarded. */
390
+ export function discardCurrentConversation(
391
+ db: WritableDatabase,
392
+ sessionId: string | null,
393
+ options: { interface: string },
394
+ deps: LoggerDeps,
395
+ ): string | null {
396
+ let resolved = sessionId;
397
+ if (resolved === null) {
398
+ const row = db
399
+ .prepare(
400
+ `SELECT session_id FROM runtime_sessions
401
+ WHERE active = 1 AND interface = ?
402
+ ORDER BY updated_at DESC`,
403
+ )
404
+ .get(options.interface);
405
+ resolved = row ? String(row.session_id) : null;
406
+ }
407
+ if (resolved === null) return null;
408
+
409
+ const runtimeSession = getRuntimeSession(db, resolved);
410
+ if (!runtimeSession || runtimeSession.conversationId === null) return null;
411
+ const conversationId = runtimeSession.conversationId;
412
+
413
+ withTransaction(db, () => {
414
+ deleteConversation(db, conversationId);
415
+ return null;
416
+ });
417
+
418
+ const existingInterface = runtimeSession.interface;
419
+ upsertRuntimeSession(
420
+ db,
421
+ resolved,
422
+ {
423
+ // Python passes conversation_id=None, which the store PRESERVES; the
424
+ // delete above already nulled it, so preserve and clear coincide here.
425
+ interface: existingInterface ? existingInterface : options.interface,
426
+ active: false,
427
+ metadata: pythonJsonDumps({ discard_current_conversation: true }),
428
+ },
429
+ deps.nowIso(),
430
+ );
431
+ return conversationId;
432
+ }
433
+
434
+ // --- Runtime hook entries -------------------------------------------------
435
+ //
436
+ // `hook_user_prompt` and `hook_session_end` are the hot path: they run on
437
+ // every message and every session close. Python wraps both in
438
+ // `except Exception: pass` and always exits 0, so a logging failure can never
439
+ // take down the runtime. These ports return a described outcome instead of
440
+ // exiting, keeping stdin/env at the CLI edge and the behavior testable; the
441
+ // CLI maps every outcome to exit code 0.
442
+
443
+ export type UserPromptHookOutcome =
444
+ | { action: "muted" }
445
+ | {
446
+ action: "skipped";
447
+ reason: "malformed_payload" | "missing_session" | "missing_prompt" | "slash_command";
448
+ }
449
+ | { action: "logged"; sessionId: string }
450
+ | { action: "failed"; error: string };
451
+
452
+ export type SessionEndHookOutcome =
453
+ | { action: "skipped"; reason: "malformed_payload" | "nothing_to_do" }
454
+ | { action: "ended"; sessionId: string; transcriptPath: string | null; backfilled: boolean }
455
+ | { action: "backfilled"; transcriptPath: string }
456
+ | { action: "failed"; error: string };
457
+
458
+ function parsePayload(raw: string): Record<string, unknown> | null {
459
+ try {
460
+ const value: unknown = JSON.parse(raw);
461
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
462
+ return value as Record<string, unknown>;
463
+ } catch {
464
+ return null;
465
+ }
466
+ }
467
+
468
+ function payloadString(payload: Record<string, unknown>, key: string): string {
469
+ const value = payload[key];
470
+ return typeof value === "string" ? value : "";
471
+ }
472
+
473
+ /** Python `hook_user_prompt`: mute gate, slash-command skip, never throw. */
474
+ export function handleUserPromptHook(
475
+ db: WritableDatabase,
476
+ rawPayload: string,
477
+ options: { mirrorHome: string; interface?: string },
478
+ deps: LoggerDeps,
479
+ ): UserPromptHookOutcome {
480
+ try {
481
+ if (isMuted(options.mirrorHome)) return { action: "muted" };
482
+
483
+ const payload = parsePayload(rawPayload);
484
+ if (!payload) return { action: "skipped", reason: "malformed_payload" };
485
+
486
+ const sessionId = payloadString(payload, "session_id");
487
+ const prompt = payloadString(payload, "prompt");
488
+ if (!sessionId) return { action: "skipped", reason: "missing_session" };
489
+ if (!prompt) return { action: "skipped", reason: "missing_prompt" };
490
+ // Slash commands never enter conversation history.
491
+ if (prompt.startsWith("/")) return { action: "skipped", reason: "slash_command" };
492
+
493
+ logUserMessage(db, sessionId, prompt, { interface: options.interface ?? "claude_code" }, deps);
494
+ return { action: "logged", sessionId };
495
+ } catch (error) {
496
+ // Python swallows everything here; the runtime must survive a logging fault.
497
+ return { action: "failed", error: error instanceof Error ? error.message : String(error) };
498
+ }
499
+ }
500
+
501
+ /**
502
+ * Python `hook_session_end`: end the session when the payload names one, then
503
+ * backfill assistant turns from the transcript when it exists -- in that
504
+ * order, so the just-ended conversation's `ended_at` bounds the backfill.
505
+ *
506
+ * The two halves are independent: an empty `session_id` with a transcript
507
+ * still backfills (the session-less route, slice E), and a session without a
508
+ * transcript still ends. Everything is swallowed; the hook always exits 0.
509
+ */
510
+ export async function handleSessionEndHook(
511
+ db: WritableDatabase,
512
+ rawPayload: string,
513
+ options: { mirrorHome: string; claudeProjectDir: string | null; homeDir: string },
514
+ deps: LoggerDeps,
515
+ hooks: CloseHooks = {},
516
+ ): Promise<SessionEndHookOutcome> {
517
+ try {
518
+ const payload = parsePayload(rawPayload);
519
+ if (!payload) return { action: "skipped", reason: "malformed_payload" };
520
+
521
+ const sessionId = payloadString(payload, "session_id");
522
+ if (sessionId) await endSession(db, sessionId, { extract: true }, deps, hooks);
523
+
524
+ const transcriptPath = resolveTranscriptPath(
525
+ payloadString(payload, "transcript_path"),
526
+ sessionId,
527
+ options.claudeProjectDir,
528
+ options.homeDir,
529
+ );
530
+ const backfilled = transcriptPath !== null && existsSync(transcriptPath);
531
+ if (backfilled) backfillAssistantMessages(db, transcriptPath, deps);
532
+
533
+ if (sessionId) return { action: "ended", sessionId, transcriptPath, backfilled };
534
+ if (backfilled) return { action: "backfilled", transcriptPath };
535
+ return { action: "skipped", reason: "nothing_to_do" };
536
+ } catch (error) {
537
+ return { action: "failed", error: error instanceof Error ? error.message : String(error) };
538
+ }
539
+ }
540
+
541
+ /**
542
+ * Python's transcript fallback: an explicit payload path wins; otherwise
543
+ * derive `~/.claude/projects/<project-hash>/<session>.jsonl`, where the hash
544
+ * is the project dir with leading slashes stripped and `/` replaced by `-`.
545
+ */
546
+ export function resolveTranscriptPath(
547
+ payloadPath: string,
548
+ sessionId: string,
549
+ claudeProjectDir: string | null,
550
+ homeDir: string,
551
+ ): string | null {
552
+ if (payloadPath) return payloadPath;
553
+ if (!claudeProjectDir) return null;
554
+ const projectHash = claudeProjectDir.replace(/^\/+/, "").replaceAll("/", "-");
555
+ return join(homeDir, ".claude", "projects", projectHash, `${sessionId}.jsonl`);
556
+ }
557
+
558
+ function muteFlagPath(mirrorHome: string): string {
559
+ return join(expandHome(mirrorHome), "mute");
560
+ }
561
+
562
+ /** Python `is_muted` with an explicit mirror home. */
563
+ export function isMuted(mirrorHome: string): boolean {
564
+ return existsSync(muteFlagPath(mirrorHome));
565
+ }
566
+
567
+ /** Python `set_mute`: touch (no truncate) or unlink with missing_ok. */
568
+ export function setMute(on: boolean, mirrorHome: string): void {
569
+ const path = muteFlagPath(mirrorHome);
570
+ if (on) {
571
+ mkdirSync(expandHome(mirrorHome), { recursive: true });
572
+ closeSync(openSync(path, "a"));
573
+ return;
574
+ }
575
+ rmSync(path, { force: true });
576
+ }