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,325 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { existsSync, readFileSync } from "node:fs";
3
+ import { isAbsolute, relative, resolve } from "node:path";
4
+ import { parse } from "yaml";
5
+ import type { Database } from "#db/database.ts";
6
+
7
+ export const MIRROR_CONTEXT_PROTOCOL = "mirror-context-v1";
8
+ const DEFAULT_TIMEOUT_MS = 60_000;
9
+ const DEFAULT_MAX_OUTPUT_BYTES = 1024 * 1024;
10
+ const EXTENSION_ID = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
11
+ const CAPABILITY_ID = /^[a-z][a-z0-9_-]*$/;
12
+
13
+ export interface ExtensionContextRequest {
14
+ protocol: typeof MIRROR_CONTEXT_PROTOCOL;
15
+ extension_id: string;
16
+ capability_id: string;
17
+ extension_root: string;
18
+ table_prefix: string;
19
+ database_path: string;
20
+ persona_id: string | null;
21
+ journey_id: string | null;
22
+ user: string;
23
+ query: string | null;
24
+ binding_kind: string;
25
+ binding_target: string | null;
26
+ }
27
+
28
+ export interface ExtensionContextSection {
29
+ extensionId: string;
30
+ capabilityId: string;
31
+ bindingKind: string;
32
+ bindingTarget: string | null;
33
+ text: string;
34
+ }
35
+
36
+ export type ExtensionContextDiagnosticKind =
37
+ | "missing_extension"
38
+ | "invalid_manifest"
39
+ | "unknown_capability"
40
+ /**
41
+ * The capability exists and is bound, but declares no `provider_runtime`.
42
+ *
43
+ * Before CV22.DS10.TS2 this case silently reached the Python compatibility
44
+ * host. With the host deleted there is nothing to reach, and the failure is
45
+ * SOFT on purpose: one missing provider must not cost the Navigator the
46
+ * whole `mirror load`. It is reported rather than swallowed so the section
47
+ * going dark is visible as a migration state, not as an empty context.
48
+ */
49
+ | "no_provider_runtime"
50
+ | "provider_failed"
51
+ | "provider_timeout"
52
+ | "invalid_output";
53
+
54
+ export interface ExtensionContextDiagnostic {
55
+ kind: ExtensionContextDiagnosticKind;
56
+ }
57
+
58
+ export interface CollectExtensionContextOptions {
59
+ mirrorHome: string;
60
+ databasePath: string;
61
+ personaId?: string | null;
62
+ journeyId?: string | null;
63
+ user?: string;
64
+ query?: string | null;
65
+ timeoutMs?: number;
66
+ maxOutputBytes?: number;
67
+ environment?: NodeJS.ProcessEnv;
68
+ }
69
+
70
+ interface Binding {
71
+ extensionId: string;
72
+ capabilityId: string;
73
+ targetKind: string;
74
+ targetId: string | null;
75
+ }
76
+
77
+ interface ProviderRuntime {
78
+ protocol: typeof MIRROR_CONTEXT_PROTOCOL;
79
+ command: string[];
80
+ }
81
+
82
+ interface Capability {
83
+ id: string;
84
+ providerRuntime: ProviderRuntime | null;
85
+ }
86
+
87
+ interface ExtensionManifest {
88
+ id: string;
89
+ tablePrefix: string;
90
+ capabilities: Capability[];
91
+ }
92
+
93
+ export interface CollectedExtensionContext {
94
+ sections: ExtensionContextSection[];
95
+ rendered: string;
96
+ diagnostics: ExtensionContextDiagnostic[];
97
+ }
98
+
99
+ export function selectExtensionBindings(
100
+ db: Database,
101
+ personaId?: string | null,
102
+ journeyId?: string | null,
103
+ ): Binding[] {
104
+ const clauses: string[] = [];
105
+ const params: string[] = [];
106
+ if (personaId) {
107
+ clauses.push("(target_kind = 'persona' AND target_id = ?)");
108
+ params.push(personaId);
109
+ }
110
+ if (journeyId) {
111
+ clauses.push("(target_kind = 'journey' AND target_id = ?)");
112
+ params.push(journeyId);
113
+ }
114
+ if (clauses.length === 0) return [];
115
+ return db
116
+ .prepare(
117
+ `SELECT extension_id, capability_id, target_kind, target_id
118
+ FROM _ext_bindings
119
+ WHERE ${clauses.join(" OR ")}
120
+ ORDER BY extension_id, capability_id, target_kind, target_id`,
121
+ )
122
+ .all(...params)
123
+ .map((row) => ({
124
+ extensionId: String(row.extension_id),
125
+ capabilityId: String(row.capability_id),
126
+ targetKind: String(row.target_kind),
127
+ targetId: typeof row.target_id === "string" ? row.target_id : null,
128
+ }));
129
+ }
130
+
131
+ export function renderExtensionSections(sections: readonly ExtensionContextSection[]): string {
132
+ return sections
133
+ .map((section) =>
134
+ [`=== extension/${section.extensionId}/${section.capabilityId} ===`, section.text].join("\n"),
135
+ )
136
+ .join("\n\n");
137
+ }
138
+
139
+ export function collectExtensionContext(
140
+ db: Database,
141
+ options: CollectExtensionContextOptions,
142
+ ): CollectedExtensionContext {
143
+ const sections: ExtensionContextSection[] = [];
144
+ const diagnostics: ExtensionContextDiagnostic[] = [];
145
+ const bindings = selectExtensionBindings(db, options.personaId, options.journeyId);
146
+ for (const binding of bindings) {
147
+ const extensionRoot = resolve(options.mirrorHome, "extensions", binding.extensionId);
148
+ if (!existsSync(extensionRoot)) {
149
+ diagnostics.push({ kind: "missing_extension" });
150
+ continue;
151
+ }
152
+ let manifest: ExtensionManifest;
153
+ try {
154
+ manifest = readContextManifest(extensionRoot);
155
+ } catch {
156
+ diagnostics.push({ kind: "invalid_manifest" });
157
+ continue;
158
+ }
159
+ const capability = manifest.capabilities.find(
160
+ (candidate) => candidate.id === binding.capabilityId,
161
+ );
162
+ if (!capability) {
163
+ diagnostics.push({ kind: "unknown_capability" });
164
+ continue;
165
+ }
166
+ const request: ExtensionContextRequest = {
167
+ protocol: MIRROR_CONTEXT_PROTOCOL,
168
+ extension_id: binding.extensionId,
169
+ capability_id: binding.capabilityId,
170
+ extension_root: extensionRoot,
171
+ table_prefix: manifest.tablePrefix,
172
+ database_path: resolve(options.databasePath),
173
+ persona_id: options.personaId ?? null,
174
+ journey_id: options.journeyId ?? null,
175
+ user: options.user ?? "",
176
+ query: options.query ?? null,
177
+ binding_kind: binding.targetKind,
178
+ binding_target: binding.targetId,
179
+ };
180
+ if (!capability.providerRuntime) {
181
+ diagnostics.push({ kind: "no_provider_runtime" });
182
+ continue;
183
+ }
184
+ const outcome = invokeProvider(
185
+ capability.providerRuntime.command,
186
+ extensionRoot,
187
+ request,
188
+ options,
189
+ );
190
+ if (outcome.diagnostic) {
191
+ diagnostics.push({ kind: outcome.diagnostic });
192
+ continue;
193
+ }
194
+ if (!outcome.text) continue;
195
+ sections.push({
196
+ extensionId: binding.extensionId,
197
+ capabilityId: binding.capabilityId,
198
+ bindingKind: binding.targetKind,
199
+ bindingTarget: binding.targetId,
200
+ text: outcome.text,
201
+ });
202
+ }
203
+ return { sections, rendered: renderExtensionSections(sections), diagnostics };
204
+ }
205
+
206
+ /**
207
+ * Bound capability ids that declare no `provider_runtime`, for `install` to
208
+ * report.
209
+ *
210
+ * Tolerant like every other manifest read on this path: a manifest that cannot
211
+ * be parsed has nothing to report here, and `install` already refuses it
212
+ * through the validator that owns that judgment.
213
+ */
214
+ export function readProvidersWithoutRuntime(extensionRoot: string): string[] {
215
+ try {
216
+ return readContextManifest(extensionRoot)
217
+ .capabilities.filter((capability) => capability.providerRuntime === null)
218
+ .map((capability) => capability.id);
219
+ } catch {
220
+ return [];
221
+ }
222
+ }
223
+
224
+ function readContextManifest(extensionRoot: string): ExtensionManifest {
225
+ const manifestPath = resolve(extensionRoot, "skill.yaml");
226
+ const raw = parse(readFileSync(manifestPath, "utf8")) as unknown;
227
+ if (!isRecord(raw) || typeof raw.id !== "string" || !EXTENSION_ID.test(raw.id)) throw new Error();
228
+ if (resolve(extensionRoot).split(/[\\/]/).at(-1) !== raw.id) throw new Error();
229
+ const expectedPrefix = tablePrefix(raw.id);
230
+ if (raw.table_prefix !== undefined && raw.table_prefix !== expectedPrefix) throw new Error();
231
+ const providers = raw.mirror_context_providers;
232
+ if (providers === undefined || providers === null)
233
+ return { id: raw.id, tablePrefix: expectedPrefix, capabilities: [] };
234
+ if (!Array.isArray(providers)) throw new Error();
235
+ const seen = new Set<string>();
236
+ const capabilities = providers.map((provider): Capability => {
237
+ if (!isRecord(provider) || typeof provider.id !== "string" || !CAPABILITY_ID.test(provider.id))
238
+ throw new Error();
239
+ if (seen.has(provider.id)) throw new Error();
240
+ seen.add(provider.id);
241
+ const runtime = provider.provider_runtime;
242
+ if (runtime === undefined || runtime === null)
243
+ return { id: provider.id, providerRuntime: null };
244
+ if (
245
+ !isRecord(runtime) ||
246
+ runtime.protocol !== MIRROR_CONTEXT_PROTOCOL ||
247
+ !Array.isArray(runtime.command) ||
248
+ runtime.command.length === 0 ||
249
+ !runtime.command.every((part) => typeof part === "string" && part.length > 0)
250
+ )
251
+ throw new Error();
252
+ validateCommandPaths(extensionRoot, runtime.command as string[]);
253
+ return {
254
+ id: provider.id,
255
+ providerRuntime: {
256
+ protocol: MIRROR_CONTEXT_PROTOCOL,
257
+ command: [...(runtime.command as string[])],
258
+ },
259
+ };
260
+ });
261
+ return {
262
+ id: raw.id,
263
+ tablePrefix: expectedPrefix,
264
+ capabilities,
265
+ };
266
+ }
267
+
268
+ function validateCommandPaths(extensionRoot: string, command: readonly string[]): void {
269
+ for (const [index, argument] of command.entries()) {
270
+ const pathLike =
271
+ isAbsolute(argument) ||
272
+ argument.includes("/") ||
273
+ argument.includes("\\") ||
274
+ (index > 0 && /\.(?:[cm]?js|py)$/i.test(argument));
275
+ if (!pathLike) continue;
276
+ const candidate = resolve(extensionRoot, argument);
277
+ const rel = relative(resolve(extensionRoot), candidate);
278
+ if (isAbsolute(argument) || rel === ".." || rel.startsWith("../") || rel.startsWith("..\\"))
279
+ throw new Error();
280
+ if (!existsSync(candidate)) throw new Error();
281
+ }
282
+ }
283
+
284
+ function invokeProvider(
285
+ command: readonly string[],
286
+ cwd: string,
287
+ request: ExtensionContextRequest,
288
+ options: CollectExtensionContextOptions,
289
+ ): { text?: string | null; diagnostic?: ExtensionContextDiagnosticKind } {
290
+ const result = spawnSync(command[0] as string, command.slice(1), {
291
+ cwd,
292
+ encoding: "utf8",
293
+ input: `${JSON.stringify(request)}\n`,
294
+ env: options.environment ?? process.env,
295
+ timeout: options.timeoutMs ?? DEFAULT_TIMEOUT_MS,
296
+ maxBuffer: options.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES,
297
+ windowsHide: true,
298
+ shell: false,
299
+ });
300
+ if (result.error) {
301
+ const code = (result.error as NodeJS.ErrnoException).code;
302
+ return { diagnostic: code === "ETIMEDOUT" ? "provider_timeout" : "provider_failed" };
303
+ }
304
+ if (result.signal || result.status !== 0) return { diagnostic: "provider_failed" };
305
+ try {
306
+ const parsed = JSON.parse(result.stdout.trim()) as unknown;
307
+ if (
308
+ !isRecord(parsed) ||
309
+ parsed.protocol !== MIRROR_CONTEXT_PROTOCOL ||
310
+ !(parsed.text === null || typeof parsed.text === "string")
311
+ )
312
+ return { diagnostic: "invalid_output" };
313
+ return { text: parsed.text as string | null };
314
+ } catch {
315
+ return { diagnostic: "invalid_output" };
316
+ }
317
+ }
318
+
319
+ function tablePrefix(extensionId: string): string {
320
+ return `ext_${extensionId.replaceAll("-", "_")}_`;
321
+ }
322
+
323
+ function isRecord(value: unknown): value is Record<string, unknown> {
324
+ return typeof value === "object" && value !== null && !Array.isArray(value);
325
+ }
@@ -0,0 +1,374 @@
1
+ // `ext <id>` and `ext <id> <subcommand>` (CV22.DS7.TS4 plateau 4).
2
+ //
3
+ // The dispatch is the one leaf in DS7 whose behavior is decided by code Mirror
4
+ // does not own. Navigator decision D1 (2026-09-16) split it in two:
5
+ //
6
+ // * TypeScript owns everything the dispatcher can decide by itself -- argv
7
+ // splitting, the built-in verbs, the help guard, the installed check, and
8
+ // the exit code. That half lives in `catalogCommands.ts` and produces the
9
+ // `ExtensionDispatch` DECISION below, without running anything.
10
+ // * Execution is this module.
11
+ //
12
+ // CV22.DS10.TS2 closed that gate. The Python compatibility host is deleted, so
13
+ // the ONLY thing this module executes is a subcommand whose extension declares
14
+ // a language-neutral `mirror-cli-v1` runtime. Everything else -- the listing,
15
+ // an unknown subcommand, a subcommand documented without a runtime -- is now a
16
+ // DECISION rendered as bytes in `catalogCommands.ts`, with no process at all.
17
+ // `readSubcommandListing` and `renderSubcommandListing` therefore live here,
18
+ // beside the manifest reader they share, but are called from the decision
19
+ // layer: parsing the manifest is this module's knowledge; choosing what to
20
+ // print is not.
21
+ //
22
+ // TWO measured facts shape this file, both recorded in the golden:
23
+ //
24
+ // 1. `_installed_extension_dir` builds its path with `Path.__truediv__` and
25
+ // NEVER normalizes it, and that path is printed. `ext ../../etc ping`
26
+ // answers `extension not installed: <home>/extensions/../../etc`, and an
27
+ // ABSOLUTE id discards the extensions root entirely (`ext /etc ping`
28
+ // reads `/etc/skill.yaml`). `join()` would quietly repair both, which is
29
+ // the divergence this module exists to avoid.
30
+ // 2. Streams are INHERITED, not captured. A command's output is its product:
31
+ // it must stream as it is produced, keep stdout/stderr interleaving, and
32
+ // preserve the handler's exit code unchanged (a handler returning 3 exits
33
+ // 3; `int("7")` exits 7).
34
+ //
35
+ // KNOWN LIMIT, recorded rather than papered over: the request travels on the
36
+ // host's stdin, so a legacy handler cannot read the user's stdin through the
37
+ // bridge. Measured first -- no CLI handler in any installed extension reads
38
+ // stdin -- and the declared-runtime path is free to define stdin passthrough
39
+ // when it lands.
40
+
41
+ import { spawnSync } from "node:child_process";
42
+ import { existsSync, readFileSync } from "node:fs";
43
+ import { isAbsolute, join, relative, resolve } from "node:path";
44
+ import { parse } from "yaml";
45
+ import type { RenderedCommand } from "./catalog.ts";
46
+ import { tablePrefixFor } from "./migrations.ts";
47
+
48
+ export const MIRROR_CLI_PROTOCOL = "mirror-cli-v1";
49
+
50
+ /** The extension id rule, unchanged since the catalog defined it. */
51
+ const EXTENSION_ID = /^[a-z][a-z0-9-]*$/;
52
+
53
+ /** Generous but finite, like the front door's Python fallback. */
54
+ const DEFAULT_TIMEOUT_MS = 10 * 60 * 1000;
55
+
56
+ /**
57
+ * What the pure dispatcher decided, for a caller that is allowed to spawn.
58
+ *
59
+ * `subcommand` is `--help` for the `ext <id>` listing, which is how the Python
60
+ * dispatcher already models it: the listing is a dispatch, not a separate
61
+ * command, and it loads the extension exactly like any other subcommand.
62
+ */
63
+ export interface ExtensionDispatch {
64
+ kind: "extension-subcommand";
65
+ extensionId: string;
66
+ subcommand: string;
67
+ argv: readonly string[];
68
+ mirrorHome: string;
69
+ extensionRoot: string;
70
+ /**
71
+ * The declared command to run, resolved by the decision layer.
72
+ *
73
+ * Carried on the decision rather than re-read here: deciding that this
74
+ * subcommand was executable at all already required parsing the manifest,
75
+ * and reading it twice is how two layers drift.
76
+ */
77
+ command: readonly string[];
78
+ }
79
+
80
+ export interface DispatchOptions {
81
+ databasePath: string;
82
+ environment?: NodeJS.ProcessEnv;
83
+ timeoutMs?: number;
84
+ /** Injected so tests can assert what would be spawned without spawning. */
85
+ spawn?: typeof spawnSync;
86
+ onError?: (message: string) => void;
87
+ }
88
+
89
+ /**
90
+ * One `cli.subcommands[]` entry as the listing needs it.
91
+ *
92
+ * `hasRuntime` is the migration state made visible: an entry the manifest
93
+ * documents but declares no runtime for is listed, flagged, and refused --
94
+ * never silently absent, which would read as "the extension lost a command"
95
+ * rather than "this command needs migrating".
96
+ */
97
+ export interface SubcommandEntry {
98
+ name: string;
99
+ summary: string;
100
+ hasRuntime: boolean;
101
+ }
102
+
103
+ /**
104
+ * A subcommand's declared, language-neutral command.
105
+ *
106
+ * It hangs off the `cli.subcommands[]` entries the manifest format already
107
+ * had, rather than a parallel `commands[]` array: an extension keeps ONE list
108
+ * of its subcommands, so a declared name and a documented name cannot drift.
109
+ */
110
+ interface DeclaredCommand {
111
+ name: string;
112
+ command: string[];
113
+ }
114
+
115
+ /**
116
+ * Read the declared commands, tolerantly.
117
+ *
118
+ * Deliberately NOT part of `loadExtensionManifest`: that function is a port of
119
+ * Python's validator, graded byte for byte by the catalog reads, and Python
120
+ * ignores `cli:` entirely. A TypeScript-only field that could FAIL validation
121
+ * would make `extensions list` report an invalid extension where Python
122
+ * reports a valid one.
123
+ *
124
+ * So every structural problem here -- unreadable manifest, wrong protocol,
125
+ * empty argv, a path escaping the extension root -- drops that one entry and
126
+ * lets the host answer, which is exactly what Python does today. A declaration
127
+ * TypeScript will not execute is never a refusal; it is a fallback.
128
+ */
129
+ export function readDeclaredCommands(extensionRoot: string): DeclaredCommand[] {
130
+ let parsed: unknown;
131
+ try {
132
+ parsed = parse(readFileSync(join(extensionRoot, "skill.yaml"), "utf8"));
133
+ } catch {
134
+ return [];
135
+ }
136
+ if (!isRecord(parsed) || !isRecord(parsed.cli)) return [];
137
+ const subcommands = parsed.cli.subcommands;
138
+ if (!Array.isArray(subcommands)) return [];
139
+ const declared: DeclaredCommand[] = [];
140
+ const seen = new Set<string>();
141
+ for (const entry of subcommands) {
142
+ if (!isRecord(entry) || typeof entry.name !== "string" || entry.name.length === 0) continue;
143
+ if (seen.has(entry.name)) continue;
144
+ seen.add(entry.name);
145
+ const runtime = entry.runtime;
146
+ if (!isRecord(runtime) || runtime.protocol !== MIRROR_CLI_PROTOCOL) continue;
147
+ const command = runtime.command;
148
+ if (
149
+ !Array.isArray(command) ||
150
+ command.length === 0 ||
151
+ !command.every((part) => typeof part === "string" && part.length > 0)
152
+ ) {
153
+ continue;
154
+ }
155
+ if (!commandStaysInside(extensionRoot, command as string[])) continue;
156
+ declared.push({ name: entry.name, command: [...(command as string[])] });
157
+ }
158
+ return declared;
159
+ }
160
+
161
+ /**
162
+ * Path-like arguments must resolve inside the installed extension root, the
163
+ * same rule `provider_runtime.command` follows. An extension is trusted code,
164
+ * but a manifest that reaches OUT of its own directory is not a contract --
165
+ * it is a way to make the core launch something the installer never placed.
166
+ */
167
+ function commandStaysInside(extensionRoot: string, command: readonly string[]): boolean {
168
+ const root = resolve(extensionRoot);
169
+ for (const [index, argument] of command.entries()) {
170
+ const pathLike =
171
+ isAbsolute(argument) ||
172
+ argument.includes("/") ||
173
+ argument.includes("\\") ||
174
+ (index > 0 && /\.(?:[cm]?js|py)$/i.test(argument));
175
+ if (!pathLike) continue;
176
+ if (isAbsolute(argument)) return false;
177
+ const candidate = resolve(root, argument);
178
+ const rel = relative(root, candidate);
179
+ if (rel === ".." || rel.startsWith(`..${"/"}`) || rel.startsWith("..\\")) return false;
180
+ if (!existsSync(candidate)) return false;
181
+ }
182
+ return true;
183
+ }
184
+
185
+ /**
186
+ * The context a declared command receives.
187
+ *
188
+ * In the ENVIRONMENT, never on stdin: stdin belongs to the user, which is the
189
+ * one thing the legacy bridge cannot offer. Argv is appended to the declared
190
+ * command verbatim, so a declared command sees exactly what a legacy handler
191
+ * sees.
192
+ */
193
+ function declaredEnvironment(
194
+ dispatch: ExtensionDispatch,
195
+ options: DispatchOptions,
196
+ ): NodeJS.ProcessEnv {
197
+ return {
198
+ ...(options.environment ?? process.env),
199
+ MIRROR_HOME: dispatch.mirrorHome,
200
+ MIRROR_DATABASE_PATH: options.databasePath,
201
+ MIRROR_EXTENSION_ID: dispatch.extensionId,
202
+ MIRROR_EXTENSION_ROOT: dispatch.extensionRoot,
203
+ MIRROR_TABLE_PREFIX: tablePrefixFor(dispatch.extensionId),
204
+ };
205
+ }
206
+
207
+ function isRecord(value: unknown): value is Record<string, unknown> {
208
+ return typeof value === "object" && value !== null && !Array.isArray(value);
209
+ }
210
+
211
+ /**
212
+ * Python's `PurePath.__truediv__`, which is NOT `node:path.join`.
213
+ *
214
+ * Empty and `.` segments vanish, a trailing slash is dropped, `..` is
215
+ * PRESERVED, and an absolute segment replaces the whole left side. Every one
216
+ * of those is an observable byte in `extension not installed: <path>`.
217
+ */
218
+ export function pythonPathJoin(root: string, segment: string): string {
219
+ const absolute = segment.startsWith("/");
220
+ const parts = segment.split("/").filter((part) => part !== "" && part !== ".");
221
+ if (absolute) return parts.length === 0 ? "/" : `/${parts.join("/")}`;
222
+ if (parts.length === 0) return root;
223
+ const separator = root.endsWith("/") ? "" : "/";
224
+ return `${root}${separator}${parts.join("/")}`;
225
+ }
226
+
227
+ /** Port of `_installed_extension_dir`. */
228
+ export function installedExtensionDir(mirrorHome: string, extensionId: string): string {
229
+ return pythonPathJoin(pythonPathJoin(mirrorHome, "extensions"), extensionId);
230
+ }
231
+
232
+ /**
233
+ * Port of the `not ext_dir.exists()` branch shared by `_dispatch_subcommand`
234
+ * and `_cmd_migrate`: one printed line on stdout at exit 1, with the
235
+ * unnormalized path.
236
+ */
237
+ export function extensionNotInstalled(mirrorHome: string, extensionId: string): RenderedCommand {
238
+ return {
239
+ stdout: `extension not installed: ${installedExtensionDir(mirrorHome, extensionId)}\n`,
240
+ stderr: "",
241
+ exitCode: 1,
242
+ };
243
+ }
244
+
245
+ export function isExtensionDispatch(value: unknown): value is ExtensionDispatch {
246
+ return (
247
+ typeof value === "object" &&
248
+ value !== null &&
249
+ (value as ExtensionDispatch).kind === "extension-subcommand"
250
+ );
251
+ }
252
+
253
+ /**
254
+ * Every documented subcommand, declared or not, in manifest order.
255
+ *
256
+ * Tolerant for the same reason `readDeclaredCommands` is: a manifest problem
257
+ * must never turn a listing into a refusal. An unreadable or shapeless
258
+ * manifest simply has no subcommands to list.
259
+ */
260
+ export function readSubcommandListing(extensionRoot: string): SubcommandEntry[] {
261
+ let parsed: unknown;
262
+ try {
263
+ parsed = parse(readFileSync(join(extensionRoot, "skill.yaml"), "utf8"));
264
+ } catch {
265
+ return [];
266
+ }
267
+ if (!isRecord(parsed) || !isRecord(parsed.cli)) return [];
268
+ const subcommands = parsed.cli.subcommands;
269
+ if (!Array.isArray(subcommands)) return [];
270
+ const declared = new Set(readDeclaredCommands(extensionRoot).map((entry) => entry.name));
271
+ const entries: SubcommandEntry[] = [];
272
+ const seen = new Set<string>();
273
+ for (const entry of subcommands) {
274
+ if (!isRecord(entry) || typeof entry.name !== "string" || entry.name.length === 0) continue;
275
+ if (seen.has(entry.name)) continue;
276
+ seen.add(entry.name);
277
+ entries.push({
278
+ name: entry.name,
279
+ summary: typeof entry.summary === "string" ? entry.summary : "",
280
+ hasRuntime: declared.has(entry.name),
281
+ });
282
+ }
283
+ return entries;
284
+ }
285
+
286
+ /**
287
+ * The `ext <id>` listing, rendered from the manifest.
288
+ *
289
+ * Python rendered this from the LIVE `api.cli_registry`, so a handler
290
+ * registered but never documented appeared, and a documented-but-unregistered
291
+ * one did not. With the host gone the manifest is the only source, which makes
292
+ * this TypeScript's surface from here on -- a recorded divergence, not a
293
+ * parity failure. Sorted by name, as Python sorted its registry, so the bytes
294
+ * stay stable across manifest reordering.
295
+ */
296
+ export function renderSubcommandListing(extensionId: string, entries: SubcommandEntry[]): string {
297
+ const lines = [`=== subcommands of extension/${extensionId} ===`];
298
+ if (entries.length === 0) {
299
+ lines.push(" (none declared)");
300
+ return `${lines.join("\n")}\n`;
301
+ }
302
+ const sorted = [...entries].sort((left, right) => (left.name < right.name ? -1 : 1));
303
+ for (const entry of sorted) {
304
+ const summary = entry.summary ? ` — ${entry.summary}` : "";
305
+ const flag = entry.hasRuntime ? "" : " (no runtime declared)";
306
+ lines.push(` ${entry.name}${summary}${flag}`);
307
+ }
308
+ return `${lines.join("\n")}\n`;
309
+ }
310
+
311
+ /**
312
+ * What a documented subcommand with no declared runtime says now.
313
+ *
314
+ * One line naming the extension, the subcommand, and the fix. No traceback, no
315
+ * spawn, no partial output: the command did not run, and the message says why
316
+ * in the vocabulary of the thing the user has to change.
317
+ */
318
+ export function noRuntimeRefusal(extensionId: string, subcommand: string): string {
319
+ return (
320
+ `Mirror: extension/${extensionId} declares no runtime for '${subcommand}'. ` +
321
+ `The Python compatibility host was retired — declare ` +
322
+ `cli.subcommands[].runtime (${MIRROR_CLI_PROTOCOL}) in skill.yaml. ` +
323
+ `See docs/releases/pending-cutoffs.md.\n`
324
+ );
325
+ }
326
+
327
+ /**
328
+ * Execute one DECLARED extension subcommand and return its exit code.
329
+ *
330
+ * Every other outcome was decided before this is called, so there is exactly
331
+ * one process here and one way to fail to start it. Refuses an id that cannot
332
+ * be a legal extension id BEFORE spawning: the installed check has already
333
+ * passed, so such an id can only be a traversal attempt at a real directory.
334
+ */
335
+ export function runExtensionSubcommand(
336
+ dispatch: ExtensionDispatch,
337
+ options: DispatchOptions,
338
+ ): number {
339
+ const report = options.onError ?? ((message: string) => process.stderr.write(`${message}\n`));
340
+ if (!EXTENSION_ID.test(dispatch.extensionId)) {
341
+ report(`Mirror: '${dispatch.extensionId}' is not a valid extension id.`);
342
+ return 1;
343
+ }
344
+ if (!existsSync(dispatch.extensionRoot)) return 1;
345
+
346
+ const [executable, ...head] = dispatch.command;
347
+ const spawn = options.spawn ?? spawnSync;
348
+ const result = spawn(executable as string, [...head, ...dispatch.argv], {
349
+ cwd: dispatch.extensionRoot,
350
+ env: declaredEnvironment(dispatch, options),
351
+ // All three streams are the user's: a declared command may read stdin,
352
+ // which is precisely what the retired bridge could never allow.
353
+ stdio: "inherit",
354
+ timeout: options.timeoutMs ?? DEFAULT_TIMEOUT_MS,
355
+ windowsHide: true,
356
+ shell: false,
357
+ });
358
+ if (result.error) {
359
+ const code = (result.error as NodeJS.ErrnoException).code;
360
+ if (code === "ETIMEDOUT") {
361
+ report(
362
+ `Mirror: extension/${dispatch.extensionId} '${dispatch.subcommand}' timed out after ` +
363
+ `${options.timeoutMs ?? DEFAULT_TIMEOUT_MS}ms.`,
364
+ );
365
+ } else {
366
+ report(
367
+ `Mirror: extension/${dispatch.extensionId} declares a command for ` +
368
+ `'${dispatch.subcommand}' that could not be started: ${result.error.message}`,
369
+ );
370
+ }
371
+ return 1;
372
+ }
373
+ return typeof result.status === "number" ? result.status : 1;
374
+ }