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.
- package/.claude/hooks/log-assistant-reply.sh +85 -0
- package/.claude/hooks/log-session-end.sh +85 -0
- package/.claude/hooks/log-user-prompt.sh +85 -0
- package/.claude/hooks/mirror-inject.sh +85 -0
- package/.claude/hooks/session-start.sh +85 -0
- package/.gemini/hooks/log-assistant.sh +85 -0
- package/.gemini/hooks/log-user.sh +85 -0
- package/.gemini/hooks/session-end.sh +85 -0
- package/.gemini/hooks/session-start.sh +85 -0
- package/.pi/extensions/mirror-logger.ts +646 -0
- package/.pi/skills/mm-backup/SKILL.md +21 -0
- package/.pi/skills/mm-build/SKILL.md +1206 -0
- package/.pi/skills/mm-consolidate/SKILL.md +112 -0
- package/.pi/skills/mm-consult/SKILL.md +70 -0
- package/.pi/skills/mm-conversations/SKILL.md +16 -0
- package/.pi/skills/mm-discard/SKILL.md +39 -0
- package/.pi/skills/mm-explore/SKILL.md +255 -0
- package/.pi/skills/mm-help/SKILL.md +74 -0
- package/.pi/skills/mm-identity/SKILL.md +96 -0
- package/.pi/skills/mm-journal/SKILL.md +16 -0
- package/.pi/skills/mm-journey/SKILL.md +23 -0
- package/.pi/skills/mm-journeys/SKILL.md +15 -0
- package/.pi/skills/mm-memories/SKILL.md +17 -0
- package/.pi/skills/mm-mirror/SKILL.md +98 -0
- package/.pi/skills/mm-mute/SKILL.md +18 -0
- package/.pi/skills/mm-new/SKILL.md +18 -0
- package/.pi/skills/mm-recall/SKILL.md +17 -0
- package/.pi/skills/mm-release-notes/SKILL.md +52 -0
- package/.pi/skills/mm-seed/SKILL.md +19 -0
- package/.pi/skills/mm-shadow/SKILL.md +117 -0
- package/.pi/skills/mm-soul/SKILL.md +502 -0
- package/.pi/skills/mm-tasks/SKILL.md +24 -0
- package/.pi/skills/mm-update/SKILL.md +32 -0
- package/.pi/skills/mm-week/SKILL.md +28 -0
- package/.pi/skills/mm-welcome/SKILL.md +24 -0
- package/AGENTS.md +188 -0
- package/README.md +126 -9
- package/bin/loader.js +67 -0
- package/bin/mirror-hook.js +7 -0
- package/bin/mirror.js +7 -0
- package/docs/releases/index.md +100 -0
- package/docs/releases/pending-cutoffs.md +510 -0
- package/docs/releases/v0.10.0.md +123 -0
- package/docs/releases/v0.10.1.md +66 -0
- package/docs/releases/v0.10.10.md +51 -0
- package/docs/releases/v0.10.11.md +39 -0
- package/docs/releases/v0.10.12.md +57 -0
- package/docs/releases/v0.10.13.md +35 -0
- package/docs/releases/v0.10.14.md +42 -0
- package/docs/releases/v0.10.15.md +53 -0
- package/docs/releases/v0.10.16.md +35 -0
- package/docs/releases/v0.10.17.md +43 -0
- package/docs/releases/v0.10.2.md +64 -0
- package/docs/releases/v0.10.3.md +78 -0
- package/docs/releases/v0.10.4.md +42 -0
- package/docs/releases/v0.10.5.md +57 -0
- package/docs/releases/v0.10.6.md +47 -0
- package/docs/releases/v0.10.7.md +53 -0
- package/docs/releases/v0.10.8.md +49 -0
- package/docs/releases/v0.10.9.md +37 -0
- package/docs/releases/v0.11.0.md +86 -0
- package/docs/releases/v0.12.0.md +67 -0
- package/docs/releases/v0.13.0.md +64 -0
- package/docs/releases/v0.14.0.md +68 -0
- package/docs/releases/v0.15.0.md +48 -0
- package/docs/releases/v0.16.0.md +50 -0
- package/docs/releases/v0.16.1.md +55 -0
- package/docs/releases/v0.16.2.md +36 -0
- package/docs/releases/v0.17.0.md +50 -0
- package/docs/releases/v0.17.1.md +58 -0
- package/docs/releases/v0.17.2.md +42 -0
- package/docs/releases/v0.17.3.md +58 -0
- package/docs/releases/v0.18.0.md +84 -0
- package/docs/releases/v0.18.1.md +51 -0
- package/docs/releases/v0.18.2.md +69 -0
- package/docs/releases/v0.19.0.md +85 -0
- package/docs/releases/v0.19.1.md +46 -0
- package/docs/releases/v0.20.0.md +85 -0
- package/docs/releases/v0.21.0.md +61 -0
- package/docs/releases/v0.21.1.md +47 -0
- package/docs/releases/v0.22.0.md +77 -0
- package/docs/releases/v0.22.1.md +42 -0
- package/docs/releases/v0.23.0.md +105 -0
- package/docs/releases/v0.24.0.md +125 -0
- package/docs/releases/v0.25.0.md +107 -0
- package/docs/releases/v0.26.0.md +139 -0
- package/docs/releases/v0.26.1.md +65 -0
- package/docs/releases/v0.26.2.md +43 -0
- package/docs/releases/v0.27.0.md +63 -0
- package/docs/releases/v0.28.0.md +59 -0
- package/docs/releases/v0.29.0.md +61 -0
- package/docs/releases/v0.29.1.md +59 -0
- package/docs/releases/v0.29.2.md +59 -0
- package/docs/releases/v0.29.3.md +93 -0
- package/docs/releases/v0.30.0.md +75 -0
- package/docs/releases/v0.30.1.md +93 -0
- package/docs/releases/v0.30.2.md +66 -0
- package/docs/releases/v0.31.0.md +122 -0
- package/docs/releases/v0.31.1.md +142 -0
- package/docs/releases/v0.31.10.md +118 -0
- package/docs/releases/v0.31.11.md +88 -0
- package/docs/releases/v0.31.12.md +105 -0
- package/docs/releases/v0.31.13.md +99 -0
- package/docs/releases/v0.31.14.md +97 -0
- package/docs/releases/v0.31.2.md +115 -0
- package/docs/releases/v0.31.3.md +142 -0
- package/docs/releases/v0.31.4.md +117 -0
- package/docs/releases/v0.31.5.md +155 -0
- package/docs/releases/v0.31.6.md +132 -0
- package/docs/releases/v0.31.7.md +132 -0
- package/docs/releases/v0.31.8.md +120 -0
- package/docs/releases/v0.31.9.md +120 -0
- package/docs/releases/v0.32.0.md +261 -0
- package/docs/releases/v0.32.1.md +59 -0
- package/docs/releases/v0.33.0.md +164 -0
- package/docs/releases/v0.8.0.md +118 -0
- package/docs/releases/v0.9.0.md +98 -0
- package/docs/releases/v0.9.1.md +83 -0
- package/package.json +102 -6
- package/plugins/mirror-mind/.claude-plugin/plugin.json +17 -0
- package/plugins/mirror-mind/hooks/hooks.json +62 -0
- package/plugins/mirror-mind/hooks/log-assistant-reply.sh +98 -0
- package/plugins/mirror-mind/hooks/log-session-end.sh +98 -0
- package/plugins/mirror-mind/hooks/log-user-prompt.sh +98 -0
- package/plugins/mirror-mind/hooks/mirror-inject.sh +98 -0
- package/plugins/mirror-mind/hooks/session-start.sh +98 -0
- package/plugins/mirror-mind/skills/mm-backup/SKILL.md +27 -0
- package/plugins/mirror-mind/skills/mm-build/SKILL.md +1206 -0
- package/plugins/mirror-mind/skills/mm-consolidate/SKILL.md +112 -0
- package/plugins/mirror-mind/skills/mm-consult/SKILL.md +83 -0
- package/plugins/mirror-mind/skills/mm-conversations/SKILL.md +17 -0
- package/plugins/mirror-mind/skills/mm-discard/SKILL.md +40 -0
- package/plugins/mirror-mind/skills/mm-explore/SKILL.md +241 -0
- package/plugins/mirror-mind/skills/mm-help/SKILL.md +104 -0
- package/plugins/mirror-mind/skills/mm-identity/SKILL.md +96 -0
- package/plugins/mirror-mind/skills/mm-journal/SKILL.md +21 -0
- package/plugins/mirror-mind/skills/mm-journey/SKILL.md +37 -0
- package/plugins/mirror-mind/skills/mm-journeys/SKILL.md +15 -0
- package/plugins/mirror-mind/skills/mm-memories/SKILL.md +20 -0
- package/plugins/mirror-mind/skills/mm-mirror/SKILL.md +67 -0
- package/plugins/mirror-mind/skills/mm-mute/SKILL.md +18 -0
- package/plugins/mirror-mind/skills/mm-new/SKILL.md +18 -0
- package/plugins/mirror-mind/skills/mm-recall/SKILL.md +17 -0
- package/plugins/mirror-mind/skills/mm-release-notes/SKILL.md +31 -0
- package/plugins/mirror-mind/skills/mm-seed/SKILL.md +15 -0
- package/plugins/mirror-mind/skills/mm-shadow/SKILL.md +117 -0
- package/plugins/mirror-mind/skills/mm-soul/SKILL.md +488 -0
- package/plugins/mirror-mind/skills/mm-tasks/SKILL.md +36 -0
- package/plugins/mirror-mind/skills/mm-update/SKILL.md +30 -0
- package/plugins/mirror-mind/skills/mm-week/SKILL.md +46 -0
- package/plugins/mirror-mind/skills/mm-welcome/SKILL.md +22 -0
- package/scripts/codex-hooks/session-end.sh +85 -0
- package/scripts/codex-hooks/session-start.sh +85 -0
- package/scripts/codex-mirror.sh +48 -0
- package/templates/identity/README.md +119 -0
- package/templates/identity/ego/behavior.yaml +52 -0
- package/templates/identity/ego/constraints.yaml +15 -0
- package/templates/identity/ego/identity.yaml +43 -0
- package/templates/identity/journeys/personal-growth.yaml +39 -0
- package/templates/identity/organization/identity.yaml +24 -0
- package/templates/identity/organization/principles.yaml +17 -0
- package/templates/identity/personas/coach.yaml +94 -0
- package/templates/identity/personas/designer.yaml +93 -0
- package/templates/identity/personas/doctor.yaml +96 -0
- package/templates/identity/personas/engineer.yaml +77 -0
- package/templates/identity/personas/financial.yaml +97 -0
- package/templates/identity/personas/prompt-engineer.yaml +122 -0
- package/templates/identity/personas/researcher.yaml +88 -0
- package/templates/identity/personas/strategist.yaml +91 -0
- package/templates/identity/personas/teacher.yaml +95 -0
- package/templates/identity/personas/therapist.yaml +91 -0
- package/templates/identity/personas/thinker.yaml +74 -0
- package/templates/identity/personas/writer.yaml +76 -0
- package/templates/identity/self/config.yaml +16 -0
- package/templates/identity/self/soul.yaml +105 -0
- package/templates/identity/user/identity.yaml +30 -0
- package/ts/src/backup/staging.ts +62 -0
- package/ts/src/backup/zipBackup.ts +267 -0
- package/ts/src/backup/zipReader.ts +109 -0
- package/ts/src/backup/zipWriter.ts +148 -0
- package/ts/src/builder/activeCheckpoint.ts +134 -0
- package/ts/src/builder/aggregateStatus.ts +34 -0
- package/ts/src/builder/approve.ts +149 -0
- package/ts/src/builder/argv.ts +292 -0
- package/ts/src/builder/ariadMethod.ts +536 -0
- package/ts/src/builder/artifacts/artifactSurfaces.ts +106 -0
- package/ts/src/builder/artifacts/artifactWriter.ts +113 -0
- package/ts/src/builder/artifacts/closureArtifacts.ts +199 -0
- package/ts/src/builder/artifacts/deliveryStoryArtifacts.ts +316 -0
- package/ts/src/builder/artifacts/planArtifacts.ts +242 -0
- package/ts/src/builder/artifacts/scaffoldSections.ts +273 -0
- package/ts/src/builder/artifacts/scaffoldState.ts +227 -0
- package/ts/src/builder/artifacts/storyFiles.ts +61 -0
- package/ts/src/builder/artifacts/storyIndex.ts +166 -0
- package/ts/src/builder/cadence.ts +59 -0
- package/ts/src/builder/card.ts +192 -0
- package/ts/src/builder/checkpointRefused.ts +66 -0
- package/ts/src/builder/closure.ts +964 -0
- package/ts/src/builder/commands.ts +2053 -0
- package/ts/src/builder/cursorPosition.ts +176 -0
- package/ts/src/builder/cursorTransitions.ts +77 -0
- package/ts/src/builder/deliveryCursor.ts +577 -0
- package/ts/src/builder/deliveryStoryClosure.ts +521 -0
- package/ts/src/builder/deliveryStoryPlan.ts +536 -0
- package/ts/src/builder/deliveryStoryReady.ts +160 -0
- package/ts/src/builder/deliveryStoryRoadmapClosure.ts +151 -0
- package/ts/src/builder/expand.ts +336 -0
- package/ts/src/builder/flowUnit.ts +339 -0
- package/ts/src/builder/homeSurface.ts +122 -0
- package/ts/src/builder/implementationGuard.ts +125 -0
- package/ts/src/builder/index.ts +11 -0
- package/ts/src/builder/lifecycleRefusal.ts +92 -0
- package/ts/src/builder/lifecycleRibbon.ts +159 -0
- package/ts/src/builder/load.ts +313 -0
- package/ts/src/builder/methodAdoption.ts +113 -0
- package/ts/src/builder/methodDefinition.ts +429 -0
- package/ts/src/builder/methodInspection.ts +371 -0
- package/ts/src/builder/plan.ts +408 -0
- package/ts/src/builder/planPreauthorization.ts +265 -0
- package/ts/src/builder/prepare.ts +225 -0
- package/ts/src/builder/projectPaths.ts +64 -0
- package/ts/src/builder/pull.ts +356 -0
- package/ts/src/builder/pullCandidates.ts +463 -0
- package/ts/src/builder/pullCandidatesRender.ts +238 -0
- package/ts/src/builder/refinementField.ts +93 -0
- package/ts/src/builder/releaseIntent.ts +173 -0
- package/ts/src/builder/resumeState.ts +84 -0
- package/ts/src/builder/resumeSurface.ts +149 -0
- package/ts/src/builder/roadmapGrammar.ts +300 -0
- package/ts/src/builder/roadmapScan.ts +97 -0
- package/ts/src/builder/roadmapScope.ts +210 -0
- package/ts/src/builder/scopePhrases.ts +146 -0
- package/ts/src/builder/storyPaths.ts +196 -0
- package/ts/src/builder/storyPlanPreauthorization.ts +287 -0
- package/ts/src/builder/surfaceProtocol.ts +42 -0
- package/ts/src/builder/templateGeneration.ts +148 -0
- package/ts/src/builder/transition.ts +178 -0
- package/ts/src/consult/args.ts +99 -0
- package/ts/src/consult/core.ts +122 -0
- package/ts/src/consult/modelCatalog.ts +55 -0
- package/ts/src/consult/render.ts +47 -0
- package/ts/src/conversation/append.ts +414 -0
- package/ts/src/conversation/backfill.ts +387 -0
- package/ts/src/conversation/closeTail.ts +467 -0
- package/ts/src/conversation/extraction.ts +333 -0
- package/ts/src/conversation/extractionDriver.ts +130 -0
- package/ts/src/conversation/extractionRun.ts +70 -0
- package/ts/src/conversation/hookPayload.ts +51 -0
- package/ts/src/conversation/journeyInference.ts +165 -0
- package/ts/src/conversation/journeyRepair.ts +114 -0
- package/ts/src/conversation/lifecycleFaces.ts +197 -0
- package/ts/src/conversation/lifecycleWrites.ts +313 -0
- package/ts/src/conversation/listing.ts +77 -0
- package/ts/src/conversation/logger.ts +851 -0
- package/ts/src/conversation/loggerCli.ts +321 -0
- package/ts/src/conversation/loggerRuntime.ts +259 -0
- package/ts/src/conversation/metadataLifecycle.ts +443 -0
- package/ts/src/conversation/recall.ts +66 -0
- package/ts/src/conversation/sessionComposites.ts +258 -0
- package/ts/src/conversation/sessionImport.ts +90 -0
- package/ts/src/conversation/transcriptBackfill.ts +141 -0
- package/ts/src/cultivation/applyActions.ts +241 -0
- package/ts/src/cultivation/cluster.ts +86 -0
- package/ts/src/cultivation/consolidationStore.ts +281 -0
- package/ts/src/cultivation/promptContext.ts +62 -0
- package/ts/src/cultivation/propose.ts +305 -0
- package/ts/src/cultivation/scan.ts +155 -0
- package/ts/src/db/backupGate.ts +47 -0
- package/ts/src/db/bootstrap.ts +108 -0
- package/ts/src/db/bootstrapLock.ts +223 -0
- package/ts/src/db/copyGuard.ts +60 -0
- package/ts/src/db/database.ts +397 -0
- package/ts/src/db/decode.ts +66 -0
- package/ts/src/db/ftsIntegrity.ts +41 -0
- package/ts/src/db/migrateOnOpen.ts +214 -0
- package/ts/src/db/migrations.ts +576 -0
- package/ts/src/db/readyOnOpen.ts +35 -0
- package/ts/src/db/rowDecode.ts +42 -0
- package/ts/src/db/schema.ts +432 -0
- package/ts/src/db/schemaInventory.ts +225 -0
- package/ts/src/db/schemaInventorySnapshot.ts +2107 -0
- package/ts/src/db/schemaState.ts +91 -0
- package/ts/src/db/schemaTsDivergence.ts +114 -0
- package/ts/src/descriptor/descriptorRead.ts +49 -0
- package/ts/src/docs/docsLint.ts +265 -0
- package/ts/src/explorer/handoff.ts +522 -0
- package/ts/src/explorer/render.ts +335 -0
- package/ts/src/explorer/story.ts +769 -0
- package/ts/src/explorer/transition.ts +72 -0
- package/ts/src/extensions/bindings.ts +170 -0
- package/ts/src/extensions/catalog.ts +249 -0
- package/ts/src/extensions/catalogCommands.ts +513 -0
- package/ts/src/extensions/catalogWrites.ts +652 -0
- package/ts/src/extensions/contextRuntime.ts +325 -0
- package/ts/src/extensions/dispatch.ts +374 -0
- package/ts/src/extensions/errors.ts +24 -0
- package/ts/src/extensions/manifest.ts +356 -0
- package/ts/src/extensions/migrations.ts +350 -0
- package/ts/src/extraction/conversation.ts +304 -0
- package/ts/src/extraction/conversationMetadata.ts +206 -0
- package/ts/src/extraction/fencing.ts +106 -0
- package/ts/src/extraction/json.ts +17 -0
- package/ts/src/extraction/prompts.ts +470 -0
- package/ts/src/frontDoor/args.ts +30 -0
- package/ts/src/frontDoor/argvShape.ts +106 -0
- package/ts/src/frontDoor/buildLoadRuntime.ts +139 -0
- package/ts/src/frontDoor/buildRoute.ts +398 -0
- package/ts/src/frontDoor/cli.ts +1872 -0
- package/ts/src/frontDoor/cloneRoleGuard.ts +98 -0
- package/ts/src/frontDoor/consultRoute.ts +50 -0
- package/ts/src/frontDoor/contentTailRoute.ts +239 -0
- package/ts/src/frontDoor/conversationLoggerRoute.ts +83 -0
- package/ts/src/frontDoor/cultivationRoute.ts +210 -0
- package/ts/src/frontDoor/dbPath.ts +131 -0
- package/ts/src/frontDoor/dbSafetyToolsRoute.ts +240 -0
- package/ts/src/frontDoor/exploreRoute.ts +572 -0
- package/ts/src/frontDoor/extensionCatalogRoute.ts +252 -0
- package/ts/src/frontDoor/frontDoorLog.ts +57 -0
- package/ts/src/frontDoor/identityWrite.ts +36 -0
- package/ts/src/frontDoor/journeyWriteRoute.ts +26 -0
- package/ts/src/frontDoor/lifecycleWriteRoute.ts +110 -0
- package/ts/src/frontDoor/liveBackup.ts +100 -0
- package/ts/src/frontDoor/mirrorModeRoute.ts +163 -0
- package/ts/src/frontDoor/namedRefusal.ts +35 -0
- package/ts/src/frontDoor/nodeSupport.ts +24 -0
- package/ts/src/frontDoor/render/consolidate.ts +199 -0
- package/ts/src/frontDoor/render/conversations.ts +33 -0
- package/ts/src/frontDoor/render/cultivationShared.ts +21 -0
- package/ts/src/frontDoor/render/descriptor.ts +20 -0
- package/ts/src/frontDoor/render/detectPersona.ts +50 -0
- package/ts/src/frontDoor/render/icons.ts +13 -0
- package/ts/src/frontDoor/render/identity.ts +59 -0
- package/ts/src/frontDoor/render/identityRows.ts +17 -0
- package/ts/src/frontDoor/render/inspectPersona.ts +53 -0
- package/ts/src/frontDoor/render/journeyStatus.ts +78 -0
- package/ts/src/frontDoor/render/journeys.ts +79 -0
- package/ts/src/frontDoor/render/list.ts +41 -0
- package/ts/src/frontDoor/render/memories.ts +67 -0
- package/ts/src/frontDoor/render/recall.ts +49 -0
- package/ts/src/frontDoor/render/shadow.ts +155 -0
- package/ts/src/frontDoor/render/tasks.ts +99 -0
- package/ts/src/frontDoor/render/tasksImportSync.ts +63 -0
- package/ts/src/frontDoor/render/week.ts +113 -0
- package/ts/src/frontDoor/routing.ts +1001 -0
- package/ts/src/frontDoor/runtimeRoute.ts +623 -0
- package/ts/src/frontDoor/searchRoute.ts +166 -0
- package/ts/src/frontDoor/seedPaths.ts +98 -0
- package/ts/src/frontDoor/sessionStartWelcomeRoute.ts +166 -0
- package/ts/src/frontDoor/soulRoute.ts +433 -0
- package/ts/src/frontDoor/stdinSource.ts +37 -0
- package/ts/src/frontDoor/tasksImportSyncRoute.ts +110 -0
- package/ts/src/frontDoor/tasksWriteRoute.ts +101 -0
- package/ts/src/frontDoor/usage.ts +176 -0
- package/ts/src/guards/claudePlugin.ts +298 -0
- package/ts/src/guards/packContents.ts +306 -0
- package/ts/src/guards/piSourcedSkills.ts +70 -0
- package/ts/src/guards/retiredRouteShadows.ts +57 -0
- package/ts/src/guards/retiredSurfaces.ts +564 -0
- package/ts/src/hooks/claude.ts +214 -0
- package/ts/src/hooks/codex.ts +45 -0
- package/ts/src/hooks/gemini.ts +126 -0
- package/ts/src/hooks/main.ts +109 -0
- package/ts/src/hooks/mirrorState.ts +71 -0
- package/ts/src/hooks/payload.ts +73 -0
- package/ts/src/hooks/runtime.ts +179 -0
- package/ts/src/hooks/runtimeDetection.ts +34 -0
- package/ts/src/identity/applyConsolidationIdentityUpdate.ts +67 -0
- package/ts/src/identity/descriptorGenerate.ts +162 -0
- package/ts/src/identity/identityEdit.ts +143 -0
- package/ts/src/identity/identityKey.ts +77 -0
- package/ts/src/identity/identityRead.ts +69 -0
- package/ts/src/identity/identityStore.ts +80 -0
- package/ts/src/identity/journeyListing.ts +53 -0
- package/ts/src/identity/personaInspect.ts +44 -0
- package/ts/src/identity/personaListing.ts +37 -0
- package/ts/src/identity/setIdentity.ts +64 -0
- package/ts/src/index.ts +231 -0
- package/ts/src/init/init.ts +111 -0
- package/ts/src/journey/journeyOptions.ts +131 -0
- package/ts/src/journey/journeyRemoval.ts +158 -0
- package/ts/src/journey/journeyStatus.ts +149 -0
- package/ts/src/journey/journeySyncFile.ts +87 -0
- package/ts/src/journey/journeyWrite.ts +203 -0
- package/ts/src/journey/parentJourney.ts +41 -0
- package/ts/src/journey/validateParentJourney.ts +55 -0
- package/ts/src/mcp/boundary.ts +151 -0
- package/ts/src/mcp/guards.ts +244 -0
- package/ts/src/mcp/main.ts +121 -0
- package/ts/src/mcp/payload.ts +141 -0
- package/ts/src/mcp/protocol.ts +141 -0
- package/ts/src/mcp/readModels.ts +241 -0
- package/ts/src/mcp/registry.ts +201 -0
- package/ts/src/mcp/serve.ts +120 -0
- package/ts/src/mcp/tools/deterministic.ts +137 -0
- package/ts/src/mcp/tools/providerCrossing.ts +187 -0
- package/ts/src/mcp/wire.ts +8 -0
- package/ts/src/memory/journal.ts +180 -0
- package/ts/src/memory/listing.ts +107 -0
- package/ts/src/memory/memoryWrite.ts +88 -0
- package/ts/src/memory/reinforcement.ts +35 -0
- package/ts/src/mirror/context.ts +161 -0
- package/ts/src/mirror/defaultResolution.ts +199 -0
- package/ts/src/mirror/mirrorForm.ts +43 -0
- package/ts/src/mirror/orchestration.ts +364 -0
- package/ts/src/mirror/reception.ts +194 -0
- package/ts/src/mirror/render.ts +75 -0
- package/ts/src/mirror/runtimeSession.ts +205 -0
- package/ts/src/mode/operatingMode.ts +148 -0
- package/ts/src/observability/callOutcome.ts +115 -0
- package/ts/src/observability/inspectLedger.ts +329 -0
- package/ts/src/observability/ledgerHooks.ts +126 -0
- package/ts/src/observability/llmCalls.ts +154 -0
- package/ts/src/persona/detectPersona.ts +81 -0
- package/ts/src/planning/promptAssembly.ts +111 -0
- package/ts/src/planning/weekPending.ts +81 -0
- package/ts/src/planning/weekPlan.ts +183 -0
- package/ts/src/planning/weekSave.ts +117 -0
- package/ts/src/providers/config.ts +189 -0
- package/ts/src/providers/cost.ts +64 -0
- package/ts/src/providers/credits.ts +204 -0
- package/ts/src/providers/embedding.ts +321 -0
- package/ts/src/providers/familyProviders.ts +147 -0
- package/ts/src/providers/llm.ts +334 -0
- package/ts/src/providers/openrouter.ts +244 -0
- package/ts/src/providers/redaction.ts +110 -0
- package/ts/src/providers/replay.ts +15 -0
- package/ts/src/providers/transport.ts +410 -0
- package/ts/src/repair/encodingRepair.ts +253 -0
- package/ts/src/runtime/backup.ts +151 -0
- package/ts/src/runtime/channel.ts +146 -0
- package/ts/src/runtime/config.ts +168 -0
- package/ts/src/runtime/diagnose.ts +686 -0
- package/ts/src/runtime/git.ts +510 -0
- package/ts/src/runtime/installKind.ts +220 -0
- package/ts/src/runtime/migrate.ts +154 -0
- package/ts/src/runtime/packageIdentity.ts +102 -0
- package/ts/src/runtime/releaseNotes.ts +244 -0
- package/ts/src/runtime/status.ts +557 -0
- package/ts/src/runtime/strategies/clone.ts +200 -0
- package/ts/src/runtime/strategies/package.ts +328 -0
- package/ts/src/runtime/treeRoot.ts +30 -0
- package/ts/src/runtime/update.ts +445 -0
- package/ts/src/runtime/updateGate.ts +40 -0
- package/ts/src/runtime/updatePipeline.ts +159 -0
- package/ts/src/runtime/version.ts +36 -0
- package/ts/src/runtime/wiring.ts +130 -0
- package/ts/src/search/memorySearch.ts +338 -0
- package/ts/src/search/ranker.ts +154 -0
- package/ts/src/seed/seed.ts +256 -0
- package/ts/src/seed/seedYaml.ts +133 -0
- package/ts/src/soul/apply.ts +218 -0
- package/ts/src/soul/harvest.ts +267 -0
- package/ts/src/soul/prompts/soul_beauty_voice.md +103 -0
- package/ts/src/soul/prompts/soul_self_voice.md +219 -0
- package/ts/src/soul/prompts/soul_wisdom_voice.md +121 -0
- package/ts/src/soul/prompts.ts +85 -0
- package/ts/src/soul/render.ts +358 -0
- package/ts/src/soul/state.ts +212 -0
- package/ts/src/soul/transition.ts +37 -0
- package/ts/src/tasks/journeyPathParse.ts +113 -0
- package/ts/src/tasks/taskImportSync.ts +142 -0
- package/ts/src/tasks/taskStore.ts +312 -0
- package/ts/src/tasks/weekView.ts +120 -0
- package/ts/src/util/clipCodePoints.ts +26 -0
- package/ts/src/util/paths.ts +63 -0
- package/ts/src/util/program.ts +9 -0
- package/ts/src/util/pyGenerators.ts +144 -0
- package/ts/src/util/pythonText.ts +317 -0
- package/ts/src/util/semver.ts +45 -0
- package/ts/src/util/shellWord.ts +25 -0
- package/ts/src/util/slug.ts +64 -0
- package/ts/src/welcome/card.ts +339 -0
- package/ts/src/welcome/statusLine.ts +101 -0
- 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
|
+
}
|