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,1872 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
//
|
|
4
|
+
// Front-door entry: route a command, answer it, and log the outcome. Argument
|
|
5
|
+
// parsing lives in `args.ts`, DB-path resolution in `dbPath.ts`, and the read
|
|
6
|
+
// renderers in `render/`. The module is importable (the entry guard at the
|
|
7
|
+
// bottom runs `main` only when invoked directly), so its pieces can be tested
|
|
8
|
+
// in-process.
|
|
9
|
+
//
|
|
10
|
+
// Every route answers from TypeScript. Until CV22.DS10.TS5 an unported or
|
|
11
|
+
// reverted command fell back to the Python engine by spawning the
|
|
12
|
+
// interpreter; the fallback, the revert gates that chose it, and the engine
|
|
13
|
+
// itself are gone. A name nothing answers gets the front door's own usage
|
|
14
|
+
// answer.
|
|
15
|
+
//
|
|
16
|
+
// Configuration and the node:sqlite ExperimentalWarning are both handled
|
|
17
|
+
// in-process, inside the entry guard (CV22.DS10.US3, D3): the program reads
|
|
18
|
+
// its own `.env` and `~/.config/mirror/env` and silences the warning itself,
|
|
19
|
+
// so neither a `bin` shim nor a skill line needs `--env-file` or
|
|
20
|
+
// `NODE_OPTIONS=--no-warnings`. Importing this module does neither.
|
|
21
|
+
|
|
22
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
23
|
+
import { basename, dirname } from "node:path";
|
|
24
|
+
import { pathToFileURL } from "node:url";
|
|
25
|
+
import {
|
|
26
|
+
AppendRejectedError,
|
|
27
|
+
type AppendRejectionReason,
|
|
28
|
+
appendConversationMessages,
|
|
29
|
+
type ConversationAppendRequest,
|
|
30
|
+
canonicalJson,
|
|
31
|
+
MAX_PAYLOAD_BYTES,
|
|
32
|
+
parseAppendRequest,
|
|
33
|
+
} from "#conversation/append.ts";
|
|
34
|
+
import { DEFAULT_CLUSTER_THRESHOLD } from "#cultivation/cluster.ts";
|
|
35
|
+
import { listConsolidations } from "#cultivation/consolidationStore.ts";
|
|
36
|
+
import {
|
|
37
|
+
consolidateScan,
|
|
38
|
+
DEFAULT_CONSOLIDATE_SCAN_LIMIT,
|
|
39
|
+
DEFAULT_SHADOW_SCAN_LIMIT,
|
|
40
|
+
shadowScan,
|
|
41
|
+
} from "#cultivation/scan.ts";
|
|
42
|
+
import { type Database, openDatabaseReadOnly, type WritableDatabase } from "#db/database.ts";
|
|
43
|
+
import { ensureDatabaseReady } from "#db/readyOnOpen.ts";
|
|
44
|
+
import { assertSchemaState, SchemaStateError } from "#db/schemaState.ts";
|
|
45
|
+
import { allDescriptors, descriptorsByLayer } from "#descriptor/descriptorRead.ts";
|
|
46
|
+
import { newIdentityKeyProblem } from "#identity/identityKey.ts";
|
|
47
|
+
import { identityRowExists, listIdentityByLayer } from "#identity/identityRead.ts";
|
|
48
|
+
import { listJourneysForListCommand } from "#identity/journeyListing.ts";
|
|
49
|
+
import { listPersonas } from "#identity/personaListing.ts";
|
|
50
|
+
import { setIdentity } from "#identity/setIdentity.ts";
|
|
51
|
+
import { IdentityRootExistsError, initUserHome, TemplatesNotFoundError } from "#init/init.ts";
|
|
52
|
+
import { JOURNEY_PATH_LAYER } from "#journey/journeyStatus.ts";
|
|
53
|
+
import { JourneyNotFoundError } from "#journey/journeyWrite.ts";
|
|
54
|
+
import {
|
|
55
|
+
CallOutcomeTally,
|
|
56
|
+
formatCallOutcome,
|
|
57
|
+
type ProviderCallReport,
|
|
58
|
+
} from "#observability/callOutcome.ts";
|
|
59
|
+
import { chatLedgerHook } from "#observability/ledgerHooks.ts";
|
|
60
|
+
import { runWeekSave } from "#planning/weekSave.ts";
|
|
61
|
+
import type { EmbeddingProvider } from "#providers/embedding.ts";
|
|
62
|
+
import { resolveFamilyProviders } from "#providers/familyProviders.ts";
|
|
63
|
+
import type { LlmProvider } from "#providers/llm.ts";
|
|
64
|
+
import {
|
|
65
|
+
CULTIVATION_APPLY_TRANSPORT,
|
|
66
|
+
CULTIVATION_SCAN_TRANSPORT,
|
|
67
|
+
DESCRIPTOR_TRANSPORT,
|
|
68
|
+
JOURNAL_TRANSPORT,
|
|
69
|
+
WEEK_PLAN_TRANSPORT,
|
|
70
|
+
} from "#providers/transport.ts";
|
|
71
|
+
import {
|
|
72
|
+
configFilePath,
|
|
73
|
+
loadConfiguration,
|
|
74
|
+
silenceExperimentalWarnings,
|
|
75
|
+
writeConfigValue,
|
|
76
|
+
} from "#runtime/config.ts";
|
|
77
|
+
import { runningTreeRoot } from "#runtime/treeRoot.ts";
|
|
78
|
+
import { detectRuntimes, runtimeWiringLines } from "#runtime/wiring.ts";
|
|
79
|
+
import { runSeed } from "#seed/seed.ts";
|
|
80
|
+
import { getTasksForWeek, listTasks } from "#tasks/taskStore.ts";
|
|
81
|
+
import { computeWeekRange } from "#tasks/weekView.ts";
|
|
82
|
+
import { isExecutable } from "#util/paths.ts";
|
|
83
|
+
import { PROGRAM } from "#util/program.ts";
|
|
84
|
+
import { newId, nowIso } from "#util/pyGenerators.ts";
|
|
85
|
+
import { hasOption, optionValue, stripOptionWithValue } from "./args.ts";
|
|
86
|
+
import { canonicalArgv } from "./argvShape.ts";
|
|
87
|
+
import { createBuildLoadRuntime } from "./buildLoadRuntime.ts";
|
|
88
|
+
import { runConsultRoute } from "./consultRoute.ts";
|
|
89
|
+
import {
|
|
90
|
+
runDescriptorGenerateRoute,
|
|
91
|
+
runJournalRoute,
|
|
92
|
+
runLifecycleFaceRead,
|
|
93
|
+
runWeekPlanRoute,
|
|
94
|
+
} from "./contentTailRoute.ts";
|
|
95
|
+
import {
|
|
96
|
+
isConversationLoggerCommand,
|
|
97
|
+
runConversationLoggerRoute,
|
|
98
|
+
} from "./conversationLoggerRoute.ts";
|
|
99
|
+
import {
|
|
100
|
+
runConsolidateApply as runConsolidateApplyRoute,
|
|
101
|
+
runReject,
|
|
102
|
+
runShadowApply as runShadowApplyRoute,
|
|
103
|
+
} from "./cultivationRoute.ts";
|
|
104
|
+
import { MirrorHomeNotConfiguredError, resolveDbPath } from "./dbPath.ts";
|
|
105
|
+
import { runBackupRoute, runRepairEncodingRoute } from "./dbSafetyToolsRoute.ts";
|
|
106
|
+
import { defaultExploreRouteDeps, runExploreRoute } from "./exploreRoute.ts";
|
|
107
|
+
import { frontDoorLogPath, logFrontDoor } from "./frontDoorLog.ts";
|
|
108
|
+
import { applyIdentitySet } from "./identityWrite.ts";
|
|
109
|
+
import { applyJourneySetPath } from "./journeyWriteRoute.ts";
|
|
110
|
+
import { openLiveWriteDatabase } from "./liveBackup.ts";
|
|
111
|
+
import {
|
|
112
|
+
isMirrorWrite,
|
|
113
|
+
isModeWrite,
|
|
114
|
+
mirrorDeactivateMissingSession,
|
|
115
|
+
runMirrorRead,
|
|
116
|
+
runMirrorWriteRoute,
|
|
117
|
+
runModeRead,
|
|
118
|
+
runModeWriteRoute,
|
|
119
|
+
} from "./mirrorModeRoute.ts";
|
|
120
|
+
import { namedRefusal } from "./namedRefusal.ts";
|
|
121
|
+
import { nodeVersionError } from "./nodeSupport.ts";
|
|
122
|
+
import {
|
|
123
|
+
renderConsolidateApply,
|
|
124
|
+
renderConsolidateList,
|
|
125
|
+
renderConsolidateReject,
|
|
126
|
+
renderConsolidateScan,
|
|
127
|
+
} from "./render/consolidate.ts";
|
|
128
|
+
import { renderConversationsListing } from "./render/conversations.ts";
|
|
129
|
+
import { renderDescriptorList } from "./render/descriptor.ts";
|
|
130
|
+
import { renderDetectPersona } from "./render/detectPersona.ts";
|
|
131
|
+
import {
|
|
132
|
+
IdentityEntryNotFoundError,
|
|
133
|
+
identityListRows,
|
|
134
|
+
renderIdentityGet,
|
|
135
|
+
renderIdentityList,
|
|
136
|
+
} from "./render/identity.ts";
|
|
137
|
+
import { PersonaNotFoundError, renderInspectPersona } from "./render/inspectPersona.ts";
|
|
138
|
+
import { renderJourneyStatus, resolveJourneyStatusSlug } from "./render/journeyStatus.ts";
|
|
139
|
+
import { renderJourneys } from "./render/journeys.ts";
|
|
140
|
+
import { renderListJourneys, renderListPersonas } from "./render/list.ts";
|
|
141
|
+
import { renderMemories } from "./render/memories.ts";
|
|
142
|
+
import { ConversationNotFoundError, renderRecall } from "./render/recall.ts";
|
|
143
|
+
import {
|
|
144
|
+
renderShadowApply,
|
|
145
|
+
renderShadowList,
|
|
146
|
+
renderShadowReject,
|
|
147
|
+
renderShadowScan,
|
|
148
|
+
renderShadowShow,
|
|
149
|
+
} from "./render/shadow.ts";
|
|
150
|
+
import {
|
|
151
|
+
renderTasksAdd,
|
|
152
|
+
renderTasksDelete,
|
|
153
|
+
renderTasksList,
|
|
154
|
+
renderTasksStatusChange,
|
|
155
|
+
} from "./render/tasks.ts";
|
|
156
|
+
import {
|
|
157
|
+
renderTasksImport,
|
|
158
|
+
renderTasksSyncConfig,
|
|
159
|
+
renderTasksSyncNoJourneysConfigured,
|
|
160
|
+
renderTasksSyncOutcome,
|
|
161
|
+
} from "./render/tasksImportSync.ts";
|
|
162
|
+
import { renderWeekView } from "./render/week.ts";
|
|
163
|
+
import { retiredRefusal, routeMemoryCommand } from "./routing.ts";
|
|
164
|
+
import { runRuntimeReadRoute, runWelcomeRoute } from "./runtimeRoute.ts";
|
|
165
|
+
import { runMemorySearchRoute } from "./searchRoute.ts";
|
|
166
|
+
import { resolveSeedPaths } from "./seedPaths.ts";
|
|
167
|
+
import { isSessionStartWelcome, runSessionStartWelcomeRoute } from "./sessionStartWelcomeRoute.ts";
|
|
168
|
+
import { runSoulRoute } from "./soulRoute.ts";
|
|
169
|
+
import {
|
|
170
|
+
applyTasksImport,
|
|
171
|
+
applyTasksSyncConfig,
|
|
172
|
+
applyTasksSyncForJourney,
|
|
173
|
+
resolveSyncJourneys,
|
|
174
|
+
} from "./tasksImportSyncRoute.ts";
|
|
175
|
+
import {
|
|
176
|
+
applyTasksAdd,
|
|
177
|
+
applyTasksDelete,
|
|
178
|
+
applyTasksStatusChange,
|
|
179
|
+
type TaskStatusTarget,
|
|
180
|
+
} from "./tasksWriteRoute.ts";
|
|
181
|
+
import { answerUsage } from "./usage.ts";
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Resolve the database path for a CLI invocation, mapping a configuration
|
|
185
|
+
* failure (no home/env resolvable) to a printed error and null — callers
|
|
186
|
+
* translate null into exit code 2.
|
|
187
|
+
*/
|
|
188
|
+
function resolveDbPathForCli(args: readonly string[]): string | null {
|
|
189
|
+
try {
|
|
190
|
+
return resolveDbPath(args);
|
|
191
|
+
} catch (error) {
|
|
192
|
+
if (error instanceof MirrorHomeNotConfiguredError) {
|
|
193
|
+
console.error(`Mirror TS front door: ${error.message}`);
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
throw error;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Prepare a database for TS serving and record what happened in the front-door
|
|
202
|
+
* log: a redacted `migrate_on_open` event (migration ids + backup file name,
|
|
203
|
+
* never content). The activation itself lives in `db/readyOnOpen.ts`, shared
|
|
204
|
+
* with the MCP entry point, which writes its note to stderr instead.
|
|
205
|
+
*/
|
|
206
|
+
function ensureDatabaseReadyForCli(dbPath: string, command: string | null): void {
|
|
207
|
+
const migration = ensureDatabaseReady(dbPath);
|
|
208
|
+
if (migration.migrated) {
|
|
209
|
+
logFrontDoor(frontDoorLogPath(dbPath), {
|
|
210
|
+
command,
|
|
211
|
+
route: "ts",
|
|
212
|
+
exitCode: 0,
|
|
213
|
+
detail: `migrate_on_open applied=${migration.appliedIds.join(",")} backup=${
|
|
214
|
+
migration.backupPath ? basename(migration.backupPath) : "none"
|
|
215
|
+
}`,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** Serve a ported read command from the TS core, bootstrapping a missing DB via the TS core first. */
|
|
221
|
+
function runTs(argv: readonly string[]): number {
|
|
222
|
+
const command = argv[0];
|
|
223
|
+
const args = argv.slice(1);
|
|
224
|
+
const dbPath = resolveDbPathForCli(args);
|
|
225
|
+
if (dbPath === null) return 2;
|
|
226
|
+
// First-run contract (CV22.DS6.TS4): a missing database means an
|
|
227
|
+
// unbootstrapped install. TS now owns bootstrap — create the directory,
|
|
228
|
+
// schema, and migrations under the cross-process lock — then serve read-only
|
|
229
|
+
// from the fresh file. (This replaces the DS3 stopgap that delegated a
|
|
230
|
+
// missing DB to Python; see docs/project/decisions.md.)
|
|
231
|
+
ensureDatabaseReadyForCli(dbPath, command ?? null);
|
|
232
|
+
const db = openDatabaseReadOnly(dbPath);
|
|
233
|
+
try {
|
|
234
|
+
assertSchemaState(db);
|
|
235
|
+
if (command === "detect-persona") process.stdout.write(renderDetectPersona(db, args));
|
|
236
|
+
else if (command === "journeys") process.stdout.write(renderJourneys(db));
|
|
237
|
+
else if (command === "memories") process.stdout.write(renderMemories(db, args));
|
|
238
|
+
else if (command === "identity") return runIdentityRead(db, args);
|
|
239
|
+
else if (command === "descriptor") return runDescriptorRead(db, args);
|
|
240
|
+
else if (command === "list") return runListRead(db, args);
|
|
241
|
+
else if (command === "inspect") return runInspectRead(db, args);
|
|
242
|
+
else if (command === "recall") return runRecallRead(db, args);
|
|
243
|
+
else if (command === "conversations") {
|
|
244
|
+
// CV22.DS7.US11: the two ES-001 read faces share the read seam with the
|
|
245
|
+
// plain listing; routing.ts has already refused the write faces.
|
|
246
|
+
const lifecycleFace =
|
|
247
|
+
args.includes("--metadata-lifecycle-dry-run") ||
|
|
248
|
+
args.includes("--metadata-lifecycle-preview-at-message");
|
|
249
|
+
return lifecycleFace ? runLifecycleFaceRead(db, args) : runConversationsRead(db, args);
|
|
250
|
+
} else if (command === "journey") return runJourneyStatusRead(db, args);
|
|
251
|
+
else if (command === "tasks") return runTasksRead(db, args);
|
|
252
|
+
else if (command === "week") return runWeekRead(db, args);
|
|
253
|
+
else if (command === "consolidate") return runConsolidateRead(db, args);
|
|
254
|
+
else if (command === "shadow") return runShadowRead(db, args);
|
|
255
|
+
else if (command === "mirror") return runMirrorRead(db, args);
|
|
256
|
+
else if (command === "mode") return runModeRead(db, args);
|
|
257
|
+
else throw new Error(`Unsupported TS route: ${command}`);
|
|
258
|
+
return 0;
|
|
259
|
+
} catch (error) {
|
|
260
|
+
if (error instanceof SchemaStateError) {
|
|
261
|
+
console.error(`Mirror TS front door: ${error.message}`);
|
|
262
|
+
return 2;
|
|
263
|
+
}
|
|
264
|
+
throw error;
|
|
265
|
+
} finally {
|
|
266
|
+
db.close();
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Serve `identity list` / `identity get` (DS7.US1). `args` excludes the
|
|
272
|
+
* `identity` token itself, e.g. `["list", "--layer", "ego"]` or
|
|
273
|
+
* `["get", "ego", "behavior"]`. `identity set` and `identity edit` never reach
|
|
274
|
+
* here — routed elsewhere (write seam) or to Python (interactive) respectively.
|
|
275
|
+
*/
|
|
276
|
+
function runIdentityRead(db: Database, args: readonly string[]): number {
|
|
277
|
+
const sub = args[0];
|
|
278
|
+
const rest = stripOptionWithValue(
|
|
279
|
+
stripOptionWithValue(args.slice(1), "--mirror-home"),
|
|
280
|
+
"--db-path",
|
|
281
|
+
);
|
|
282
|
+
if (sub === "list") {
|
|
283
|
+
process.stdout.write(renderIdentityList(identityListRows(db, optionValue(args, "--layer"))));
|
|
284
|
+
return 0;
|
|
285
|
+
}
|
|
286
|
+
// sub === "get" (the only other route.ts-allowed subcommand).
|
|
287
|
+
const [layer, key] = rest;
|
|
288
|
+
if (!layer || !key) {
|
|
289
|
+
console.error("Usage: identity get <layer> <key>");
|
|
290
|
+
return 2;
|
|
291
|
+
}
|
|
292
|
+
try {
|
|
293
|
+
process.stdout.write(renderIdentityGet(db, layer, key));
|
|
294
|
+
return 0;
|
|
295
|
+
} catch (error) {
|
|
296
|
+
if (error instanceof IdentityEntryNotFoundError) {
|
|
297
|
+
console.error(error.message);
|
|
298
|
+
return 1;
|
|
299
|
+
}
|
|
300
|
+
throw error;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/** Serve `descriptor list` (DS7.US1). `descriptor generate` never reaches here. */
|
|
305
|
+
function runDescriptorRead(db: Database, args: readonly string[]): number {
|
|
306
|
+
const layer = optionValue(args, "--layer");
|
|
307
|
+
const rows = layer ? descriptorsByLayer(db, layer) : allDescriptors(db);
|
|
308
|
+
process.stdout.write(renderDescriptorList(rows));
|
|
309
|
+
return 0;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/** Serve `list personas` / `list journeys` (DS7.US1). `extensions`/`all` never reach here. */
|
|
313
|
+
function runListRead(db: Database, args: readonly string[]): number {
|
|
314
|
+
if (args[0] === "personas") {
|
|
315
|
+
process.stdout.write(renderListPersonas(listPersonas(db), hasOption(args, "--verbose")));
|
|
316
|
+
return 0;
|
|
317
|
+
}
|
|
318
|
+
process.stdout.write(renderListJourneys(listJourneysForListCommand(db)));
|
|
319
|
+
return 0;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/** Serve `inspect persona <id>` (DS7.US1). Other inspect targets never reach here. */
|
|
323
|
+
function runInspectRead(db: Database, args: readonly string[]): number {
|
|
324
|
+
const rest = stripOptionWithValue(
|
|
325
|
+
stripOptionWithValue(args.slice(1), "--mirror-home"),
|
|
326
|
+
"--extensions-root",
|
|
327
|
+
);
|
|
328
|
+
const personaId = rest[0];
|
|
329
|
+
if (!personaId) {
|
|
330
|
+
console.error("Usage: inspect persona <id>");
|
|
331
|
+
return 2;
|
|
332
|
+
}
|
|
333
|
+
try {
|
|
334
|
+
process.stdout.write(renderInspectPersona(db, personaId));
|
|
335
|
+
return 0;
|
|
336
|
+
} catch (error) {
|
|
337
|
+
if (error instanceof PersonaNotFoundError) {
|
|
338
|
+
// Matches Python: this goes to STDOUT, not stderr.
|
|
339
|
+
process.stdout.write(`${error.message}\n`);
|
|
340
|
+
return 1;
|
|
341
|
+
}
|
|
342
|
+
throw error;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/** Serve `recall <conv_id> [--limit N]` (DS7.US1). */
|
|
347
|
+
function runRecallRead(db: Database, args: readonly string[]): number {
|
|
348
|
+
const limitRaw = optionValue(args, "--limit");
|
|
349
|
+
const limit = limitRaw !== null ? Number(limitRaw) : 50;
|
|
350
|
+
const positionals = stripOptionWithValue(
|
|
351
|
+
stripOptionWithValue(stripOptionWithValue(args, "--limit"), "--mirror-home"),
|
|
352
|
+
"--db-path",
|
|
353
|
+
);
|
|
354
|
+
const convId = positionals[0];
|
|
355
|
+
if (!convId) {
|
|
356
|
+
console.error("Usage: recall <conv_id> [--limit N]");
|
|
357
|
+
return 2;
|
|
358
|
+
}
|
|
359
|
+
try {
|
|
360
|
+
process.stdout.write(renderRecall(db, convId, limit));
|
|
361
|
+
return 0;
|
|
362
|
+
} catch (error) {
|
|
363
|
+
if (error instanceof ConversationNotFoundError) {
|
|
364
|
+
console.error(error.message);
|
|
365
|
+
return 1;
|
|
366
|
+
}
|
|
367
|
+
throw error;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/** Serve `conversations` (plain listing only; DS7.US1). */
|
|
372
|
+
function runConversationsRead(db: Database, args: readonly string[]): number {
|
|
373
|
+
const limitRaw = optionValue(args, "--limit");
|
|
374
|
+
process.stdout.write(
|
|
375
|
+
renderConversationsListing(db, {
|
|
376
|
+
limit: limitRaw !== null ? Number(limitRaw) : 20,
|
|
377
|
+
journey: optionValue(args, "--journey"),
|
|
378
|
+
persona: optionValue(args, "--persona"),
|
|
379
|
+
}),
|
|
380
|
+
);
|
|
381
|
+
return 0;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Serve `journey` status reads (DS7.US1): `journey`, `journey <slug>`,
|
|
386
|
+
* `journey status`, `journey status <slug>`. `journey set-path`/`update` never
|
|
387
|
+
* reach here (their own write routes).
|
|
388
|
+
*/
|
|
389
|
+
function runJourneyStatusRead(db: Database, args: readonly string[]): number {
|
|
390
|
+
const remaining = stripOptionWithValue(stripOptionWithValue(args, "--mirror-home"), "--db-path");
|
|
391
|
+
process.stdout.write(renderJourneyStatus(db, resolveJourneyStatusSlug(remaining)));
|
|
392
|
+
return 0;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Serve `tasks list` (and the bare `tasks`/`tasks --journey ...` default,
|
|
397
|
+
* DS7.US2 slice 3a). `args` excludes the `tasks` token itself. Flags are
|
|
398
|
+
* scanned across the whole slice (the existing recall/conversations
|
|
399
|
+
* convention) rather than requiring them after a `list` token -- Python's own
|
|
400
|
+
* argparse subparser quirk (a flag given only BEFORE the subcommand can be
|
|
401
|
+
* overwritten back to the subparser's default) is a narrow, unreproduced edge
|
|
402
|
+
* case; the common "flag after/around the subcommand" usage matches exactly.
|
|
403
|
+
*/
|
|
404
|
+
function runTasksRead(db: Database, args: readonly string[]): number {
|
|
405
|
+
const journey = optionValue(args, "--journey");
|
|
406
|
+
const status = optionValue(args, "--status");
|
|
407
|
+
const all = hasOption(args, "--all");
|
|
408
|
+
const filters = all ? { journey } : status ? { journey, status } : { journey, openOnly: true };
|
|
409
|
+
const tasks = listTasks(db, filters);
|
|
410
|
+
process.stdout.write(renderTasksList(tasks, { all, status }));
|
|
411
|
+
return 0;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Serve `week view` (and the bare `week` default, DS7.US2 slice 3b). `plan`/
|
|
416
|
+
* `save` never reach here (their own routes). `now` is the real current time
|
|
417
|
+
* in production; tests inject a frozen instant by calling `renderWeekView`
|
|
418
|
+
* directly rather than through this CLI entry point.
|
|
419
|
+
*/
|
|
420
|
+
/**
|
|
421
|
+
* `week save` (CV22.DS7.US11 plateau 1) is the only `week` leaf that writes.
|
|
422
|
+
* `view` stays on the read seam; `plan` is Python's until it lands behind the
|
|
423
|
+
* replay transport.
|
|
424
|
+
*/
|
|
425
|
+
function isWeekSaveWrite(argv: readonly string[]): boolean {
|
|
426
|
+
return argv[0] === "week" && argv[1] === "save";
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function runWeekSaveWrite(argv: readonly string[]): number {
|
|
430
|
+
return withLiveWriteDb(argv, (db) => {
|
|
431
|
+
runWeekSave(db, {});
|
|
432
|
+
return 0;
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* The content-tail routes (CV22.DS7.US11, live under CV22.DS8.US3).
|
|
438
|
+
*
|
|
439
|
+
* Each builds its providers from the same spec `routing.ts` decided with, so
|
|
440
|
+
* the route cannot disagree with the router about which transport is in play.
|
|
441
|
+
* Half a replay fixture throws `ReplayFixtureIncompleteError` from the factory,
|
|
442
|
+
* which the front door answers as a refusal; a missing provider here is defense
|
|
443
|
+
* in depth.
|
|
444
|
+
*/
|
|
445
|
+
async function runJournalContentRoute(argv: readonly string[]): Promise<number> {
|
|
446
|
+
const family = await resolveFamilyProviders(process.env, JOURNAL_TRANSPORT);
|
|
447
|
+
if (!family?.llm || !family.embedding) {
|
|
448
|
+
throw new Error("journal requires an LLM and an embedding provider");
|
|
449
|
+
}
|
|
450
|
+
return withLiveWriteDbAsync(argv, (db) =>
|
|
451
|
+
runJournalRoute(db, argv.slice(1), {
|
|
452
|
+
llm: family.llm as LlmProvider,
|
|
453
|
+
embedding: family.embedding as EmbeddingProvider,
|
|
454
|
+
}),
|
|
455
|
+
);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
async function runWeekPlanContentRoute(argv: readonly string[]): Promise<number> {
|
|
459
|
+
const family = await resolveFamilyProviders(process.env, WEEK_PLAN_TRANSPORT);
|
|
460
|
+
if (!family?.llm) throw new Error("week plan requires an LLM provider");
|
|
461
|
+
const llm = family.llm;
|
|
462
|
+
return withLiveWriteDbAsync(argv, (db) => runWeekPlanRoute(db, argv.slice(2), llm));
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
async function runDescriptorContentRoute(argv: readonly string[]): Promise<number> {
|
|
466
|
+
const family = await resolveFamilyProviders(process.env, DESCRIPTOR_TRANSPORT);
|
|
467
|
+
if (!family?.llm) throw new Error("descriptor generate requires an LLM provider");
|
|
468
|
+
const llm = family.llm;
|
|
469
|
+
const reporter = outcomeReporter(argv, "descriptor");
|
|
470
|
+
return withLiveWriteDbAsync(argv, async (db) => {
|
|
471
|
+
const exitCode = await runDescriptorGenerateRoute(db, argv.slice(2), llm, reporter.onOutcome);
|
|
472
|
+
reporter.logSummary();
|
|
473
|
+
return exitCode;
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function runWeekRead(db: Database, _args: readonly string[]): number {
|
|
478
|
+
const now = new Date();
|
|
479
|
+
const range = computeWeekRange(now);
|
|
480
|
+
const tasks = getTasksForWeek(db, range.start, range.end);
|
|
481
|
+
process.stdout.write(renderWeekView(tasks, now));
|
|
482
|
+
return 0;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/** Serve `consolidate list` (CV22.DS7.US3). `args` excludes the `consolidate` token itself. */
|
|
486
|
+
function runConsolidateRead(db: Database, args: readonly string[]): number {
|
|
487
|
+
const status = optionValue(args, "--status");
|
|
488
|
+
const limitRaw = optionValue(args, "--limit");
|
|
489
|
+
const limit = limitRaw !== null ? Number(limitRaw) : 20;
|
|
490
|
+
process.stdout.write(renderConsolidateList(listConsolidations(db, { status, limit }), status));
|
|
491
|
+
return 0;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/** Serve `shadow list` / `shadow show` (CV22.DS7.US3). `args` excludes the `shadow` token itself. */
|
|
495
|
+
function runShadowRead(db: Database, args: readonly string[]): number {
|
|
496
|
+
if (args[0] === "show") {
|
|
497
|
+
process.stdout.write(renderShadowShow(listIdentityByLayer(db, "shadow")));
|
|
498
|
+
return 0;
|
|
499
|
+
}
|
|
500
|
+
// args[0] === "list" (the only other route.ts-allowed read subcommand).
|
|
501
|
+
const status = optionValue(args, "--status");
|
|
502
|
+
const limitRaw = optionValue(args, "--limit");
|
|
503
|
+
const limit = limitRaw !== null ? Number(limitRaw) : 20;
|
|
504
|
+
// Port of Python's own over-fetch-then-filter: list_consolidations(limit*5),
|
|
505
|
+
// then keep only shadow_observation rows, THEN cap at limit.
|
|
506
|
+
const pool = listConsolidations(db, { status, limit: limit * 5 });
|
|
507
|
+
const items = pool.filter((c) => c.action === "shadow_observation").slice(0, limit);
|
|
508
|
+
process.stdout.write(renderShadowList(items, status));
|
|
509
|
+
return 0;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function readStdinContent(): string {
|
|
513
|
+
try {
|
|
514
|
+
return readFileSync(0, "utf8");
|
|
515
|
+
} catch {
|
|
516
|
+
return "";
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/** `conversations append` (CV22.DS7.US10 slice B′). */
|
|
521
|
+
function isConversationsAppend(argv: readonly string[]): boolean {
|
|
522
|
+
return argv[0] === "conversations" && argv[1] === "append";
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* Serve `conversations append`: the published external-shell write boundary.
|
|
527
|
+
*
|
|
528
|
+
* Mirrors Python's `_append_main` exactly, including its two exit codes — 0 with
|
|
529
|
+
* a success receipt, 2 with a bounded rejection receipt — because the contract
|
|
530
|
+
* is consumed by third-party shells that branch on them. Receipts are emitted
|
|
531
|
+
* as canonical JSON (sorted keys, tight separators) so the bytes match the
|
|
532
|
+
* oracle's.
|
|
533
|
+
*
|
|
534
|
+
* The stdin read is bounded before parsing: Python reads one byte past the
|
|
535
|
+
* limit and rejects `limit_exceeded` without ever decoding an oversized
|
|
536
|
+
* payload, so an attacker cannot force the parser to walk 100 MB of JSON.
|
|
537
|
+
*/
|
|
538
|
+
function runConversationsAppend(argv: readonly string[]): number {
|
|
539
|
+
const emitRejection = (reason: AppendRejectionReason): number => {
|
|
540
|
+
process.stdout.write(`${canonicalJson(new AppendRejectedError(reason).receipt())}\n`);
|
|
541
|
+
return 2;
|
|
542
|
+
};
|
|
543
|
+
|
|
544
|
+
const raw = readStdinBytes(MAX_PAYLOAD_BYTES + 1);
|
|
545
|
+
if (raw.byteLength > MAX_PAYLOAD_BYTES) return emitRejection("limit_exceeded");
|
|
546
|
+
|
|
547
|
+
let request: ConversationAppendRequest;
|
|
548
|
+
try {
|
|
549
|
+
request = parseAppendRequest(JSON.parse(raw.toString("utf8")));
|
|
550
|
+
} catch (error) {
|
|
551
|
+
if (error instanceof AppendRejectedError) return emitRejection(error.reason);
|
|
552
|
+
// Malformed UTF-8/JSON and recursion overflow are all `malformed_request`.
|
|
553
|
+
return emitRejection("malformed_request");
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
try {
|
|
557
|
+
return withLiveWriteDb(argv, (db) => {
|
|
558
|
+
const receipt = appendConversationMessages(db, request);
|
|
559
|
+
process.stdout.write(`${canonicalJson(receipt)}\n`);
|
|
560
|
+
return 0;
|
|
561
|
+
});
|
|
562
|
+
} catch (error) {
|
|
563
|
+
if (error instanceof AppendRejectedError) return emitRejection(error.reason);
|
|
564
|
+
return emitRejection("persistence_failure");
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/** Read at most `limit` bytes from stdin, matching Python's bounded read. */
|
|
569
|
+
function readStdinBytes(limit: number): Buffer {
|
|
570
|
+
try {
|
|
571
|
+
const buffer = readFileSync(0);
|
|
572
|
+
return buffer.byteLength > limit ? buffer.subarray(0, limit) : buffer;
|
|
573
|
+
} catch {
|
|
574
|
+
return Buffer.alloc(0);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* Shared skeleton for the sanctioned live-write commands: resolve the DB (exit 2
|
|
580
|
+
* on config failure), bootstrap a missing DB via the TS core, open the
|
|
581
|
+
* backup-gated live-write seam, assert schema state, run `write`, and always
|
|
582
|
+
* close. Schema
|
|
583
|
+
* drift maps to exit 2; every other error propagates for the caller's own
|
|
584
|
+
* handling. Callers do their argument pre-validation before invoking this.
|
|
585
|
+
*/
|
|
586
|
+
function withLiveWriteDb(argv: readonly string[], write: (db: WritableDatabase) => number): number {
|
|
587
|
+
const dbPath = resolveDbPathForCli(argv.slice(2));
|
|
588
|
+
if (dbPath === null) return 2;
|
|
589
|
+
return withLiveWriteDbAt(dbPath, argv[0] ?? null, write);
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/** The live-write seam for an already-resolved database path. */
|
|
593
|
+
function withLiveWriteDbAt(
|
|
594
|
+
dbPath: string,
|
|
595
|
+
command: string | null,
|
|
596
|
+
write: (db: WritableDatabase) => number,
|
|
597
|
+
): number {
|
|
598
|
+
// Missing DB => unbootstrapped install; TS bootstraps it (CV22.DS6.TS4) and
|
|
599
|
+
// applies any pending TS-authored migration (US3), then the backup-gated
|
|
600
|
+
// live-write seam opens the now-current file.
|
|
601
|
+
ensureDatabaseReadyForCli(dbPath, command);
|
|
602
|
+
const db = openLiveWriteDatabase(dbPath);
|
|
603
|
+
try {
|
|
604
|
+
assertSchemaState(db);
|
|
605
|
+
return write(db);
|
|
606
|
+
} catch (error) {
|
|
607
|
+
if (error instanceof SchemaStateError) {
|
|
608
|
+
console.error(`Mirror TS front door: ${error.message}`);
|
|
609
|
+
return 2;
|
|
610
|
+
}
|
|
611
|
+
throw error;
|
|
612
|
+
} finally {
|
|
613
|
+
db.close();
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/**
|
|
618
|
+
* Async counterpart to `withLiveWriteDb`, for the CV22.DS7.US3 replay-gated
|
|
619
|
+
* cultivation writes (`consolidate apply`/`scan`, `shadow scan`) whose `write`
|
|
620
|
+
* callback must `await` a replay LLM/embedding provider call. Same skeleton,
|
|
621
|
+
* schema-drift handling, and always-close guarantee.
|
|
622
|
+
*/
|
|
623
|
+
async function withLiveWriteDbAsync(
|
|
624
|
+
argv: readonly string[],
|
|
625
|
+
write: (db: WritableDatabase) => Promise<number>,
|
|
626
|
+
): Promise<number> {
|
|
627
|
+
const dbPath = resolveDbPathForCli(argv.slice(2));
|
|
628
|
+
if (dbPath === null) return 2;
|
|
629
|
+
ensureDatabaseReadyForCli(dbPath, argv[0] ?? null);
|
|
630
|
+
const db = openLiveWriteDatabase(dbPath);
|
|
631
|
+
try {
|
|
632
|
+
assertSchemaState(db);
|
|
633
|
+
return await write(db);
|
|
634
|
+
} catch (error) {
|
|
635
|
+
if (error instanceof SchemaStateError) {
|
|
636
|
+
console.error(`Mirror TS front door: ${error.message}`);
|
|
637
|
+
return 2;
|
|
638
|
+
}
|
|
639
|
+
throw error;
|
|
640
|
+
} finally {
|
|
641
|
+
db.close();
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
/** Write `rendered.text` to the correct stream and return its exit code -- the
|
|
646
|
+
* shared endpoint for every `RenderedCommand`-shaped cultivation outcome. */
|
|
647
|
+
function writeRendered(rendered: { text: string; stderr: boolean; exitCode: number }): number {
|
|
648
|
+
if (rendered.stderr) process.stderr.write(rendered.text);
|
|
649
|
+
else process.stdout.write(rendered.text);
|
|
650
|
+
return rendered.exitCode;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
function isIdentityWrite(argv: readonly string[]): boolean {
|
|
654
|
+
return argv[0] === "identity" && argv[1] === "set";
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
function isIdentityEdit(argv: readonly string[]): boolean {
|
|
658
|
+
return argv[0] === "identity" && argv[1] === "edit";
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
/**
|
|
662
|
+
* The leaves CV22.DS7.TS4 answers, once `routing.ts` has allowed them.
|
|
663
|
+
*
|
|
664
|
+
* `list` and `inspect` are SHARED commands: `list personas|journeys` and
|
|
665
|
+
* `inspect persona` are DS7.US1's ported reads and must keep reaching their own
|
|
666
|
+
* handlers. The first version of this predicate claimed both commands whole and
|
|
667
|
+
* sent all four of those leaves through the catalog route — the same
|
|
668
|
+
* inheritance the routing comments warn about, committed one file away from the
|
|
669
|
+
* warning. Named leaves only.
|
|
670
|
+
*/
|
|
671
|
+
function isExtensionCatalogCommand(argv: readonly string[]): boolean {
|
|
672
|
+
const command = argv[0];
|
|
673
|
+
if (command === "extensions" || command === "ext") return true;
|
|
674
|
+
const target = firstCatalogPositional(argv.slice(1));
|
|
675
|
+
if (command === "list") return target === null || target === "extensions" || target === "all";
|
|
676
|
+
if (command === "inspect") {
|
|
677
|
+
return (
|
|
678
|
+
target === "extension" ||
|
|
679
|
+
target === "runtime-catalog" ||
|
|
680
|
+
target === "llm-calls" ||
|
|
681
|
+
target === "embedding-provenance"
|
|
682
|
+
);
|
|
683
|
+
}
|
|
684
|
+
return false;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
/** The first non-option token, skipping the valued options these commands take. */
|
|
688
|
+
function firstCatalogPositional(args: readonly string[]): string | null {
|
|
689
|
+
const valued = new Set([
|
|
690
|
+
"--mirror-home",
|
|
691
|
+
"--db-path",
|
|
692
|
+
"--extensions-root",
|
|
693
|
+
"--runtime",
|
|
694
|
+
"--target-root",
|
|
695
|
+
]);
|
|
696
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
697
|
+
const token = args[index] as string;
|
|
698
|
+
if (valued.has(token)) {
|
|
699
|
+
index += 1;
|
|
700
|
+
continue;
|
|
701
|
+
}
|
|
702
|
+
if (!token.startsWith("--")) return token;
|
|
703
|
+
}
|
|
704
|
+
return null;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
function isLifecycleWriteFace(argv: readonly string[]): boolean {
|
|
708
|
+
return (
|
|
709
|
+
argv[0] === "conversations" &&
|
|
710
|
+
(argv.includes("--metadata-lifecycle-apply") || argv.includes("--metadata-lifecycle-demo"))
|
|
711
|
+
);
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
/**
|
|
715
|
+
* `identity edit <layer> <key>`: drive $EDITOR, then write through the same
|
|
716
|
+
* seam `identity set` uses.
|
|
717
|
+
*
|
|
718
|
+
* The editor inherits this process's streams, so the write handle is taken
|
|
719
|
+
* AFTER it returns — a human staring at vim for ten minutes should not be
|
|
720
|
+
* holding the database open, and an editor that never exits should not be
|
|
721
|
+
* holding it forever.
|
|
722
|
+
*/
|
|
723
|
+
async function runIdentityEditRoute(argv: readonly string[]): Promise<number> {
|
|
724
|
+
const { runIdentityEdit } = await import("#identity/identityEdit.ts");
|
|
725
|
+
const positionals = stripOptionWithValue(
|
|
726
|
+
stripOptionWithValue(argv.slice(2), "--db-path"),
|
|
727
|
+
"--mirror-home",
|
|
728
|
+
);
|
|
729
|
+
const layer = positionals[0];
|
|
730
|
+
const key = positionals[1];
|
|
731
|
+
if (!layer || !key) {
|
|
732
|
+
console.error("identity edit requires <layer> <key>");
|
|
733
|
+
return 2;
|
|
734
|
+
}
|
|
735
|
+
const refused = refusedNewIdentityKey(argv.slice(1), layer, key);
|
|
736
|
+
if (refused !== null) {
|
|
737
|
+
console.error(`Error: ${refused}`);
|
|
738
|
+
return 1;
|
|
739
|
+
}
|
|
740
|
+
return withMirrorWriteDb(argv, (db) => {
|
|
741
|
+
const result = runIdentityEdit(db, layer, key, {
|
|
742
|
+
editor: process.env.EDITOR,
|
|
743
|
+
visual: process.env.VISUAL,
|
|
744
|
+
nowIso,
|
|
745
|
+
newId,
|
|
746
|
+
});
|
|
747
|
+
if (result.stdout) process.stdout.write(result.stdout);
|
|
748
|
+
if (result.stderr) process.stderr.write(result.stderr);
|
|
749
|
+
return result.exitCode;
|
|
750
|
+
});
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
/** `conversations --metadata-lifecycle-apply|--metadata-lifecycle-demo`. */
|
|
754
|
+
async function runLifecycleWriteFace(argv: readonly string[]): Promise<number> {
|
|
755
|
+
const { runLifecycleWriteRoute } = await import("./lifecycleWriteRoute.ts");
|
|
756
|
+
return withMirrorWriteDb(argv, (db) => runLifecycleWriteRoute(db, argv.slice(1)));
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
/** The extension catalog family, answered from the TS core. */
|
|
760
|
+
async function runExtensionCatalog(argv: readonly string[]): Promise<DispatchOutcome> {
|
|
761
|
+
const { runExtensionCatalogRoute } = await import("./extensionCatalogRoute.ts");
|
|
762
|
+
const dbPath = resolveDbPathForCli(argv.slice(1));
|
|
763
|
+
if (dbPath === null) return { exitCode: 2 };
|
|
764
|
+
ensureDatabaseReadyForCli(dbPath, argv[0] ?? null);
|
|
765
|
+
const { pythonUtcIsoformat } = await import("#extensions/bindings.ts");
|
|
766
|
+
return runExtensionCatalogRoute(argv, {
|
|
767
|
+
mirrorHome: dirname(dbPath),
|
|
768
|
+
databasePath: dbPath,
|
|
769
|
+
deps: { nowIso: () => pythonUtcIsoformat() },
|
|
770
|
+
withReadOnlyDatabase: (run) => withMirrorReadDb(argv, run),
|
|
771
|
+
withWritableDatabase: (run) => withMirrorWriteDb(argv, run),
|
|
772
|
+
});
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
/**
|
|
776
|
+
* CR104: why `identity set` or `identity edit` must refuse to create `key` in
|
|
777
|
+
* `layer`, decided before the write seam opens, or null to go ahead.
|
|
778
|
+
*
|
|
779
|
+
* The store refuses such a create on its own (`upsertIdentity`), but only after
|
|
780
|
+
* the seam has taken its pre-write snapshot, and after `identity edit` has had the
|
|
781
|
+
* user type into an editor. Asked here first, a refusal costs neither.
|
|
782
|
+
*
|
|
783
|
+
* A key outside the grammar may still name a row that predates the grammar, and
|
|
784
|
+
* that row stays writable, so only for such a key is the row looked up, read-only.
|
|
785
|
+
* When the database cannot be resolved or read, the answer is left to the write
|
|
786
|
+
* path, which reports the resolution error and where the store still refuses.
|
|
787
|
+
*/
|
|
788
|
+
function refusedNewIdentityKey(args: readonly string[], layer: string, key: string): string | null {
|
|
789
|
+
const problem = newIdentityKeyProblem(layer, key);
|
|
790
|
+
if (problem === null) return null;
|
|
791
|
+
let dbPath: string;
|
|
792
|
+
try {
|
|
793
|
+
dbPath = resolveDbPath(args);
|
|
794
|
+
} catch {
|
|
795
|
+
return null;
|
|
796
|
+
}
|
|
797
|
+
if (!existsSync(dbPath)) return problem;
|
|
798
|
+
try {
|
|
799
|
+
const db = openDatabaseReadOnly(dbPath);
|
|
800
|
+
try {
|
|
801
|
+
return identityRowExists(db, layer, key) ? null : problem;
|
|
802
|
+
} finally {
|
|
803
|
+
db.close();
|
|
804
|
+
}
|
|
805
|
+
} catch {
|
|
806
|
+
return null;
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
/**
|
|
811
|
+
* Route `identity set <layer> <key> --content ... | stdin` to the TS core. Mirrors
|
|
812
|
+
* the Python `identity set` interface and output, but writes through the sanctioned
|
|
813
|
+
* live-write seam after a backup, reusing the ported `setIdentity`. A new journey
|
|
814
|
+
* slug or persona id outside the grammar is refused first (CR104).
|
|
815
|
+
*/
|
|
816
|
+
function runIdentityWrite(argv: readonly string[]): number {
|
|
817
|
+
const args = argv.slice(2);
|
|
818
|
+
const positionals = stripOptionWithValue(
|
|
819
|
+
stripOptionWithValue(stripOptionWithValue(args, "--content"), "--db-path"),
|
|
820
|
+
"--mirror-home",
|
|
821
|
+
);
|
|
822
|
+
const layer = positionals[0];
|
|
823
|
+
const key = positionals[1];
|
|
824
|
+
if (!layer || !key) {
|
|
825
|
+
console.error("identity set requires <layer> <key>");
|
|
826
|
+
return 2;
|
|
827
|
+
}
|
|
828
|
+
const refused = refusedNewIdentityKey(args, layer, key);
|
|
829
|
+
if (refused !== null) {
|
|
830
|
+
console.error(`Error: ${refused}`);
|
|
831
|
+
return 1;
|
|
832
|
+
}
|
|
833
|
+
const content = optionValue(args, "--content") ?? readStdinContent();
|
|
834
|
+
if (!content.trim()) {
|
|
835
|
+
console.error("Error: content is empty.");
|
|
836
|
+
return 1;
|
|
837
|
+
}
|
|
838
|
+
return withLiveWriteDb(argv, (db) => {
|
|
839
|
+
const outcome = applyIdentitySet(db, { layer, key, content, id: newId(), nowIso: nowIso() });
|
|
840
|
+
process.stdout.write(`\u2713 ${outcome.layer}/${outcome.key} ${outcome.action}\n`);
|
|
841
|
+
return 0;
|
|
842
|
+
});
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
function isJourneyWrite(argv: readonly string[]): boolean {
|
|
846
|
+
return argv[0] === "journey" && argv[1] === "set-path";
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
function isJourneyUpdateWrite(argv: readonly string[]): boolean {
|
|
850
|
+
return argv[0] === "journey" && argv[1] === "update";
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
function isMemorySearch(argv: readonly string[]): boolean {
|
|
854
|
+
return argv[0] === "memories" && argv.includes("--search");
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
function isConsult(argv: readonly string[]): boolean {
|
|
858
|
+
return argv[0] === "consult";
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
function isInit(argv: readonly string[]): boolean {
|
|
862
|
+
return argv[0] === "init";
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
/**
|
|
866
|
+
* Serve `init <user>` (DS7.US1 Slice B). Filesystem-only — no database is
|
|
867
|
+
* opened. Python's argparse defines only the `user` positional (no
|
|
868
|
+
* --mirror-home/--db-path), so none are stripped here.
|
|
869
|
+
*
|
|
870
|
+
* Deliberate divergence: an already-populated identity root or a missing
|
|
871
|
+
* templates root are uncaught exceptions in Python (a raw traceback, exit 1) --
|
|
872
|
+
* not a designed message. This preserves the contract (exit 1, a stderr
|
|
873
|
+
* message naming the failure) without fabricating a fake traceback.
|
|
874
|
+
*
|
|
875
|
+
* CV22.DS10.US3 (D3): when no source configured a user, `init` writes
|
|
876
|
+
* `MIRROR_USER=<user>` into `~/.config/mirror/env` (0600 in 0700) and says
|
|
877
|
+
* where to add `OPENROUTER_API_KEY` by hand. The key is never read from argv.
|
|
878
|
+
* When a user IS configured -- the checkout's `.env`, or a shell running a
|
|
879
|
+
* second mirror -- the file is left alone and the person is told how to
|
|
880
|
+
* select the new home.
|
|
881
|
+
*
|
|
882
|
+
* Plateau 3: it ends with the wiring step for each runtime found on the PATH
|
|
883
|
+
* (`#runtime/wiring.ts`), with this tree's path in it, so the person is handed
|
|
884
|
+
* the next line rather than a page. Printing is not automating.
|
|
885
|
+
*/
|
|
886
|
+
function runInit(argv: readonly string[], env: NodeJS.ProcessEnv = process.env): number {
|
|
887
|
+
const user = argv[1];
|
|
888
|
+
if (!user) {
|
|
889
|
+
console.error("Usage: init <user>");
|
|
890
|
+
return 2;
|
|
891
|
+
}
|
|
892
|
+
try {
|
|
893
|
+
const identityRoot = initUserHome(user);
|
|
894
|
+
const mirrorHome = dirname(identityRoot);
|
|
895
|
+
const configured = Boolean(env.MIRROR_USER || env.MIRROR_HOME);
|
|
896
|
+
const configuration = configured
|
|
897
|
+
? [
|
|
898
|
+
` 1. This machine already selects a user; to use this one, run with MIRROR_USER=${user}`,
|
|
899
|
+
` or set it in ${configFilePath(env)}`,
|
|
900
|
+
]
|
|
901
|
+
: [
|
|
902
|
+
`Configuration: ${writeConfigValue("MIRROR_USER", user, { env })} (MIRROR_USER=${user})`,
|
|
903
|
+
"",
|
|
904
|
+
"Next steps:",
|
|
905
|
+
" 1. Add OPENROUTER_API_KEY=<your key> to that file (it is created readable by you only)",
|
|
906
|
+
];
|
|
907
|
+
const prints = [
|
|
908
|
+
`Created user home: ${mirrorHome}`,
|
|
909
|
+
`Identity ready at: ${identityRoot}`,
|
|
910
|
+
...(configured ? ["", "Next steps:"] : []),
|
|
911
|
+
...configuration,
|
|
912
|
+
` 2. Run: ${PROGRAM} seed`,
|
|
913
|
+
"\nYour identity is ready to use. Deepen it over time with:",
|
|
914
|
+
` ${PROGRAM} identity edit user identity`,
|
|
915
|
+
...runtimeWiringSection(env),
|
|
916
|
+
];
|
|
917
|
+
process.stdout.write(prints.map((line) => `${line}\n`).join(""));
|
|
918
|
+
return 0;
|
|
919
|
+
} catch (error) {
|
|
920
|
+
if (error instanceof IdentityRootExistsError || error instanceof TemplatesNotFoundError) {
|
|
921
|
+
console.error(error.message);
|
|
922
|
+
return 1;
|
|
923
|
+
}
|
|
924
|
+
throw error;
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
/** The "Wire your runtime" tail of `init`, empty when no runtime is on the PATH. */
|
|
929
|
+
function runtimeWiringSection(env: NodeJS.ProcessEnv): string[] {
|
|
930
|
+
const detected = detectRuntimes(env, isExecutable);
|
|
931
|
+
if (detected.length === 0) return [];
|
|
932
|
+
const root = runningTreeRoot();
|
|
933
|
+
return [
|
|
934
|
+
"\nWire your runtime to this install (each step is one you run and can read first):",
|
|
935
|
+
...detected.flatMap((runtime) => ["", ...runtimeWiringLines(runtime, root)]),
|
|
936
|
+
];
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
function isSeed(argv: readonly string[]): boolean {
|
|
940
|
+
return argv[0] === "seed";
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
/**
|
|
944
|
+
* Serve `seed [--env E] [--mirror-home PATH] [--force]` (DS7.US1 Slice B).
|
|
945
|
+
* Uses its own resolution (seedPaths.ts) rather than the shared
|
|
946
|
+
* withLiveWriteDb skeleton: `seed`'s Python source resolves the database path
|
|
947
|
+
* differently from every other command (see seedPaths.ts's module comment for
|
|
948
|
+
* the verified, surprising divergence), though the actual backup-gated
|
|
949
|
+
* write-open below is the same sanctioned seam every other write uses.
|
|
950
|
+
*/
|
|
951
|
+
function runSeedCommand(argv: readonly string[]): number {
|
|
952
|
+
const args = argv.slice(1);
|
|
953
|
+
const force = hasOption(args, "--force");
|
|
954
|
+
const envFlag = optionValue(args, "--env");
|
|
955
|
+
const ambientEnv = process.env.MEMORY_ENV || "production";
|
|
956
|
+
const headerPrints = [`Seeding identity into [${envFlag || ambientEnv}]...\n`];
|
|
957
|
+
if (force) {
|
|
958
|
+
headerPrints.push(" (--force: existing entries will be overwritten from YAML files)\n");
|
|
959
|
+
}
|
|
960
|
+
process.stdout.write(headerPrints.map((line) => `${line}\n`).join(""));
|
|
961
|
+
|
|
962
|
+
let paths: ReturnType<typeof resolveSeedPaths>;
|
|
963
|
+
try {
|
|
964
|
+
paths = resolveSeedPaths(args);
|
|
965
|
+
} catch (error) {
|
|
966
|
+
if (error instanceof MirrorHomeNotConfiguredError) {
|
|
967
|
+
console.error(`Mirror TS front door: ${error.message}`);
|
|
968
|
+
return 2;
|
|
969
|
+
}
|
|
970
|
+
throw error;
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
ensureDatabaseReadyForCli(paths.dbPath, "seed");
|
|
974
|
+
const db = openLiveWriteDatabase(paths.dbPath);
|
|
975
|
+
try {
|
|
976
|
+
assertSchemaState(db);
|
|
977
|
+
const prints = [`Mirror home: ${paths.mirrorHome}`, `Identity root: ${paths.identityRoot}`];
|
|
978
|
+
const result = runSeed(db, paths.identityRoot, { force });
|
|
979
|
+
prints.push(...result.lines);
|
|
980
|
+
prints.push(
|
|
981
|
+
`\nResult: ${result.created} created, ${result.updated} updated, ${result.skipped} skipped`,
|
|
982
|
+
);
|
|
983
|
+
if (result.errors.length > 0) {
|
|
984
|
+
prints.push(`Errors: ${result.errors.length}`);
|
|
985
|
+
for (const err of result.errors) prints.push(` - ${err}`);
|
|
986
|
+
}
|
|
987
|
+
process.stdout.write(prints.map((line) => `${line}\n`).join(""));
|
|
988
|
+
return result.errors.length > 0 ? 1 : 0;
|
|
989
|
+
} catch (error) {
|
|
990
|
+
if (error instanceof SchemaStateError) {
|
|
991
|
+
console.error(`Mirror TS front door: ${error.message}`);
|
|
992
|
+
return 2;
|
|
993
|
+
}
|
|
994
|
+
throw error;
|
|
995
|
+
} finally {
|
|
996
|
+
db.close();
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
/** `consult credits` never logs to llm_calls (Python doesn't either, AI-09) --
|
|
1001
|
+
* so it skips the live-write db-open entirely, matching its current
|
|
1002
|
+
* read-only-external-API shape. */
|
|
1003
|
+
function isConsultCredits(argv: readonly string[]): boolean {
|
|
1004
|
+
return argv[1] === "credits";
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
/**
|
|
1008
|
+
* Route `journey set-path <slug> <path>` to the TS core. Normalizes the path like
|
|
1009
|
+
* Python (`Path.expanduser().resolve()`), writes through the live-write seam after a
|
|
1010
|
+
* backup via the ported setProjectPath, and mirrors Python's output: the resolved
|
|
1011
|
+
* path on stdout, a status line on stderr, and a not-found error + exit 1.
|
|
1012
|
+
*/
|
|
1013
|
+
function runJourneyWrite(argv: readonly string[]): number {
|
|
1014
|
+
const args = argv.slice(2);
|
|
1015
|
+
const positionals = stripOptionWithValue(
|
|
1016
|
+
stripOptionWithValue(args, "--db-path"),
|
|
1017
|
+
"--mirror-home",
|
|
1018
|
+
);
|
|
1019
|
+
const slug = positionals[0];
|
|
1020
|
+
const rawPath = positionals[1];
|
|
1021
|
+
if (!slug || !rawPath) {
|
|
1022
|
+
console.error("Usage: journey set-path <slug> <path>");
|
|
1023
|
+
return 2;
|
|
1024
|
+
}
|
|
1025
|
+
return withLiveWriteDb(argv, (db) => {
|
|
1026
|
+
try {
|
|
1027
|
+
const resolved = applyJourneySetPath(db, slug, rawPath, nowIso());
|
|
1028
|
+
console.error(`project_path set for '${slug}': ${resolved}`);
|
|
1029
|
+
process.stdout.write(`${resolved}\n`);
|
|
1030
|
+
return 0;
|
|
1031
|
+
} catch (error) {
|
|
1032
|
+
if (error instanceof JourneyNotFoundError) {
|
|
1033
|
+
console.error(`Error: journey '${slug}' not found.`);
|
|
1034
|
+
return 1;
|
|
1035
|
+
}
|
|
1036
|
+
throw error;
|
|
1037
|
+
}
|
|
1038
|
+
});
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
/**
|
|
1042
|
+
* Route `journey update <slug> <content>` to the TS core (DS7.US1 Slice B).
|
|
1043
|
+
* A thin wrapper over the already-ported `setIdentity`: Python's own
|
|
1044
|
+
* `set_journey_path` is the SAME upsert-identity primitive with a fixed
|
|
1045
|
+
* `journey_path` layer, no created/updated verb distinction (always prints
|
|
1046
|
+
* "updated", matching Python exactly), and no check that the journey slug
|
|
1047
|
+
* itself exists.
|
|
1048
|
+
*
|
|
1049
|
+
* CR073: the stdin sentinel is exactly `-`. A FLAG-SHAPED argument -- one or
|
|
1050
|
+
* two hyphens then a letter -- is a mistyped sentinel (`-stdin`, `--stdin`) or
|
|
1051
|
+
* a stray option, never path text, and is refused before anything is written.
|
|
1052
|
+
* A markdown list (`- item`) or an em-dash lead is not flag-shaped and stays
|
|
1053
|
+
* valid: four of six real journey paths carry lists. Empty content is refused
|
|
1054
|
+
* as `identity set` already does. Messages and exit codes are byte-exact to
|
|
1055
|
+
* the Python oracle (`ts/test/goldens/journey-update.golden.json`), including
|
|
1056
|
+
* the usage path's exit 1, which this route previously returned as 2.
|
|
1057
|
+
*/
|
|
1058
|
+
const JOURNEY_UPDATE_USAGE =
|
|
1059
|
+
`Usage: ${PROGRAM} journey update <slug> <content>\n` +
|
|
1060
|
+
" Pass '-' as <content> to read it from stdin.";
|
|
1061
|
+
const FLAG_SHAPED_CONTENT = /^--?[A-Za-z]/;
|
|
1062
|
+
|
|
1063
|
+
function runJourneyUpdateWrite(argv: readonly string[]): number {
|
|
1064
|
+
const args = argv.slice(2);
|
|
1065
|
+
const positionals = stripOptionWithValue(
|
|
1066
|
+
stripOptionWithValue(args, "--db-path"),
|
|
1067
|
+
"--mirror-home",
|
|
1068
|
+
);
|
|
1069
|
+
const slug = positionals[0];
|
|
1070
|
+
let content = positionals[1];
|
|
1071
|
+
if (!slug || content === undefined) {
|
|
1072
|
+
console.error(JOURNEY_UPDATE_USAGE);
|
|
1073
|
+
return 1;
|
|
1074
|
+
}
|
|
1075
|
+
if (content === "-") {
|
|
1076
|
+
content = readStdinContent();
|
|
1077
|
+
} else if (FLAG_SHAPED_CONTENT.test(content)) {
|
|
1078
|
+
console.error(
|
|
1079
|
+
`Error: '${content}' looks like an option, not journey path text. ` +
|
|
1080
|
+
"Pass '-' as <content> to read it from stdin.",
|
|
1081
|
+
);
|
|
1082
|
+
return 1;
|
|
1083
|
+
}
|
|
1084
|
+
if (!content.trim()) {
|
|
1085
|
+
console.error("Error: content is empty.");
|
|
1086
|
+
return 1;
|
|
1087
|
+
}
|
|
1088
|
+
return withLiveWriteDb(argv, (db) => {
|
|
1089
|
+
setIdentity(db, { id: newId(), layer: JOURNEY_PATH_LAYER, key: slug, content }, nowIso());
|
|
1090
|
+
console.error(`Journey path '${slug}' updated.`);
|
|
1091
|
+
return 0;
|
|
1092
|
+
});
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
function isTasksSubcommandWrite(argv: readonly string[]): boolean {
|
|
1096
|
+
return (
|
|
1097
|
+
argv[0] === "tasks" &&
|
|
1098
|
+
(argv[1] === "add" ||
|
|
1099
|
+
argv[1] === "done" ||
|
|
1100
|
+
argv[1] === "doing" ||
|
|
1101
|
+
argv[1] === "block" ||
|
|
1102
|
+
argv[1] === "delete" ||
|
|
1103
|
+
argv[1] === "import" ||
|
|
1104
|
+
argv[1] === "sync" ||
|
|
1105
|
+
argv[1] === "sync-config")
|
|
1106
|
+
);
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
/**
|
|
1110
|
+
* Route `tasks add <title> [--journey J] [--due D] [--stage S]` to the TS core
|
|
1111
|
+
* (DS7.US2 slice 3a). Python's `title` positional accepts exactly one argv
|
|
1112
|
+
* token (no `nargs`); extra unrecognized positionals are an argparse error in
|
|
1113
|
+
* Python, not reproduced here beyond a generic usage message.
|
|
1114
|
+
*/
|
|
1115
|
+
function runTasksAddWrite(argv: readonly string[]): number {
|
|
1116
|
+
const args = argv.slice(2);
|
|
1117
|
+
const journey = optionValue(args, "--journey");
|
|
1118
|
+
const due = optionValue(args, "--due");
|
|
1119
|
+
const stage = optionValue(args, "--stage");
|
|
1120
|
+
const title = stripOptionWithValue(
|
|
1121
|
+
stripOptionWithValue(
|
|
1122
|
+
stripOptionWithValue(
|
|
1123
|
+
stripOptionWithValue(stripOptionWithValue(args, "--journey"), "--due"),
|
|
1124
|
+
"--stage",
|
|
1125
|
+
),
|
|
1126
|
+
"--db-path",
|
|
1127
|
+
),
|
|
1128
|
+
"--mirror-home",
|
|
1129
|
+
)[0];
|
|
1130
|
+
if (!title) {
|
|
1131
|
+
console.error("Usage: tasks add <title> [--journey J] [--due D] [--stage S]");
|
|
1132
|
+
return 2;
|
|
1133
|
+
}
|
|
1134
|
+
return withLiveWriteDb(argv, (db) => {
|
|
1135
|
+
const task = applyTasksAdd(db, { title, journey, due, stage }, newId(), nowIso());
|
|
1136
|
+
process.stdout.write(renderTasksAdd(task));
|
|
1137
|
+
return 0;
|
|
1138
|
+
});
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
/**
|
|
1142
|
+
* Route `tasks done|doing|block <task_id>` to the TS core (DS7.US2 slice 3a).
|
|
1143
|
+
* Matches Python's `cmd_status_change`: business-logic outcomes (ambiguous,
|
|
1144
|
+
* not found) print to STDOUT and exit 0 -- Python never calls `sys.exit()` in
|
|
1145
|
+
* this command, so no branch here maps to a nonzero exit code either.
|
|
1146
|
+
*/
|
|
1147
|
+
function runTasksStatusChangeWrite(argv: readonly string[]): number {
|
|
1148
|
+
// argv[1] is "done" | "doing" | "block" here (isTasksSubcommandWrite already
|
|
1149
|
+
// narrowed it); only "block" needs translating to the Task model's status.
|
|
1150
|
+
const newStatus: TaskStatusTarget =
|
|
1151
|
+
argv[1] === "block" ? "blocked" : (argv[1] as TaskStatusTarget);
|
|
1152
|
+
const args = argv.slice(2);
|
|
1153
|
+
const idOrPrefix = stripOptionWithValue(
|
|
1154
|
+
stripOptionWithValue(args, "--db-path"),
|
|
1155
|
+
"--mirror-home",
|
|
1156
|
+
)[0];
|
|
1157
|
+
if (!idOrPrefix) {
|
|
1158
|
+
console.error(`Usage: tasks ${argv[1]} <task_id>`);
|
|
1159
|
+
return 2;
|
|
1160
|
+
}
|
|
1161
|
+
return withLiveWriteDb(argv, (db) => {
|
|
1162
|
+
const outcome = applyTasksStatusChange(db, idOrPrefix, newStatus, nowIso());
|
|
1163
|
+
process.stdout.write(renderTasksStatusChange(outcome));
|
|
1164
|
+
return 0;
|
|
1165
|
+
});
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
/**
|
|
1169
|
+
* Route `tasks delete <task_id>` to the TS core (DS7.US2 slice 3a). Matches
|
|
1170
|
+
* Python's `cmd_delete`: business-logic "not found" prints to STDOUT and exits
|
|
1171
|
+
* 0, same as status-change.
|
|
1172
|
+
*/
|
|
1173
|
+
function runTasksDeleteWrite(argv: readonly string[]): number {
|
|
1174
|
+
const args = argv.slice(2);
|
|
1175
|
+
const idOrPrefix = stripOptionWithValue(
|
|
1176
|
+
stripOptionWithValue(args, "--db-path"),
|
|
1177
|
+
"--mirror-home",
|
|
1178
|
+
)[0];
|
|
1179
|
+
if (!idOrPrefix) {
|
|
1180
|
+
console.error("Usage: tasks delete <task_id>");
|
|
1181
|
+
return 2;
|
|
1182
|
+
}
|
|
1183
|
+
return withLiveWriteDb(argv, (db) => {
|
|
1184
|
+
const outcome = applyTasksDelete(db, idOrPrefix);
|
|
1185
|
+
process.stdout.write(renderTasksDelete(outcome));
|
|
1186
|
+
return 0;
|
|
1187
|
+
});
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
/**
|
|
1191
|
+
* Route `tasks import [journey]` to the TS core (DS7.US2 slice 3c). The
|
|
1192
|
+
* `journey` positional is optional in Python (`nargs="?"`); when absent, every
|
|
1193
|
+
* known journey is attempted.
|
|
1194
|
+
*/
|
|
1195
|
+
function runTasksImportWrite(argv: readonly string[]): number {
|
|
1196
|
+
const args = argv.slice(2);
|
|
1197
|
+
const journey =
|
|
1198
|
+
stripOptionWithValue(stripOptionWithValue(args, "--db-path"), "--mirror-home")[0] ?? null;
|
|
1199
|
+
return withLiveWriteDb(argv, (db) => {
|
|
1200
|
+
const results = applyTasksImport(db, journey);
|
|
1201
|
+
process.stdout.write(renderTasksImport(results));
|
|
1202
|
+
return 0;
|
|
1203
|
+
});
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
/**
|
|
1207
|
+
* Route `tasks sync [journey]` to the TS core (DS7.US2 slice 3c). Matches
|
|
1208
|
+
* Python's `cmd_sync`: per-journey errors (no sync file, sync file missing on
|
|
1209
|
+
* disk, or any other failure) print `❌ {journey}: {message}` and move on to
|
|
1210
|
+
* the next journey rather than aborting the whole command; exit is always 0
|
|
1211
|
+
* (no `sys.exit()` anywhere in `cmd_sync`).
|
|
1212
|
+
*/
|
|
1213
|
+
function runTasksSyncWrite(argv: readonly string[]): number {
|
|
1214
|
+
const args = argv.slice(2);
|
|
1215
|
+
const journey =
|
|
1216
|
+
stripOptionWithValue(stripOptionWithValue(args, "--db-path"), "--mirror-home")[0] ?? null;
|
|
1217
|
+
return withLiveWriteDb(argv, (db) => {
|
|
1218
|
+
const journeys = resolveSyncJourneys(db, journey);
|
|
1219
|
+
if (journeys.length === 0) {
|
|
1220
|
+
process.stdout.write(renderTasksSyncNoJourneysConfigured());
|
|
1221
|
+
return 0;
|
|
1222
|
+
}
|
|
1223
|
+
const lines: string[] = [];
|
|
1224
|
+
for (const j of journeys) {
|
|
1225
|
+
lines.push(renderTasksSyncOutcome(applyTasksSyncForJourney(db, j)));
|
|
1226
|
+
}
|
|
1227
|
+
process.stdout.write(lines.join(""));
|
|
1228
|
+
return 0;
|
|
1229
|
+
});
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
/**
|
|
1233
|
+
* Route `tasks sync-config <journey> <file_path>` to the TS core (DS7.US2
|
|
1234
|
+
* slice 3c). Matches Python's `cmd_sync_config`: an unknown journey is an
|
|
1235
|
+
* uncaught `ValueError` in Python (no try/except around `set_sync_file`); this
|
|
1236
|
+
* maps it to a clear stderr message and exit 1 instead of fabricating a fake
|
|
1237
|
+
* traceback (the same DS7.US1 `init` precedent).
|
|
1238
|
+
*/
|
|
1239
|
+
function runTasksSyncConfigWrite(argv: readonly string[]): number {
|
|
1240
|
+
const args = argv.slice(2);
|
|
1241
|
+
const positionals = stripOptionWithValue(
|
|
1242
|
+
stripOptionWithValue(args, "--db-path"),
|
|
1243
|
+
"--mirror-home",
|
|
1244
|
+
);
|
|
1245
|
+
const journey = positionals[0];
|
|
1246
|
+
const filePath = positionals[1];
|
|
1247
|
+
if (!journey || !filePath) {
|
|
1248
|
+
console.error("Usage: tasks sync-config <journey> <file_path>");
|
|
1249
|
+
return 2;
|
|
1250
|
+
}
|
|
1251
|
+
return withLiveWriteDb(argv, (db) => {
|
|
1252
|
+
try {
|
|
1253
|
+
const outcome = applyTasksSyncConfig(db, journey, filePath, nowIso());
|
|
1254
|
+
process.stdout.write(renderTasksSyncConfig(outcome));
|
|
1255
|
+
return 0;
|
|
1256
|
+
} catch (error) {
|
|
1257
|
+
if (error instanceof JourneyNotFoundError) {
|
|
1258
|
+
console.error(`Error: journey '${journey}' not found.`);
|
|
1259
|
+
return 1;
|
|
1260
|
+
}
|
|
1261
|
+
throw error;
|
|
1262
|
+
}
|
|
1263
|
+
});
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
/** Dispatch a `tasks add|done|doing|block|delete|import|sync|sync-config` write to its sub-handler. */
|
|
1267
|
+
function runTasksWrite(argv: readonly string[]): number {
|
|
1268
|
+
if (argv[1] === "add") return runTasksAddWrite(argv);
|
|
1269
|
+
if (argv[1] === "delete") return runTasksDeleteWrite(argv);
|
|
1270
|
+
if (argv[1] === "import") return runTasksImportWrite(argv);
|
|
1271
|
+
if (argv[1] === "sync-config") return runTasksSyncConfigWrite(argv);
|
|
1272
|
+
if (argv[1] === "sync") return runTasksSyncWrite(argv);
|
|
1273
|
+
return runTasksStatusChangeWrite(argv);
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
// --- consolidate / shadow writes (CV22.DS7.US3) ------------------------------
|
|
1277
|
+
|
|
1278
|
+
/** Route `consolidate reject <proposal_id>` to the TS core. */
|
|
1279
|
+
function runConsolidateRejectWrite(argv: readonly string[]): number {
|
|
1280
|
+
const args = argv.slice(2);
|
|
1281
|
+
const proposalId = stripOptionWithValue(
|
|
1282
|
+
stripOptionWithValue(args, "--db-path"),
|
|
1283
|
+
"--mirror-home",
|
|
1284
|
+
)[0];
|
|
1285
|
+
if (!proposalId) {
|
|
1286
|
+
console.error("Usage: consolidate reject <proposal_id>");
|
|
1287
|
+
return 2;
|
|
1288
|
+
}
|
|
1289
|
+
return withLiveWriteDb(argv, (db) =>
|
|
1290
|
+
writeRendered(renderConsolidateReject(runReject(db, proposalId, nowIso()))),
|
|
1291
|
+
);
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
/** Route `shadow reject <proposal_id>` to the TS core. */
|
|
1295
|
+
function runShadowRejectWrite(argv: readonly string[]): number {
|
|
1296
|
+
const args = argv.slice(2);
|
|
1297
|
+
const proposalId = stripOptionWithValue(
|
|
1298
|
+
stripOptionWithValue(args, "--db-path"),
|
|
1299
|
+
"--mirror-home",
|
|
1300
|
+
)[0];
|
|
1301
|
+
if (!proposalId) {
|
|
1302
|
+
console.error("Usage: shadow reject <proposal_id>");
|
|
1303
|
+
return 2;
|
|
1304
|
+
}
|
|
1305
|
+
return withLiveWriteDb(argv, (db) =>
|
|
1306
|
+
writeRendered(renderShadowReject(runReject(db, proposalId, nowIso()))),
|
|
1307
|
+
);
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
/** Route `shadow apply <proposal_id> [--content "..."]` to the TS core. Deterministic --
|
|
1311
|
+
* no replay gate needed (the write is a hardcoded-layer identity append). */
|
|
1312
|
+
function runShadowApplyWrite(argv: readonly string[]): number {
|
|
1313
|
+
const args = argv.slice(2);
|
|
1314
|
+
const positionals = stripOptionWithValue(
|
|
1315
|
+
stripOptionWithValue(stripOptionWithValue(args, "--content"), "--db-path"),
|
|
1316
|
+
"--mirror-home",
|
|
1317
|
+
);
|
|
1318
|
+
const proposalId = positionals[0];
|
|
1319
|
+
if (!proposalId) {
|
|
1320
|
+
console.error('Usage: shadow apply <proposal_id> [--content "..."]');
|
|
1321
|
+
return 2;
|
|
1322
|
+
}
|
|
1323
|
+
const overrideContent = optionValue(args, "--content");
|
|
1324
|
+
return withLiveWriteDb(argv, (db) => {
|
|
1325
|
+
const outcome = runShadowApplyRoute(db, proposalId, overrideContent, {
|
|
1326
|
+
id: newId(),
|
|
1327
|
+
nowIso: nowIso(),
|
|
1328
|
+
});
|
|
1329
|
+
return writeRendered(renderShadowApply(outcome));
|
|
1330
|
+
});
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
/**
|
|
1334
|
+
* Route `consolidate apply <proposal_id> [--content "..."]` to the TS core.
|
|
1335
|
+
* The whole command is gated on `MIRROR_TS_CULTIVATION_EMBEDDING_REPLAY`
|
|
1336
|
+
* (routing.ts already refuses to route here otherwise) because the action is
|
|
1337
|
+
* read from the DB, not argv -- a `merge` proposal needs the replay embedding
|
|
1338
|
+
* provider even though `identity_update`/`shadow_candidate` do not.
|
|
1339
|
+
*/
|
|
1340
|
+
async function runConsolidateApplyWrite(argv: readonly string[]): Promise<number> {
|
|
1341
|
+
const args = argv.slice(2);
|
|
1342
|
+
const positionals = stripOptionWithValue(
|
|
1343
|
+
stripOptionWithValue(stripOptionWithValue(args, "--content"), "--db-path"),
|
|
1344
|
+
"--mirror-home",
|
|
1345
|
+
);
|
|
1346
|
+
const proposalId = positionals[0];
|
|
1347
|
+
if (!proposalId) {
|
|
1348
|
+
console.error('Usage: consolidate apply <proposal_id> [--content "..."]');
|
|
1349
|
+
return 2;
|
|
1350
|
+
}
|
|
1351
|
+
const overrideContent = optionValue(args, "--content");
|
|
1352
|
+
const family = await resolveFamilyProviders(process.env, CULTIVATION_APPLY_TRANSPORT);
|
|
1353
|
+
if (!family?.embedding) throw new Error("consolidate apply requires an embedding provider");
|
|
1354
|
+
const embeddingProvider = family.embedding;
|
|
1355
|
+
return withLiveWriteDbAsync(argv, async (db) => {
|
|
1356
|
+
const outcome = await runConsolidateApplyRoute(
|
|
1357
|
+
db,
|
|
1358
|
+
proposalId,
|
|
1359
|
+
overrideContent,
|
|
1360
|
+
{ identityId: newId(), mergeMemoryId: newId(), nowIso: nowIso() },
|
|
1361
|
+
embeddingProvider,
|
|
1362
|
+
);
|
|
1363
|
+
return writeRendered(renderConsolidateApply(outcome));
|
|
1364
|
+
});
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
/**
|
|
1368
|
+
* Route `consolidate scan [--journey J] [--layer L] [--limit N] [--threshold F]`
|
|
1369
|
+
* to the TS core, gated on `MIRROR_TS_CULTIVATION_LLM_REPLAY` (routing.ts).
|
|
1370
|
+
*/
|
|
1371
|
+
async function runConsolidateScanWrite(argv: readonly string[]): Promise<number> {
|
|
1372
|
+
const args = argv.slice(2);
|
|
1373
|
+
const journey = optionValue(args, "--journey");
|
|
1374
|
+
const layer = optionValue(args, "--layer");
|
|
1375
|
+
const limitRaw = optionValue(args, "--limit");
|
|
1376
|
+
const limit = limitRaw !== null ? Number(limitRaw) : DEFAULT_CONSOLIDATE_SCAN_LIMIT;
|
|
1377
|
+
const thresholdRaw = optionValue(args, "--threshold");
|
|
1378
|
+
const threshold = thresholdRaw !== null ? Number(thresholdRaw) : DEFAULT_CLUSTER_THRESHOLD;
|
|
1379
|
+
const family = await resolveFamilyProviders(process.env, CULTIVATION_SCAN_TRANSPORT);
|
|
1380
|
+
if (!family?.llm) throw new Error("consolidate scan requires an LLM provider");
|
|
1381
|
+
const provider = family.llm;
|
|
1382
|
+
const reporter = outcomeReporter(argv, "consolidation");
|
|
1383
|
+
return withLiveWriteDbAsync(argv, async (db) => {
|
|
1384
|
+
const result = await consolidateScan(db, {
|
|
1385
|
+
journey,
|
|
1386
|
+
layer,
|
|
1387
|
+
limit,
|
|
1388
|
+
threshold,
|
|
1389
|
+
provider,
|
|
1390
|
+
id: newId,
|
|
1391
|
+
nowIso,
|
|
1392
|
+
onLlmCall: chatLedgerHook(db, "consolidation"),
|
|
1393
|
+
onOutcome: reporter.onOutcome,
|
|
1394
|
+
});
|
|
1395
|
+
reporter.logSummary();
|
|
1396
|
+
process.stdout.write(renderConsolidateScan(result, threshold));
|
|
1397
|
+
return 0;
|
|
1398
|
+
});
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
/** Route `shadow scan [--limit N]` to the TS core, gated on `MIRROR_TS_CULTIVATION_LLM_REPLAY`. */
|
|
1402
|
+
async function runShadowScanWrite(argv: readonly string[]): Promise<number> {
|
|
1403
|
+
const args = argv.slice(2);
|
|
1404
|
+
const limitRaw = optionValue(args, "--limit");
|
|
1405
|
+
const limit = limitRaw !== null ? Number(limitRaw) : DEFAULT_SHADOW_SCAN_LIMIT;
|
|
1406
|
+
const family = await resolveFamilyProviders(process.env, CULTIVATION_SCAN_TRANSPORT);
|
|
1407
|
+
if (!family?.llm) throw new Error("shadow scan requires an LLM provider");
|
|
1408
|
+
const provider = family.llm;
|
|
1409
|
+
const reporter = outcomeReporter(argv, "shadow_scan");
|
|
1410
|
+
return withLiveWriteDbAsync(argv, async (db) => {
|
|
1411
|
+
const result = await shadowScan(db, {
|
|
1412
|
+
limit,
|
|
1413
|
+
provider,
|
|
1414
|
+
id: newId,
|
|
1415
|
+
nowIso,
|
|
1416
|
+
onLlmCall: chatLedgerHook(db, "shadow_scan"),
|
|
1417
|
+
onOutcome: reporter.onOutcome,
|
|
1418
|
+
});
|
|
1419
|
+
reporter.logSummary();
|
|
1420
|
+
process.stdout.write(renderShadowScan(result));
|
|
1421
|
+
return 0;
|
|
1422
|
+
});
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
function isConsolidateSubcommandWrite(argv: readonly string[]): boolean {
|
|
1426
|
+
return (
|
|
1427
|
+
argv[0] === "consolidate" && (argv[1] === "reject" || argv[1] === "apply" || argv[1] === "scan")
|
|
1428
|
+
);
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
function isShadowSubcommandWrite(argv: readonly string[]): boolean {
|
|
1432
|
+
return (
|
|
1433
|
+
argv[0] === "shadow" && (argv[1] === "reject" || argv[1] === "apply" || argv[1] === "scan")
|
|
1434
|
+
);
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
/** Dispatch a `consolidate reject|apply|scan` write to its sub-handler. */
|
|
1438
|
+
function runConsolidateWrite(argv: readonly string[]): number | Promise<number> {
|
|
1439
|
+
if (argv[1] === "reject") return runConsolidateRejectWrite(argv);
|
|
1440
|
+
if (argv[1] === "apply") return runConsolidateApplyWrite(argv);
|
|
1441
|
+
return runConsolidateScanWrite(argv); // "scan"
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
/** Dispatch a `shadow reject|apply|scan` write to its sub-handler. */
|
|
1445
|
+
function runShadowWrite(argv: readonly string[]): number | Promise<number> {
|
|
1446
|
+
if (argv[1] === "reject") return runShadowRejectWrite(argv);
|
|
1447
|
+
if (argv[1] === "apply") return runShadowApplyWrite(argv);
|
|
1448
|
+
return runShadowScanWrite(argv); // "scan"
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
function withMirrorReadDb(argv: readonly string[], read: (db: Database) => number): number {
|
|
1452
|
+
const dbPath = resolveDbPathForCli(argv.slice(1));
|
|
1453
|
+
if (dbPath === null) return 2;
|
|
1454
|
+
ensureDatabaseReadyForCli(dbPath, argv[0] ?? null);
|
|
1455
|
+
const db = openDatabaseReadOnly(dbPath);
|
|
1456
|
+
try {
|
|
1457
|
+
assertSchemaState(db);
|
|
1458
|
+
return read(db);
|
|
1459
|
+
} catch (error) {
|
|
1460
|
+
if (error instanceof SchemaStateError) {
|
|
1461
|
+
console.error(`Mirror TS front door: ${error.message}`);
|
|
1462
|
+
return 2;
|
|
1463
|
+
}
|
|
1464
|
+
throw error;
|
|
1465
|
+
} finally {
|
|
1466
|
+
db.close();
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
async function withMirrorWriteDb(
|
|
1471
|
+
argv: readonly string[],
|
|
1472
|
+
write: (db: WritableDatabase, dbPath: string) => Promise<number> | number,
|
|
1473
|
+
): Promise<number> {
|
|
1474
|
+
const dbPath = resolveDbPathForCli(argv.slice(1));
|
|
1475
|
+
if (dbPath === null) return 2;
|
|
1476
|
+
ensureDatabaseReadyForCli(dbPath, argv[0] ?? null);
|
|
1477
|
+
const db = openLiveWriteDatabase(dbPath);
|
|
1478
|
+
try {
|
|
1479
|
+
assertSchemaState(db);
|
|
1480
|
+
return await write(db, dbPath);
|
|
1481
|
+
} catch (error) {
|
|
1482
|
+
if (error instanceof SchemaStateError) {
|
|
1483
|
+
console.error(`Mirror TS front door: ${error.message}`);
|
|
1484
|
+
return 2;
|
|
1485
|
+
}
|
|
1486
|
+
throw error;
|
|
1487
|
+
} finally {
|
|
1488
|
+
db.close();
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
function runMirrorWrite(argv: readonly string[]): Promise<number> | number {
|
|
1493
|
+
if (mirrorDeactivateMissingSession(argv)) {
|
|
1494
|
+
console.error("warning: mirror deactivate invoked without --session-id; no state change.");
|
|
1495
|
+
return 0;
|
|
1496
|
+
}
|
|
1497
|
+
const reporter = outcomeReporter(argv, "reception");
|
|
1498
|
+
return withMirrorWriteDb(argv, async (db, dbPath) => {
|
|
1499
|
+
const exitCode = await runMirrorWriteRoute(db, dbPath, argv, process.env, {
|
|
1500
|
+
onReceptionOutcome: reporter.onOutcome,
|
|
1501
|
+
});
|
|
1502
|
+
// Only a `--query` run reaches reception; a deterministic `mirror load`
|
|
1503
|
+
// makes no call, and a `calls=0` line for it would be noise.
|
|
1504
|
+
if (reporter.calls > 0) reporter.logSummary();
|
|
1505
|
+
return exitCode;
|
|
1506
|
+
});
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
function runModeWrite(argv: readonly string[]): Promise<number> {
|
|
1510
|
+
return withMirrorWriteDb(argv, (db) => runModeWriteRoute(db, argv));
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
function runSoulWrite(argv: readonly string[]): Promise<number> {
|
|
1514
|
+
return withMirrorWriteDb(argv, (db) => runSoulRoute(db, argv));
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
function runExploreWrite(argv: readonly string[]): Promise<number> {
|
|
1518
|
+
return withMirrorWriteDb(argv, (db, dbPath) =>
|
|
1519
|
+
runExploreRoute(db, argv, {
|
|
1520
|
+
...defaultExploreRouteDeps(),
|
|
1521
|
+
// `story promote` ends in a Builder session start, so it needs the same
|
|
1522
|
+
// wiring plateau 8's `build` route will use: the composed transport, the
|
|
1523
|
+
// close tail, and the clone-role guard. Lazy, so the other eleven story
|
|
1524
|
+
// actions never touch the provider seam.
|
|
1525
|
+
buildLoadRuntime: () => createBuildLoadRuntime({ db, dbPath }),
|
|
1526
|
+
}),
|
|
1527
|
+
);
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
/** CV22.DS7.US5: the conversation logger, the product's primary write path. */
|
|
1531
|
+
function runConversationLoggerWrite(argv: readonly string[]): Promise<number> {
|
|
1532
|
+
return withMirrorWriteDb(argv, (db, dbPath) => runConversationLoggerRoute(db, dbPath, argv));
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
/**
|
|
1536
|
+
* Surface a provider-backed call's outcome into the front-door log
|
|
1537
|
+
* (CV22.DS8.US3).
|
|
1538
|
+
*
|
|
1539
|
+
* One SUMMARY line per invocation, always -- it carries `calls=N` and the full
|
|
1540
|
+
* outcome distribution, which is what makes a fan-out visible after the fact
|
|
1541
|
+
* and what the live smoke reads instead of counting ledger rows. Plus one line
|
|
1542
|
+
* per NON-answered call, because only those carry a `kind=` worth having; a
|
|
1543
|
+
* line per successful call would triple the log for no diagnostic gain.
|
|
1544
|
+
*
|
|
1545
|
+
* Category only: no cluster content, no query, no model output.
|
|
1546
|
+
*/
|
|
1547
|
+
function outcomeReporter(
|
|
1548
|
+
argv: readonly string[],
|
|
1549
|
+
surface: string,
|
|
1550
|
+
): {
|
|
1551
|
+
onOutcome: (report: ProviderCallReport) => void;
|
|
1552
|
+
logSummary: () => void;
|
|
1553
|
+
readonly calls: number;
|
|
1554
|
+
} {
|
|
1555
|
+
const logPath = resolveLogPath(argv);
|
|
1556
|
+
const tally = new CallOutcomeTally();
|
|
1557
|
+
return {
|
|
1558
|
+
get calls() {
|
|
1559
|
+
return tally.calls;
|
|
1560
|
+
},
|
|
1561
|
+
onOutcome: (report) => {
|
|
1562
|
+
tally.record(report);
|
|
1563
|
+
if (report.outcome === "answered") return;
|
|
1564
|
+
logFrontDoor(logPath, {
|
|
1565
|
+
command: argv[0] ?? null,
|
|
1566
|
+
route: "ts",
|
|
1567
|
+
exitCode: 0,
|
|
1568
|
+
detail: formatCallOutcome(surface, report),
|
|
1569
|
+
});
|
|
1570
|
+
},
|
|
1571
|
+
logSummary: () => {
|
|
1572
|
+
logFrontDoor(logPath, {
|
|
1573
|
+
command: argv[0] ?? null,
|
|
1574
|
+
route: "ts",
|
|
1575
|
+
exitCode: 0,
|
|
1576
|
+
detail: `${surface} ${tally.summary()}`,
|
|
1577
|
+
});
|
|
1578
|
+
},
|
|
1579
|
+
};
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1582
|
+
/** Best-effort log path from the same resolver; null when unconfigured. */
|
|
1583
|
+
function resolveLogPath(argv: readonly string[]): string | null {
|
|
1584
|
+
try {
|
|
1585
|
+
return frontDoorLogPath(resolveDbPath(argv));
|
|
1586
|
+
} catch {
|
|
1587
|
+
return null;
|
|
1588
|
+
}
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
/**
|
|
1592
|
+
* `consult <model> <question>` logs to llm_calls on a best-effort basis
|
|
1593
|
+
* (AI-09). Unlike `runMemorySearch`/`runIdentityWrite`/`runJourneyWrite`,
|
|
1594
|
+
* consult's established contract does NOT require a database to exist --
|
|
1595
|
+
* it is fundamentally an external-API command, and logging is an added,
|
|
1596
|
+
* optional side effect. If a db cannot be resolved, opened, backed up, or
|
|
1597
|
+
* schema-checked for any reason, consult must still run; it just logs
|
|
1598
|
+
* nothing -- the same fail-soft principle `logLlmCall` applies to the write
|
|
1599
|
+
* itself, extended to db resolution.
|
|
1600
|
+
*/
|
|
1601
|
+
async function runConsultAskWithDb(argv: readonly string[]): Promise<number> {
|
|
1602
|
+
const db = tryOpenDbForConsultLogging(argv);
|
|
1603
|
+
try {
|
|
1604
|
+
process.stdout.write(await runConsultRoute(db, argv.slice(1)));
|
|
1605
|
+
return 0;
|
|
1606
|
+
} finally {
|
|
1607
|
+
db?.close();
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
export function tryOpenDbForConsultLogging(argv: readonly string[]): WritableDatabase | null {
|
|
1612
|
+
let db: WritableDatabase | null = null;
|
|
1613
|
+
try {
|
|
1614
|
+
// Uses resolveDbPath directly, NOT resolveDbPathForCli -- that wrapper
|
|
1615
|
+
// prints a user-facing error on an unconfigured mirror home, which is
|
|
1616
|
+
// correct for commands that require a db but wrong here: consult without
|
|
1617
|
+
// a configured home must stay completely silent about logging, exactly
|
|
1618
|
+
// as it behaved before this CR.
|
|
1619
|
+
const dbPath = resolveDbPath(argv.slice(1));
|
|
1620
|
+
if (!existsSync(dbPath)) return null;
|
|
1621
|
+
db = openLiveWriteDatabase(dbPath);
|
|
1622
|
+
assertSchemaState(db);
|
|
1623
|
+
return db;
|
|
1624
|
+
} catch {
|
|
1625
|
+
db?.close();
|
|
1626
|
+
return null;
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
/**
|
|
1631
|
+
* `mcp` (CV22.DS10.TS5, F9): the oracle's command, answered by the server
|
|
1632
|
+
* CV22.DS9 ported. The module IS the server -- importing it serves stdio, and
|
|
1633
|
+
* the import settles when the client closes stdin. The plugin launches the same
|
|
1634
|
+
* module directly; this is the front door's way in, not a second server.
|
|
1635
|
+
*/
|
|
1636
|
+
async function runMcpServer(): Promise<number> {
|
|
1637
|
+
await import("#mcp/main.ts");
|
|
1638
|
+
return 0;
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
async function runMemorySearch(argv: readonly string[]): Promise<number> {
|
|
1642
|
+
const dbPath = resolveDbPathForCli(argv.slice(1));
|
|
1643
|
+
if (dbPath === null) return 2;
|
|
1644
|
+
// Missing DB => unbootstrapped install; TS bootstraps it (CV22.DS6.TS4) and
|
|
1645
|
+
// applies any pending TS-authored migration (US3) before the backup-gated
|
|
1646
|
+
// search read/log path opens it.
|
|
1647
|
+
ensureDatabaseReadyForCli(dbPath, argv[0] ?? null);
|
|
1648
|
+
const db = openLiveWriteDatabase(dbPath);
|
|
1649
|
+
try {
|
|
1650
|
+
assertSchemaState(db);
|
|
1651
|
+
process.stdout.write(
|
|
1652
|
+
await runMemorySearchRoute(db, argv.slice(1), {
|
|
1653
|
+
onDegraded: (detail) =>
|
|
1654
|
+
logFrontDoor(frontDoorLogPath(dbPath), {
|
|
1655
|
+
command: argv[0] ?? null,
|
|
1656
|
+
route: "ts",
|
|
1657
|
+
exitCode: 0,
|
|
1658
|
+
detail,
|
|
1659
|
+
}),
|
|
1660
|
+
}),
|
|
1661
|
+
);
|
|
1662
|
+
return 0;
|
|
1663
|
+
} catch (error) {
|
|
1664
|
+
if (error instanceof SchemaStateError) {
|
|
1665
|
+
console.error(`Mirror TS front door: ${error.message}`);
|
|
1666
|
+
return 2;
|
|
1667
|
+
}
|
|
1668
|
+
throw error;
|
|
1669
|
+
} finally {
|
|
1670
|
+
db.close();
|
|
1671
|
+
}
|
|
1672
|
+
}
|
|
1673
|
+
|
|
1674
|
+
/**
|
|
1675
|
+
* The exit code, with the content-free route metadata a route may add for the
|
|
1676
|
+
* durable front-door log.
|
|
1677
|
+
*
|
|
1678
|
+
* (Until CV22.DS10.TS5 this also carried which ENGINE answered, because a
|
|
1679
|
+
* TypeScript route could still fall back to Python inside dispatch and the log
|
|
1680
|
+
* records the outcome, not the intent -- RS009 CR059/CR064. Nothing falls back
|
|
1681
|
+
* now; every outcome is TypeScript's.)
|
|
1682
|
+
*/
|
|
1683
|
+
interface DispatchOutcome {
|
|
1684
|
+
exitCode: number;
|
|
1685
|
+
/** Content-free route metadata for the durable front-door log. */
|
|
1686
|
+
detail?: string;
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
async function dispatch(argv: readonly string[]): Promise<DispatchOutcome> {
|
|
1690
|
+
const result = await dispatchTs(argv);
|
|
1691
|
+
return typeof result === "number" ? { exitCode: result } : result;
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1694
|
+
/** Every route the front door answers once routing has chosen it. */
|
|
1695
|
+
async function dispatchTs(argv: readonly string[]): Promise<number | DispatchOutcome> {
|
|
1696
|
+
if (isConversationLoggerCommand(argv)) {
|
|
1697
|
+
// CV9.E2.S32: the Pi extension's one-process session open. The write is
|
|
1698
|
+
// the same `withMirrorWriteDb`; the welcome renders follow it in-process.
|
|
1699
|
+
if (isSessionStartWelcome(argv))
|
|
1700
|
+
return runSessionStartWelcomeRoute(argv, { withMirrorWriteDb });
|
|
1701
|
+
return runConversationLoggerWrite(argv);
|
|
1702
|
+
}
|
|
1703
|
+
if (argv[0] === "build") {
|
|
1704
|
+
// The Builder command tree is deliberately lazy: the other commands never
|
|
1705
|
+
// pay for loading it, and a broken Builder core cannot take them down.
|
|
1706
|
+
const { runBuildRoute } = await import("./buildRoute.ts");
|
|
1707
|
+
return runBuildRoute(argv, {
|
|
1708
|
+
withReadOnlyDatabase: (run) => withMirrorReadDb(argv, run),
|
|
1709
|
+
withWritableDatabase: (run) => withMirrorWriteDb(argv, run),
|
|
1710
|
+
createLoadRuntime: (db, dbPath, ignoreProductionRole) =>
|
|
1711
|
+
createBuildLoadRuntime({ db, dbPath, ignoreProductionRole }),
|
|
1712
|
+
nowIso,
|
|
1713
|
+
environmentSessionId: process.env.MIRROR_SESSION_ID ?? null,
|
|
1714
|
+
});
|
|
1715
|
+
}
|
|
1716
|
+
if (isInit(argv)) return runInit(argv);
|
|
1717
|
+
if (isSeed(argv)) return runSeedCommand(argv);
|
|
1718
|
+
if (isIdentityWrite(argv)) return runIdentityWrite(argv);
|
|
1719
|
+
if (isIdentityEdit(argv)) return runIdentityEditRoute(argv);
|
|
1720
|
+
// CV22.DS7.TS4: the extension catalog family. Lazy like the Builder tree, and
|
|
1721
|
+
// for the same reason: the other commands never pay for loading it.
|
|
1722
|
+
if (isExtensionCatalogCommand(argv)) return runExtensionCatalog(argv);
|
|
1723
|
+
if (isLifecycleWriteFace(argv)) return runLifecycleWriteFace(argv);
|
|
1724
|
+
if (isJourneyWrite(argv)) return runJourneyWrite(argv);
|
|
1725
|
+
if (isJourneyUpdateWrite(argv)) return runJourneyUpdateWrite(argv);
|
|
1726
|
+
if (isTasksSubcommandWrite(argv)) return runTasksWrite(argv);
|
|
1727
|
+
if (isWeekSaveWrite(argv)) return runWeekSaveWrite(argv);
|
|
1728
|
+
if (argv[0] === "week" && argv[1] === "plan") return runWeekPlanContentRoute(argv);
|
|
1729
|
+
if (argv[0] === "journal") return runJournalContentRoute(argv);
|
|
1730
|
+
if (argv[0] === "descriptor" && argv[1] === "generate") return runDescriptorContentRoute(argv);
|
|
1731
|
+
if (isConsolidateSubcommandWrite(argv)) return runConsolidateWrite(argv);
|
|
1732
|
+
if (isShadowSubcommandWrite(argv)) return runShadowWrite(argv);
|
|
1733
|
+
if (isMirrorWrite(argv)) return runMirrorWrite(argv);
|
|
1734
|
+
if (isModeWrite(argv)) return runModeWrite(argv);
|
|
1735
|
+
// CV22.DS7.US6: the Soul ritual; `routing.ts` allowlists its subcommands by
|
|
1736
|
+
// name.
|
|
1737
|
+
if (argv[0] === "soul") return runSoulWrite(argv);
|
|
1738
|
+
if (argv[0] === "explore") return runExploreWrite(argv);
|
|
1739
|
+
if (isConversationsAppend(argv)) return runConversationsAppend(argv);
|
|
1740
|
+
// CV22.DS7.TS1: the DB safety tools. `backup` never opens or bootstraps the
|
|
1741
|
+
// database; `repair-encoding --apply` rides the live-write seam.
|
|
1742
|
+
if (argv[0] === "backup") {
|
|
1743
|
+
return runBackupRoute(argv, {
|
|
1744
|
+
// CR060: the backup reports an unresolved home itself, in one line.
|
|
1745
|
+
resolveDbPath: (args) => resolveDbPath(args),
|
|
1746
|
+
withLiveWriteDb: (dbPath, write) => withLiveWriteDbAt(dbPath, "backup", write),
|
|
1747
|
+
});
|
|
1748
|
+
}
|
|
1749
|
+
if (argv[0] === "repair-encoding") {
|
|
1750
|
+
return runRepairEncodingRoute(argv, {
|
|
1751
|
+
resolveDbPath: resolveDbPathForCli,
|
|
1752
|
+
withLiveWriteDb: (dbPath, write) => withLiveWriteDbAt(dbPath, "repair-encoding", write),
|
|
1753
|
+
});
|
|
1754
|
+
}
|
|
1755
|
+
// CV22.DS7.TS3: the daily-visible tail. Both are read-only and open the
|
|
1756
|
+
// database, when at all, through a read-only handle -- no bootstrap, no
|
|
1757
|
+
// migrate-on-open, and for `welcome --status-line` no subprocess whatsoever.
|
|
1758
|
+
if (argv[0] === "welcome") return runWelcomeRoute(argv);
|
|
1759
|
+
if (argv[0] === "runtime") return runRuntimeReadRoute(argv);
|
|
1760
|
+
if (argv[0] === "mcp") return runMcpServer();
|
|
1761
|
+
if (isMemorySearch(argv)) return runMemorySearch(argv);
|
|
1762
|
+
if (isConsult(argv)) {
|
|
1763
|
+
if (isConsultCredits(argv)) {
|
|
1764
|
+
process.stdout.write(await runConsultRoute(null, argv.slice(1)));
|
|
1765
|
+
return 0;
|
|
1766
|
+
}
|
|
1767
|
+
return runConsultAskWithDb(argv);
|
|
1768
|
+
}
|
|
1769
|
+
return runTs(argv);
|
|
1770
|
+
}
|
|
1771
|
+
|
|
1772
|
+
export async function main(rawArgv = process.argv.slice(2)): Promise<number> {
|
|
1773
|
+
const nodeError = nodeVersionError(process.versions.node);
|
|
1774
|
+
if (nodeError) {
|
|
1775
|
+
console.error(nodeError);
|
|
1776
|
+
return 1;
|
|
1777
|
+
}
|
|
1778
|
+
// Once, before routing, and the SAME argv goes to dispatch: a family's
|
|
1779
|
+
// leading options move after its subcommand (CV22.DS10.TS5, finding F5), so
|
|
1780
|
+
// the router and every handler read one shape.
|
|
1781
|
+
const argv = canonicalArgv(rawArgv);
|
|
1782
|
+
const decision = routeMemoryCommand(argv);
|
|
1783
|
+
const logPath = resolveLogPath(argv);
|
|
1784
|
+
// CV22.DS10.TS4: a surface this migration removed answers here, before any
|
|
1785
|
+
// dispatch -- so nothing is spawned, no database is opened, and NO STDIN IS
|
|
1786
|
+
// READ. `journey mutate` took JSON on stdin; refusing before the read is what
|
|
1787
|
+
// makes the refusal immediate instead of a hang, and what keeps the caller's
|
|
1788
|
+
// payload out of this process entirely.
|
|
1789
|
+
if (decision.engine === "retired") {
|
|
1790
|
+
process.stderr.write(retiredRefusal(decision));
|
|
1791
|
+
logFrontDoor(logPath, {
|
|
1792
|
+
command: decision.command,
|
|
1793
|
+
route: "retired",
|
|
1794
|
+
exitCode: 1,
|
|
1795
|
+
// Metadata only, and constant per entry: the anchor names the cutoff,
|
|
1796
|
+
// never the arguments the caller passed to the removed command.
|
|
1797
|
+
detail: `cutoff=${decision.anchor}`,
|
|
1798
|
+
});
|
|
1799
|
+
return 1;
|
|
1800
|
+
}
|
|
1801
|
+
// CV22.DS10.TS5 (D2): a name the front door does not answer. Same posture as
|
|
1802
|
+
// a retired surface -- answered before dispatch, so nothing is opened, read
|
|
1803
|
+
// from stdin, or spawned -- and logged by SCOPE only: an unknown top-level
|
|
1804
|
+
// name is whatever the caller typed, and it stays out of the log.
|
|
1805
|
+
if (decision.engine === "usage") {
|
|
1806
|
+
const env = process.env;
|
|
1807
|
+
const answer = answerUsage(decision.request, {
|
|
1808
|
+
userConfigured: Boolean(env.MIRROR_USER || env.MIRROR_HOME || env.MEMORY_DIR || env.DB_PATH),
|
|
1809
|
+
configFile: configFilePath(env),
|
|
1810
|
+
});
|
|
1811
|
+
(answer.stream === "stdout" ? process.stdout : process.stderr).write(answer.text);
|
|
1812
|
+
logFrontDoor(logPath, {
|
|
1813
|
+
command: decision.command,
|
|
1814
|
+
route: "usage",
|
|
1815
|
+
exitCode: answer.exitCode,
|
|
1816
|
+
detail: `usage=${decision.request.scope}`,
|
|
1817
|
+
});
|
|
1818
|
+
return answer.exitCode;
|
|
1819
|
+
}
|
|
1820
|
+
try {
|
|
1821
|
+
const outcome = await dispatch(argv);
|
|
1822
|
+
logFrontDoor(logPath, {
|
|
1823
|
+
command: decision.command,
|
|
1824
|
+
route: decision.engine,
|
|
1825
|
+
exitCode: outcome.exitCode,
|
|
1826
|
+
...(outcome.detail ? { detail: outcome.detail } : {}),
|
|
1827
|
+
});
|
|
1828
|
+
return outcome.exitCode;
|
|
1829
|
+
} catch (error) {
|
|
1830
|
+
// Metadata-only: the error's name/category, never argument values.
|
|
1831
|
+
const detail = error instanceof Error ? error.name : "unknown error";
|
|
1832
|
+
// A refusal no route answered is still a refusal: one line, by name, and
|
|
1833
|
+
// never a stack trace (`namedRefusal.ts`).
|
|
1834
|
+
const refusal = namedRefusal(error);
|
|
1835
|
+
if (refusal !== null) {
|
|
1836
|
+
console.error(refusal.line);
|
|
1837
|
+
logFrontDoor(logPath, {
|
|
1838
|
+
command: decision.command,
|
|
1839
|
+
route: decision.engine,
|
|
1840
|
+
exitCode: refusal.exitCode,
|
|
1841
|
+
detail,
|
|
1842
|
+
});
|
|
1843
|
+
return refusal.exitCode;
|
|
1844
|
+
}
|
|
1845
|
+
logFrontDoor(logPath, {
|
|
1846
|
+
command: decision.command,
|
|
1847
|
+
route: decision.engine,
|
|
1848
|
+
exitCode: 1,
|
|
1849
|
+
detail,
|
|
1850
|
+
});
|
|
1851
|
+
throw error;
|
|
1852
|
+
}
|
|
1853
|
+
}
|
|
1854
|
+
|
|
1855
|
+
/**
|
|
1856
|
+
* What the entry does, for the two ways the program is started: this file run
|
|
1857
|
+
* directly (a checkout, `npm link`), or the `bin/mirror.js` shim that an
|
|
1858
|
+
* installed package needs because Node will not strip types under
|
|
1859
|
+
* `node_modules` (CV22.DS10.US3, D15). Configuration and the warning filter
|
|
1860
|
+
* run here and nowhere else, so importing this module does neither.
|
|
1861
|
+
*/
|
|
1862
|
+
export async function runAsEntry(rawArgv = process.argv.slice(2)): Promise<number> {
|
|
1863
|
+
silenceExperimentalWarnings();
|
|
1864
|
+
loadConfiguration({ entryPath: import.meta.filename });
|
|
1865
|
+
return main(rawArgv);
|
|
1866
|
+
}
|
|
1867
|
+
|
|
1868
|
+
// Run only when invoked as the CLI entry, not when imported (keeps the module
|
|
1869
|
+
// importable for tests and tooling, and keeps `process.env` untouched there).
|
|
1870
|
+
if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
|
|
1871
|
+
process.exitCode = await runAsEntry();
|
|
1872
|
+
}
|