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,244 @@
1
+ // Wallet guards for the MCP surface (CV22.DS9.TS1).
2
+ //
3
+ // This module has no oracle. Nothing here exists in Python: US1, US2, and TS2 were ports
4
+ // graded by byte equality, and this is deliberate divergence, argued from the DS9 threat
5
+ // model (item 3, denial of wallet) and sized from measured spend.
6
+ //
7
+ // What it guards, and why that shape:
8
+ //
9
+ // * The risk is RATE, not dollars. Measured on the author's ledger, an embedding costs
10
+ // ~$0.000002 and the whole account spent $0.0001 on embeddings in seven days -- one
11
+ // dollar is ~500,000 calls. A USD ceiling that bites would have to be set at cents.
12
+ // What a runaway agent loop actually does is exhaust the provider's rate limit (whose
13
+ // 429s then land on the user's OTHER work), stall at ~2s per call, and fill its own
14
+ // context. So the rate guard is the control and the ceiling is an opt-in backstop.
15
+ //
16
+ // * State lives in the `llm_calls` ledger, not in this process. A user runs several MCP
17
+ // clients at once; a per-process counter is a belief, not a ceiling.
18
+ //
19
+ // * The count is attributed. Extraction embeds every memory it creates, so a session
20
+ // close writes dozens of embedding rows in a minute. Counting all of them would refuse
21
+ // the agent because the USER ended a conversation -- so only rows marked as this
22
+ // surface's are counted.
23
+ //
24
+ // * The decision is pure and the ledger read is at the edge, so the policy is testable
25
+ // against synthetic states and the refusal text is under test rather than in a comment.
26
+
27
+ import type { Database } from "#db/database.ts";
28
+
29
+ /**
30
+ * The `llm_calls.session_id` value marking a call made by the MCP surface.
31
+ *
32
+ * One fact in one column (D2). The first draft encoded the tool too
33
+ * (`mcp:search_memories`), to honour the AI-19 rider's "per-tool" phrasing; measured, that
34
+ * granularity buys nothing -- both paid tools cost one embedding per call and a global cap
35
+ * bounds the harm identically -- so it was rejected as two facts in one column. If per-tool
36
+ * attribution is ever needed it is a real column with a real migration, not a delimiter.
37
+ *
38
+ * `session_id` is the right column: free text, semantically "the session that made the
39
+ * call", and null on every one of the 225 embedding rows that existed when this was
40
+ * written, so the marker collides with nothing. It also makes MCP spend filterable in
41
+ * `inspect llm-calls --session mcp`, which is the operator's view of this surface.
42
+ */
43
+ export const MCP_LEDGER_SESSION = "mcp";
44
+
45
+ /** The tools whose calls cross an embedding provider, and are therefore counted. */
46
+ export const PAID_TOOLS: readonly string[] = ["search_memories", "mirror_context"];
47
+
48
+ export interface SpendPolicy {
49
+ /** Max attributed embedding calls inside the window. */
50
+ rateLimit: number;
51
+ /** Sliding window, in minutes. */
52
+ windowMinutes: number;
53
+ /** Trailing-24h USD ceiling, or null for none (the default). */
54
+ dailyUsdCeiling: number | null;
55
+ }
56
+
57
+ /** What the ledger says about this surface right now. */
58
+ export interface SpendState {
59
+ /** Attributed embedding calls inside the window. */
60
+ callsInWindow: number;
61
+ /** Attributed spend over the trailing 24h. Unpriced rows contribute 0. */
62
+ usdLast24h: number;
63
+ }
64
+
65
+ export type SpendDecision =
66
+ | { allow: true }
67
+ | { allow: false; reason: "rate_limit" | "daily_ceiling"; text: string };
68
+
69
+ export const DEFAULT_SPEND_POLICY: SpendPolicy = {
70
+ // A human-driven session rarely sustains three searches a minute; a loop at the measured
71
+ // ~2s per search reaches thirty inside one minute. The gap between the two is where this
72
+ // number lives.
73
+ rateLimit: 30,
74
+ windowMinutes: 10,
75
+ dailyUsdCeiling: null,
76
+ };
77
+
78
+ /**
79
+ * The unmetered way to use each paid tool, named in its own refusal.
80
+ *
81
+ * Per tool, because the advice has to be advice the refused tool can actually take:
82
+ * `mirror_context` has no layer or type filter, and telling an agent to use one is how it
83
+ * learns that refusal text is noise.
84
+ */
85
+ const UNMETERED_ALTERNATIVE: Record<string, string> = {
86
+ search_memories:
87
+ "Filtered calls — journey, layer, or type, with no query — are not metered and still work.",
88
+ mirror_context: "Calling it without a query is not metered and still returns the context.",
89
+ };
90
+
91
+ function alternativeFor(tool: string): string {
92
+ return UNMETERED_ALTERNATIVE[tool] ?? "Calls that do not carry a query are not metered.";
93
+ }
94
+
95
+ /**
96
+ * Decide whether one paid call may proceed. Pure: no clock, no database.
97
+ *
98
+ * The refusal TEXT is produced here rather than at the call site because it is the control,
99
+ * not decoration. Keeping it here keeps it under test -- and the E2E is what tests the part
100
+ * a unit test cannot: whether a real model obeys it.
101
+ *
102
+ * Shape (prompt-engineer lens): lead with the fact, name the unmetered alternative, name
103
+ * the human who can lift the bound, instruction LAST because what comes last is what a
104
+ * model follows in the moment. Never a timed invitation: an agent told when to come back
105
+ * plans to come back, and the return is the loop.
106
+ *
107
+ * Two corrections came from the first E2E (2026-09-19), and both are the loop working:
108
+ *
109
+ * * "Use a filter instead" was read by the model as "the error message suggests a way
110
+ * around the limit", and it then admitted it did not know whether filtered calls
111
+ * counted. A refusal that reads as a loophole hint teaches bypass in the server's own
112
+ * voice. Saying plainly that filters are NOT METERED is the truth, grants nothing the
113
+ * tool schema did not already expose, and removes the evasion framing.
114
+ *
115
+ * * "Do not retry this tool" was read as "this call will not succeed": the agent moved
116
+ * to the next topic and was refused four more times. The stop is now scoped to the
117
+ * paid mode (another QUERY) and gated on the human rather than on nothing.
118
+ */
119
+ export function decideSpend(tool: string, state: SpendState, policy: SpendPolicy): SpendDecision {
120
+ const stop = `Do not send another query to this tool until the user replies.`;
121
+ if (policy.dailyUsdCeiling !== null && state.usdLast24h >= policy.dailyUsdCeiling) {
122
+ return {
123
+ allow: false,
124
+ reason: "daily_ceiling",
125
+ text:
126
+ `${tool} is over the MCP spend ceiling the user set for today. ` +
127
+ `${alternativeFor(tool)} Ask the user before continuing. ${stop}`,
128
+ };
129
+ }
130
+ if (state.callsInWindow >= policy.rateLimit) {
131
+ return {
132
+ allow: false,
133
+ reason: "rate_limit",
134
+ text:
135
+ `${tool} is rate-limited (${policy.rateLimit} query searches in ${policy.windowMinutes} minutes). ` +
136
+ `${alternativeFor(tool)} Ask the user to raise MIRROR_MCP_EMBED_RATE_LIMIT if you ` +
137
+ `need more. ${stop}`,
138
+ };
139
+ }
140
+ return { allow: true };
141
+ }
142
+
143
+ /** The env slice the tunables read. `process.env` satisfies it. */
144
+ export interface SpendPolicyEnv {
145
+ MIRROR_MCP_EMBED_RATE_LIMIT?: string;
146
+ MIRROR_MCP_EMBED_RATE_WINDOW_MINUTES?: string;
147
+ MIRROR_MCP_DAILY_USD_CEILING?: string;
148
+ }
149
+
150
+ function positiveNumber(name: string, raw: string, integer: boolean): number {
151
+ const value = Number(raw);
152
+ const valid = Number.isFinite(value) && value > 0 && (!integer || Number.isInteger(value));
153
+ if (!valid) {
154
+ throw new Error(
155
+ `${name} must be a positive ${integer ? "integer" : "number"}; got ${JSON.stringify(raw)}`,
156
+ );
157
+ }
158
+ return value;
159
+ }
160
+
161
+ /**
162
+ * Whether the wallet guards run: on unless `MIRROR_TS_MCP_GUARDS=0` (CV22.DS9.TS1).
163
+ *
164
+ * NOT one of the revert gates CV22.DS10.TS5 deleted. Those chose an ENGINE and left with
165
+ * the Python one; this switches a TypeScript behavior, and `=0` restores exactly what TS2
166
+ * shipped. Only an exact `"0"` turns the guards off, so an unrelated value cannot silently
167
+ * unguard the surface. (It was the routing module's generic `gateWithDefault` until that
168
+ * module stopped having gates; this is its only reader.)
169
+ */
170
+ export function guardsEnabled(value: string | undefined): boolean {
171
+ return value !== "0";
172
+ }
173
+
174
+ /**
175
+ * Read the tunables, failing loudly on anything malformed.
176
+ *
177
+ * A typo in `.env` takes the MCP surface down with a named reason in the client's log
178
+ * rather than silently reverting to a default -- the DS8 rule. Silently unguarded while the
179
+ * user believes otherwise is the worse of the two failures.
180
+ */
181
+ export function policyFromEnv(env: SpendPolicyEnv): SpendPolicy {
182
+ const rateLimitRaw = env.MIRROR_MCP_EMBED_RATE_LIMIT;
183
+ const windowRaw = env.MIRROR_MCP_EMBED_RATE_WINDOW_MINUTES;
184
+ const ceilingRaw = env.MIRROR_MCP_DAILY_USD_CEILING;
185
+ return {
186
+ rateLimit:
187
+ rateLimitRaw === undefined
188
+ ? DEFAULT_SPEND_POLICY.rateLimit
189
+ : positiveNumber("MIRROR_MCP_EMBED_RATE_LIMIT", rateLimitRaw, true),
190
+ windowMinutes:
191
+ windowRaw === undefined
192
+ ? DEFAULT_SPEND_POLICY.windowMinutes
193
+ : positiveNumber("MIRROR_MCP_EMBED_RATE_WINDOW_MINUTES", windowRaw, true),
194
+ dailyUsdCeiling:
195
+ ceilingRaw === undefined
196
+ ? DEFAULT_SPEND_POLICY.dailyUsdCeiling
197
+ : positiveNumber("MIRROR_MCP_DAILY_USD_CEILING", ceilingRaw, false),
198
+ };
199
+ }
200
+
201
+ /**
202
+ * Read what the ledger says about this surface: attributed calls inside the window and
203
+ * attributed spend over the trailing 24 hours.
204
+ *
205
+ * Cross-process by construction -- the state is rows, not memory, so a second MCP client
206
+ * shares the count rather than getting its own budget. One indexed-enough `SELECT` per paid
207
+ * call on a read-only WAL handle, which blocks nothing (measured at 552 rows: sub-
208
+ * millisecond; revisit if the table passes ~100k).
209
+ *
210
+ * `SUM(cost_usd)` skips NULLs, so a failed attempt contributes zero to the ceiling while
211
+ * still counting against the rate -- which is the right way round: a failing loop still
212
+ * hammers the provider, but it did not demonstrably spend.
213
+ */
214
+ export function readSpendState(
215
+ db: Database,
216
+ policy: SpendPolicy,
217
+ now: Date = new Date(),
218
+ ): SpendState {
219
+ const windowStart = isoFrom(now, policy.windowMinutes);
220
+ const dayStart = isoFrom(now, 24 * 60);
221
+ const row = db
222
+ .prepare(
223
+ `SELECT
224
+ COUNT(CASE WHEN called_at >= ? THEN 1 END) AS calls_in_window,
225
+ COALESCE(SUM(CASE WHEN called_at >= ? THEN cost_usd END), 0) AS usd_last_24h
226
+ FROM llm_calls
227
+ WHERE session_id = ? AND role = 'embedding' AND called_at >= ?`,
228
+ )
229
+ .get(windowStart, dayStart, MCP_LEDGER_SESSION, dayStart);
230
+ return {
231
+ callsInWindow: Number(row?.calls_in_window ?? 0),
232
+ usdLast24h: Number(row?.usd_last_24h ?? 0),
233
+ };
234
+ }
235
+
236
+ /**
237
+ * `called_at` is microsecond ISO UTC on both engines (`nowIso` -> `toMicrosecondIso`), and
238
+ * this bound is compared lexically against it. A millisecond `toISOString()` sorts
239
+ * correctly against microsecond values because the prefix is identical up to the
240
+ * fractional digits and the comparison is `>=` on a lower bound.
241
+ */
242
+ function isoFrom(now: Date, minutesAgo: number): string {
243
+ return new Date(now.getTime() - minutesAgo * 60_000).toISOString();
244
+ }
@@ -0,0 +1,121 @@
1
+ // Mirror MCP server entry point (CV22.DS9.US1 + US2).
2
+ //
3
+ // Launched by an MCP client over stdio. Unreferenced by any route until
4
+ // CV22.DS9.TS2 flips the plugin manifest to it; it ships now so the parity
5
+ // harness can spawn the real process the way a client would.
6
+ //
7
+ // Warning suppression is load-bearing, not cosmetic. `node:sqlite` emits an
8
+ // ExperimentalWarning on import under the Node 24 floor this package declares,
9
+ // and the Mirror skills silence it with `NODE_OPTIONS=--no-warnings` -- an
10
+ // environment variable NO MCP CLIENT WILL SET. stderr is the client's log for
11
+ // this server, and the parity harness asserts it stays empty, so the
12
+ // suppression has to live in-process and run before anything reaches for the
13
+ // database. (RS005: what lands in that log is read by the host.)
14
+
15
+ process.removeAllListeners("warning");
16
+ process.on("warning", (warning) => {
17
+ if (warning.name === "ExperimentalWarning") return;
18
+ process.stderr.write(`${warning.name}: ${warning.message}\n`);
19
+ });
20
+
21
+ const { resolveDbPath } = await import("#frontDoor/dbPath.ts");
22
+ const { resolveSearchEmbeddingProvider } = await import("#frontDoor/searchRoute.ts");
23
+ const { openDatabaseForLedgerAppend, openDatabaseReadOnly } = await import("#db/database.ts");
24
+ const { ensureDatabaseReady } = await import("#db/readyOnOpen.ts");
25
+ const { embeddingLedgerHook } = await import("#observability/ledgerHooks.ts");
26
+ const { guardsEnabled, MCP_LEDGER_SESSION, policyFromEnv } = await import("./guards.ts");
27
+ const { guardedRegistry } = await import("./boundary.ts");
28
+ const { basename } = await import("node:path");
29
+ const { wiredRegistry } = await import("./registry.ts");
30
+ const { serve } = await import("./serve.ts");
31
+
32
+ // READ-ONLY, by Navigator decision (CV22.DS9.US2, option (d)).
33
+ //
34
+ // The alternatives were a full pre-write backup on every launch -- measured at
35
+ // 399 ms and a 49.3 MB snapshot on the owner's database, paid each time a
36
+ // client spawns this process -- or opening writable without that gate, which
37
+ // would narrow the DS4 write discipline inside a wiring story. Read-only is the
38
+ // strongest posture and matches the DS9 threat model, which calls this server a
39
+ // read oracle over private memory: it now cannot write even if a tool tried.
40
+ //
41
+ // The cost is named rather than hidden: the embedding-ledger row that a query
42
+ // search would record (AI-09/D-003) is NOT written, so agent-initiated searches
43
+ // go uncounted as spend. CV22.DS9.TS1's wallet guard reads that ledger, so TS2
44
+ // must settle how this server opens its database before TS1 can guard what it
45
+ // cannot see. Recorded in the DS9 package.
46
+ //
47
+ // Opened eagerly, as Python does: `serve()` constructs its MemoryClient before
48
+ // reading the first line, so an unconfigured install fails at startup rather
49
+ // than answering `initialize` and then failing all seven tools.
50
+ const databasePath = resolveDbPath([]);
51
+
52
+ // Activate before opening, also as Python does: `get_connection` bootstraps a
53
+ // missing file and runs migrations under the bootstrap lock before handing back
54
+ // a connection, so every Python entry point -- this server included -- gets a
55
+ // current schema. US2 opened read-only and skipped it, which meant a pending
56
+ // TS-authored migration would surface as a tool error on the first read that
57
+ // touched the new shape, where Python would simply have applied it.
58
+ const migration = ensureDatabaseReady(databasePath);
59
+ if (migration.migrated) {
60
+ // The one thing this server writes to stderr in normal operation. A migration
61
+ // is an operator-visible event and stderr is the client's log for this
62
+ // process; the front door records the same fact, redacted the same way, in
63
+ // the front-door log. Ids and a backup file name only -- never content.
64
+ process.stderr.write(
65
+ `migrate_on_open applied=${migration.appliedIds.join(",")} backup=${
66
+ migration.backupPath ? basename(migration.backupPath) : "none"
67
+ }\n`,
68
+ );
69
+ }
70
+
71
+ const db = openDatabaseReadOnly(databasePath);
72
+
73
+ // The single sanctioned write (TS2 D1), on its own connection, opened lazily so
74
+ // a session that never searches never opens it. The handle can prepare nothing
75
+ // but the `llm_calls` append, and it never reaches a tool: the registry gets
76
+ // the SINK below, not this connection.
77
+ // Attributed to this surface (TS1 D2): extraction embeds every memory it creates, so a
78
+ // session close writes dozens of embedding rows in a minute. A wallet guard counting all
79
+ // of them would refuse the agent because the USER ended a conversation. The marker is what
80
+ // makes the count name the actor it guards -- and it never leaves this file, so no tool can
81
+ // write a row claiming to be someone else.
82
+ let ledgerDb: ReturnType<typeof openDatabaseForLedgerAppend> | null = null;
83
+ const embeddingLedger = (info: Parameters<ReturnType<typeof embeddingLedgerHook>>[0]): void => {
84
+ ledgerDb ??= openDatabaseForLedgerAppend(databasePath);
85
+ embeddingLedgerHook(ledgerDb, { sessionId: MCP_LEDGER_SESSION })(info);
86
+ };
87
+
88
+ // The live provider resolves its config lazily, so a missing key degrades the
89
+ // search to lexical-only instead of failing the server -- the same posture the
90
+ // `memories --search` route takes.
91
+ const embeddingProvider = await resolveSearchEmbeddingProvider();
92
+
93
+ const registry = wiredRegistry({
94
+ db,
95
+ runtime: {
96
+ embeddingProvider,
97
+ embeddingLedger,
98
+ databasePath,
99
+ ...(process.env.MIRROR_HOME ? { mirrorHome: process.env.MIRROR_HOME } : {}),
100
+ ...(process.env.MIRROR_USER ? { user: process.env.MIRROR_USER } : {}),
101
+ },
102
+ });
103
+
104
+ // Guards on by default (CV22.DS9.TS1); `MIRROR_TS_MCP_GUARDS=0` restores exactly what TS2
105
+ // shipped. The tunables are resolved here,
106
+ // at startup, so a typo in `.env` fails the launch with a named reason in the client's log
107
+ // rather than leaving the surface silently unguarded (the DS8 rule).
108
+ //
109
+ // The gate reverts REFUSALS only. The ledger row and its marker stay on either way: they
110
+ // are observability, and turning off a guard is not a reason to stop recording what the
111
+ // agent spends.
112
+ const guarded = guardsEnabled(process.env.MIRROR_TS_MCP_GUARDS)
113
+ ? guardedRegistry(registry, { db, policy: policyFromEnv(process.env) })
114
+ : registry;
115
+
116
+ await serve({ registry: guarded });
117
+
118
+ // Input ended and every response has drained (`serve` does not resolve before
119
+ // that). Exiting explicitly keeps the process from lingering on an open stdin
120
+ // handle in clients that leave the pipe half-open.
121
+ process.exit(0);
@@ -0,0 +1,141 @@
1
+ // Python-compatible JSON encoding for the MCP surface (CV22.DS9.US1 + US2).
2
+ //
3
+ // One encoder, two shapes. The JSON-RPC envelope is written compactly with
4
+ // Python's default separators (`", "` / `": "`); tool payloads are written with
5
+ // `indent=2`. Both come from `json.dumps` in the oracle, so both live here --
6
+ // two modules would be the same rules maintained twice.
7
+ //
8
+ // Two places where `JSON.stringify` is not enough:
9
+ //
10
+ // 1. **Separators.** Python writes `{"a": 1, "b": 2}`; JS writes `{"a":1,"b":2}`.
11
+ // Semantically identical, byte-wise different, and the parity claim is about
12
+ // bytes (see the US1 plan).
13
+ //
14
+ // 2. **Whole floats.** Python writes `2.0` for a float that happens to be whole;
15
+ // JS writes `2`, and JavaScript has no int/float distinction to recover from
16
+ // (`2.0 === 2`). Measured on the real database: `detect_persona` returns
17
+ // scores of `1.0` and `2.0` routinely. So float-ness is carried at runtime by
18
+ // `PyFloat`, wrapped at the mapper for exactly the fields the oracle types as
19
+ // `float`. A compile-time brand cannot do this -- it is erased before the
20
+ // encoder ever sees the value.
21
+ //
22
+ // Escaping is delegated to `JSON.stringify` for every scalar and object key, so
23
+ // string escaping, unicode (Python runs `ensure_ascii=False`, which matches JS),
24
+ // and number formatting stay exactly what the platform does.
25
+
26
+ /**
27
+ * A number the Python oracle types as `float`, so it renders `2.0` where an int
28
+ * would render `2`. A class rather than a branded type because the distinction
29
+ * must survive to runtime.
30
+ */
31
+ export class PyFloat {
32
+ // An explicit field, not a parameter property: Node runs TypeScript in
33
+ // strip-only mode, which rejects `constructor(readonly value: number)`.
34
+ readonly value: number;
35
+
36
+ constructor(value: number) {
37
+ this.value = value;
38
+ }
39
+ }
40
+
41
+ /** Wrap a value the oracle types as `float`. */
42
+ export function pyFloat(value: number): PyFloat {
43
+ return new PyFloat(value);
44
+ }
45
+
46
+ export interface PythonJsonOptions {
47
+ /** Spaces per level. Omit or pass 0 for Python's compact default. */
48
+ indent?: number;
49
+ }
50
+
51
+ /** Encode a value the way Python's `json.dumps` would. */
52
+ export function pythonJson(value: unknown, options: PythonJsonOptions = {}): string {
53
+ const indent = options.indent ?? 0;
54
+ return encode(value, indent, 0);
55
+ }
56
+
57
+ /** Encode one JSON-RPC line: compact, Python separators. */
58
+ export function encodeJsonLine(value: unknown): string {
59
+ return pythonJson(value);
60
+ }
61
+
62
+ /**
63
+ * Render a float the way Python's `repr` does.
64
+ *
65
+ * Two differences from `String(n)`, both measured against `json.dumps`:
66
+ * whole values keep a `.0` (`2` -> `2.0`), and exponents are written with at
67
+ * least two digits (`1e-7` -> `1e-07`). A value already in exponent form never
68
+ * gains a `.0` -- Python writes `1e+22`, not `1e+22.0`.
69
+ */
70
+ function encodeFloat(value: number): string {
71
+ if (!Number.isFinite(value)) {
72
+ // Python emits bare Infinity/NaN here; no MCP payload carries them, and a
73
+ // silent mismatch would be worse than a loud failure.
74
+ throw new TypeError(`cannot encode non-finite float: ${value}`);
75
+ }
76
+ if (Object.is(value, -0)) return "-0.0";
77
+
78
+ const rendered = String(value);
79
+ const exponentIndex = rendered.indexOf("e");
80
+ if (exponentIndex === -1) {
81
+ return Number.isInteger(value) ? `${rendered}.0` : rendered;
82
+ }
83
+
84
+ const mantissa = rendered.slice(0, exponentIndex);
85
+ const sign = rendered[exponentIndex + 1] === "-" ? "-" : "+";
86
+ const digits = rendered.slice(exponentIndex + 1).replace(/^[+-]/, "");
87
+ return `${mantissa}e${sign}${digits.padStart(2, "0")}`;
88
+ }
89
+
90
+ function encode(value: unknown, indent: number, depth: number): string {
91
+ if (value instanceof PyFloat) return encodeFloat(value.value);
92
+ if (value === null || value === undefined) return "null";
93
+
94
+ const type = typeof value;
95
+ if (type === "string" || type === "boolean") return JSON.stringify(value);
96
+ if (type === "number") {
97
+ if (!Number.isFinite(value as number)) {
98
+ throw new TypeError(`cannot encode non-finite number: ${String(value)}`);
99
+ }
100
+ return JSON.stringify(value);
101
+ }
102
+
103
+ if (Array.isArray(value)) {
104
+ if (value.length === 0) return "[]";
105
+ const items = value.map((item) => encode(item, indent, depth + 1));
106
+ return wrap("[", items, "]", indent, depth);
107
+ }
108
+
109
+ if (type === "object") {
110
+ // Only plain objects are data. A Date, Map, or class instance would encode
111
+ // as `{}` through Object.entries -- silently losing content that Python
112
+ // would have rendered through `default=str`. Reject it instead.
113
+ const prototype = Object.getPrototypeOf(value);
114
+ if (prototype !== Object.prototype && prototype !== null) {
115
+ throw new TypeError(
116
+ `cannot encode ${(value as object).constructor?.name ?? "object"} as Python JSON`,
117
+ );
118
+ }
119
+ const entries = Object.entries(value as Record<string, unknown>).filter(
120
+ ([, entryValue]) => entryValue !== undefined,
121
+ );
122
+ if (entries.length === 0) return "{}";
123
+ const items = entries.map(
124
+ ([key, entryValue]) => `${JSON.stringify(key)}: ${encode(entryValue, indent, depth + 1)}`,
125
+ );
126
+ return wrap("{", items, "}", indent, depth);
127
+ }
128
+
129
+ // Python's `default=str` would stringify anything else. There is no honest
130
+ // port of that -- `String(obj)` is `[object Object]`, never Python's repr --
131
+ // and after the DS9.US2 D1 fix no tool payload contains such a value. Failing
132
+ // loudly puts the surprise in a test rather than in a client's context window.
133
+ throw new TypeError(`cannot encode value of type ${type} as Python JSON`);
134
+ }
135
+
136
+ function wrap(open: string, items: string[], close: string, indent: number, depth: number): string {
137
+ if (indent <= 0) return `${open}${items.join(", ")}${close}`;
138
+ const inner = " ".repeat(indent * (depth + 1));
139
+ const outer = " ".repeat(indent * depth);
140
+ return `${open}\n${inner}${items.join(`,\n${inner}`)}\n${outer}${close}`;
141
+ }
@@ -0,0 +1,141 @@
1
+ // MCP JSON-RPC dispatch (CV22.DS9.US1).
2
+ //
3
+ // A faithful port of `handle_message` in `src/memory/mcp/server.py`. Pure over
4
+ // the message and the registry -- no I/O, no database, no stdio -- which is what
5
+ // makes it gradeable against a golden. `serve.ts` is the thin loop around it.
6
+ //
7
+ // Key-insertion order matters here. Responses are serialized with
8
+ // `JSON.stringify`, which emits keys in insertion order, and the parity golden
9
+ // compares the resulting bytes against Python's `json.dumps`. Every object
10
+ // literal below is written in the oracle's field order for that reason.
11
+
12
+ import type { ToolRegistry } from "./registry.ts";
13
+
14
+ export const PROTOCOL_VERSION = "2025-06-18";
15
+ export const SERVER_NAME = "mirror-mind";
16
+
17
+ /** A JSON-RPC id: any JSON value, including `0`, `""`, and `null`. */
18
+ export type JsonRpcId = unknown;
19
+
20
+ export interface JsonRpcResponse {
21
+ jsonrpc: "2.0";
22
+ id: JsonRpcId;
23
+ result?: unknown;
24
+ error?: { code: number; message: string };
25
+ }
26
+
27
+ export interface HandleOptions {
28
+ /** Injectable so the parity test can freeze what `importlib.metadata` returns. */
29
+ serverVersion: string;
30
+ }
31
+
32
+ function result(id: JsonRpcId, value: Record<string, unknown>): JsonRpcResponse {
33
+ return { jsonrpc: "2.0", id, result: value };
34
+ }
35
+
36
+ function error(id: JsonRpcId, code: number, message: string): JsonRpcResponse {
37
+ return { jsonrpc: "2.0", id, error: { code, message } };
38
+ }
39
+
40
+ /**
41
+ * Render a value the way Python's f-string `str()` would, for the one place the
42
+ * oracle interpolates an arbitrary argument into a message: `Unknown tool: {name}`.
43
+ * A missing name is `None` in Python, not `undefined`; `True`/`False` are
44
+ * capitalized. Getting this wrong is invisible until a client logs the string.
45
+ */
46
+ function pythonStr(value: unknown): string {
47
+ if (value === undefined || value === null) return "None";
48
+ if (value === true) return "True";
49
+ if (value === false) return "False";
50
+ if (typeof value === "string") return value;
51
+ return JSON.stringify(value) ?? String(value);
52
+ }
53
+
54
+ /**
55
+ * Python's `x or {}`: falsy values (None, {}, [], "", 0, False) become `{}`.
56
+ * Used for `params` and `arguments`, where the oracle relies on that coercion.
57
+ */
58
+ function orEmptyObject(value: unknown): unknown {
59
+ if (value === undefined || value === null || value === false || value === 0 || value === "") {
60
+ return {};
61
+ }
62
+ if (Array.isArray(value)) return value.length === 0 ? {} : value;
63
+ if (typeof value === "object" && Object.keys(value as object).length === 0) return {};
64
+ return value;
65
+ }
66
+
67
+ /** Extract the error text the way Python's `str(exc)` would. */
68
+ function errorText(thrown: unknown): string {
69
+ if (thrown instanceof Error) return thrown.message;
70
+ return String(thrown);
71
+ }
72
+
73
+ /**
74
+ * Dispatch one JSON-RPC message. Returns a response, or `null` for notifications.
75
+ *
76
+ * Request-vs-notification is decided by KEY PRESENCE (`"id" in message`), exactly
77
+ * as the oracle does -- so `{"id": 0}`, `{"id": ""}` and `{"id": null}` are all
78
+ * requests whose id must be echoed. Deciding on truthiness instead leaves those
79
+ * clients waiting forever for a response that is never written.
80
+ */
81
+ export async function handleMessage(
82
+ message: Record<string, unknown>,
83
+ registry: ToolRegistry,
84
+ options: HandleOptions,
85
+ ): Promise<JsonRpcResponse | null> {
86
+ const method = message.method;
87
+ const isNotification = !("id" in message);
88
+ const id = message.id;
89
+
90
+ if (typeof method !== "string") {
91
+ return isNotification ? null : error(id, -32600, "Invalid Request");
92
+ }
93
+
94
+ if (method === "initialize") {
95
+ const params = orEmptyObject(message.params) as Record<string, unknown>;
96
+ const requested = params.protocolVersion;
97
+ return result(id, {
98
+ protocolVersion: typeof requested === "string" ? requested : PROTOCOL_VERSION,
99
+ capabilities: { tools: {} },
100
+ serverInfo: { name: SERVER_NAME, version: options.serverVersion },
101
+ });
102
+ }
103
+
104
+ if (method === "notifications/initialized") return null;
105
+
106
+ if (method === "ping") return result(id, {});
107
+
108
+ if (method === "tools/list") {
109
+ return result(id, {
110
+ tools: registry.list.map((tool) => ({
111
+ name: tool.name,
112
+ description: tool.description,
113
+ inputSchema: tool.inputSchema,
114
+ })),
115
+ });
116
+ }
117
+
118
+ if (method === "tools/call") {
119
+ const params = orEmptyObject(message.params) as Record<string, unknown>;
120
+ const name = params.name;
121
+ const args = orEmptyObject(params.arguments);
122
+ const tool = typeof name === "string" ? registry.byName.get(name) : undefined;
123
+ if (tool === undefined) return error(id, -32602, `Unknown tool: ${pythonStr(name)}`);
124
+ try {
125
+ const text = await tool.handler(args);
126
+ return result(id, { content: [{ type: "text", text }], isError: false });
127
+ } catch (thrown) {
128
+ // Tool errors are results, not protocol errors: the client sees a readable
129
+ // failure and the session continues. A protocol error here would look to
130
+ // the client like a broken server.
131
+ return result(id, {
132
+ content: [{ type: "text", text: `Error: ${errorText(thrown)}` }],
133
+ isError: true,
134
+ });
135
+ }
136
+ }
137
+
138
+ // Unknown notifications are ignored; unknown requests get an error. Same
139
+ // method string, different outcome, decided only by the presence of `id`.
140
+ return isNotification ? null : error(id, -32601, `Method not found: ${method}`);
141
+ }