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