mirror-mind 0.0.1 → 0.32.1
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-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 +561 -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 +99 -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.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 +13 -0
- package/plugins/mirror-mind/hooks/hooks.json +50 -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/mcp/launch.sh +52 -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 +65 -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/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 +576 -0
- package/ts/src/conversation/loggerCli.ts +282 -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 +1865 -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 +73 -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 +999 -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/soulRoute.ts +433 -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 +252 -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 +176 -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 +68 -0
- package/ts/src/hooks/runtime.ts +159 -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/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,312 @@
|
|
|
1
|
+
// Task read/write model parity port (CV22.DS7.US2).
|
|
2
|
+
//
|
|
3
|
+
// A faithful TypeScript port of `TaskStore` (`src/memory/storage/tasks.py`) and
|
|
4
|
+
// `TaskService.list_tasks` (`src/memory/services/tasks.py`). Following the
|
|
5
|
+
// database-seam philosophy established in DS2/DS4, ordering and filtering are
|
|
6
|
+
// pushed down to SQLite exactly as Python's SQL does (including `NULLS LAST`,
|
|
7
|
+
// supported by the bundled `node:sqlite` engine) — these functions are query
|
|
8
|
+
// builders + row mappers, not a re-implementation of SQLite's sort.
|
|
9
|
+
//
|
|
10
|
+
// `resolveTaskByIdOrPrefix` is the ONE shared prefix resolver behind
|
|
11
|
+
// `done`/`doing`/`block`/`delete` (the `kebab_slug` writer/locator lesson: two
|
|
12
|
+
// commands must not grow two copies of the same scan). Python itself has no
|
|
13
|
+
// such shared function — each CLI command inlines its own scan and prints its
|
|
14
|
+
// own message, and the two commands disagree on the ambiguous case
|
|
15
|
+
// (`status_change` reports "Ambiguous ID"; `delete` folds it into "not found").
|
|
16
|
+
// This resolver reproduces that fact honestly: it returns what happened
|
|
17
|
+
// (found/ambiguous/not_found), and each front-door command decides how to
|
|
18
|
+
// render the ambiguous case — so the asymmetry lives in presentation, where
|
|
19
|
+
// Python's own asymmetry actually lives, not baked into the resolver's logic.
|
|
20
|
+
|
|
21
|
+
import type { Database, SqlValue, WritableDatabase } from "#db/database.ts";
|
|
22
|
+
import { optionalString, requireString } from "#db/rowDecode.ts";
|
|
23
|
+
|
|
24
|
+
/** A task row, mirroring the Python `Task` model exactly. */
|
|
25
|
+
export interface Task {
|
|
26
|
+
id: string;
|
|
27
|
+
journey: string | null;
|
|
28
|
+
title: string;
|
|
29
|
+
status: string; // 'todo' | 'doing' | 'done' | 'blocked'
|
|
30
|
+
due_date: string | null;
|
|
31
|
+
scheduled_at: string | null;
|
|
32
|
+
time_hint: string | null;
|
|
33
|
+
stage: string | null;
|
|
34
|
+
context: string | null;
|
|
35
|
+
source: string; // 'manual' | 'journey_path' | 'conversation' | 'week_plan' | 'sync'
|
|
36
|
+
created_at: string;
|
|
37
|
+
updated_at: string;
|
|
38
|
+
completed_at: string | null;
|
|
39
|
+
metadata: string | null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const TASK_COLUMNS =
|
|
43
|
+
"id, journey, title, status, due_date, scheduled_at, time_hint, stage, context, " +
|
|
44
|
+
"source, created_at, updated_at, completed_at, metadata";
|
|
45
|
+
|
|
46
|
+
function toTask(row: Record<string, unknown>): Task {
|
|
47
|
+
return {
|
|
48
|
+
id: requireString(row, "id"),
|
|
49
|
+
journey: optionalString(row, "journey"),
|
|
50
|
+
title: requireString(row, "title"),
|
|
51
|
+
status: requireString(row, "status"),
|
|
52
|
+
due_date: optionalString(row, "due_date"),
|
|
53
|
+
scheduled_at: optionalString(row, "scheduled_at"),
|
|
54
|
+
time_hint: optionalString(row, "time_hint"),
|
|
55
|
+
stage: optionalString(row, "stage"),
|
|
56
|
+
context: optionalString(row, "context"),
|
|
57
|
+
source: requireString(row, "source"),
|
|
58
|
+
created_at: requireString(row, "created_at"),
|
|
59
|
+
updated_at: requireString(row, "updated_at"),
|
|
60
|
+
completed_at: optionalString(row, "completed_at"),
|
|
61
|
+
metadata: optionalString(row, "metadata"),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// --- Reads -----------------------------------------------------------------
|
|
66
|
+
|
|
67
|
+
/** Port of `Store.get_task`. */
|
|
68
|
+
export function getTaskById(db: Database, id: string): Task | null {
|
|
69
|
+
const row = db.prepare(`SELECT ${TASK_COLUMNS} FROM tasks WHERE id = ?`).get(id);
|
|
70
|
+
return row === undefined ? null : toTask(row);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Port of `Store.get_all_tasks`: `ORDER BY status, due_date ASC NULLS LAST, created_at ASC`. */
|
|
74
|
+
export function getAllTasks(db: Database): Task[] {
|
|
75
|
+
return db
|
|
76
|
+
.prepare(
|
|
77
|
+
`SELECT ${TASK_COLUMNS} FROM tasks ORDER BY status, due_date ASC NULLS LAST, created_at ASC`,
|
|
78
|
+
)
|
|
79
|
+
.all()
|
|
80
|
+
.map(toTask);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Port of `Store.get_open_tasks`: status IN ('todo','doing','blocked'), optional journey filter. */
|
|
84
|
+
export function getOpenTasks(db: Database, journey: string | null = null): Task[] {
|
|
85
|
+
const base = `SELECT ${TASK_COLUMNS} FROM tasks WHERE status IN ('todo', 'doing', 'blocked')`;
|
|
86
|
+
const order = "ORDER BY due_date ASC NULLS LAST, created_at ASC";
|
|
87
|
+
if (journey) {
|
|
88
|
+
return db.prepare(`${base} AND journey = ? ${order}`).all(journey).map(toTask);
|
|
89
|
+
}
|
|
90
|
+
return db.prepare(`${base} ${order}`).all().map(toTask);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Port of `Store.get_tasks_by_status`. */
|
|
94
|
+
export function getTasksByStatus(db: Database, status: string): Task[] {
|
|
95
|
+
return db
|
|
96
|
+
.prepare(
|
|
97
|
+
`SELECT ${TASK_COLUMNS} FROM tasks WHERE status = ? ORDER BY due_date ASC NULLS LAST, created_at ASC`,
|
|
98
|
+
)
|
|
99
|
+
.all(status)
|
|
100
|
+
.map(toTask);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Port of `Store.get_tasks_by_journey`. */
|
|
104
|
+
export function getTasksByJourney(db: Database, journey: string): Task[] {
|
|
105
|
+
return db
|
|
106
|
+
.prepare(
|
|
107
|
+
`SELECT ${TASK_COLUMNS} FROM tasks WHERE journey = ? ORDER BY due_date ASC NULLS LAST, created_at ASC`,
|
|
108
|
+
)
|
|
109
|
+
.all(journey)
|
|
110
|
+
.map(toTask);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Port of `Store.find_tasks_by_title`: `LIKE %fragment%`, optional journey filter. */
|
|
114
|
+
export function findTasksByTitle(
|
|
115
|
+
db: Database,
|
|
116
|
+
titleFragment: string,
|
|
117
|
+
journey: string | null = null,
|
|
118
|
+
): Task[] {
|
|
119
|
+
const pattern = `%${titleFragment}%`;
|
|
120
|
+
if (journey) {
|
|
121
|
+
return db
|
|
122
|
+
.prepare(
|
|
123
|
+
`SELECT ${TASK_COLUMNS} FROM tasks WHERE title LIKE ? AND journey = ? ORDER BY created_at DESC`,
|
|
124
|
+
)
|
|
125
|
+
.all(pattern, journey)
|
|
126
|
+
.map(toTask);
|
|
127
|
+
}
|
|
128
|
+
return db
|
|
129
|
+
.prepare(`SELECT ${TASK_COLUMNS} FROM tasks WHERE title LIKE ? ORDER BY created_at DESC`)
|
|
130
|
+
.all(pattern)
|
|
131
|
+
.map(toTask);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Build the parameterized week-range query, mirroring Python's builder exactly:
|
|
136
|
+
* `due_date` in range OR `scheduled_at` in range (both bounds inclusive), the
|
|
137
|
+
* `scheduled_at` upper bound extended to `T23:59` so a same-day scheduled item
|
|
138
|
+
* is not excluded by a bare date string, ordered `due_date ASC NULLS LAST,
|
|
139
|
+
* scheduled_at ASC NULLS LAST`. `NULLS LAST` is honored by the bundled SQLite
|
|
140
|
+
* engine (>= 3.30), same as Python's.
|
|
141
|
+
*/
|
|
142
|
+
export function buildTasksForWeekQuery(
|
|
143
|
+
startDate: string,
|
|
144
|
+
endDate: string,
|
|
145
|
+
): { sql: string; params: SqlValue[] } {
|
|
146
|
+
return {
|
|
147
|
+
sql:
|
|
148
|
+
`SELECT ${TASK_COLUMNS} FROM tasks ` +
|
|
149
|
+
"WHERE (due_date >= ? AND due_date <= ?) OR (scheduled_at >= ? AND scheduled_at <= ?) " +
|
|
150
|
+
"ORDER BY due_date ASC NULLS LAST, scheduled_at ASC NULLS LAST",
|
|
151
|
+
params: [startDate, endDate, startDate, `${endDate}T23:59`],
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Port of `Store.get_tasks_for_week`. */
|
|
156
|
+
export function getTasksForWeek(db: Database, startDate: string, endDate: string): Task[] {
|
|
157
|
+
const { sql, params } = buildTasksForWeekQuery(startDate, endDate);
|
|
158
|
+
return db
|
|
159
|
+
.prepare(sql)
|
|
160
|
+
.all(...params)
|
|
161
|
+
.map(toTask);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Filters for `listTasks`, mirroring `TaskService.list_tasks`'s parameters. */
|
|
165
|
+
export interface ListTasksFilters {
|
|
166
|
+
journey?: string | null;
|
|
167
|
+
status?: string | null;
|
|
168
|
+
openOnly?: boolean;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Port of `TaskService.list_tasks`: branch order is open_only -> status ->
|
|
173
|
+
* journey -> all, matching Python exactly (an `openOnly` request ignores
|
|
174
|
+
* `status`, and a `status` request applies `journey` as a post-filter over the
|
|
175
|
+
* status read rather than its own query, exactly as Python does).
|
|
176
|
+
*/
|
|
177
|
+
export function listTasks(db: Database, filters: ListTasksFilters = {}): Task[] {
|
|
178
|
+
const { journey = null, status = null, openOnly = false } = filters;
|
|
179
|
+
if (openOnly) return getOpenTasks(db, journey);
|
|
180
|
+
if (status) {
|
|
181
|
+
const tasks = getTasksByStatus(db, status);
|
|
182
|
+
return journey ? tasks.filter((t) => t.journey === journey) : tasks;
|
|
183
|
+
}
|
|
184
|
+
if (journey) return getTasksByJourney(db, journey);
|
|
185
|
+
return getAllTasks(db);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// --- Prefix resolution -------------------------------------------------------
|
|
189
|
+
|
|
190
|
+
/** The outcome of resolving a task id or id-prefix, naming every real branch. */
|
|
191
|
+
export type ResolveTaskResult =
|
|
192
|
+
| { kind: "found"; task: Task }
|
|
193
|
+
| { kind: "ambiguous"; matches: Task[] }
|
|
194
|
+
| { kind: "not_found" };
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Resolve a task by exact id, falling back to a unique-prefix scan over ALL
|
|
198
|
+
* tasks (`Task.id.startswith(idOrPrefix)`), matching the inline logic
|
|
199
|
+
* duplicated in Python's `cmd_status_change`/`cmd_delete`. Callers render the
|
|
200
|
+
* `ambiguous` case differently on purpose (see module doc); this function only
|
|
201
|
+
* reports the fact.
|
|
202
|
+
*/
|
|
203
|
+
export function resolveTaskByIdOrPrefix(db: Database, idOrPrefix: string): ResolveTaskResult {
|
|
204
|
+
const exact = getTaskById(db, idOrPrefix);
|
|
205
|
+
if (exact) return { kind: "found", task: exact };
|
|
206
|
+
|
|
207
|
+
const matches = getAllTasks(db).filter((t) => t.id.startsWith(idOrPrefix));
|
|
208
|
+
if (matches.length === 1) return { kind: "found", task: matches[0] };
|
|
209
|
+
if (matches.length > 1) return { kind: "ambiguous", matches };
|
|
210
|
+
return { kind: "not_found" };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// --- Writes ------------------------------------------------------------------
|
|
214
|
+
|
|
215
|
+
/** Fields accepted when creating a task, mirroring `TaskService.add_task`'s parameters. */
|
|
216
|
+
export interface CreateTaskInput {
|
|
217
|
+
title: string;
|
|
218
|
+
journey?: string | null;
|
|
219
|
+
dueDate?: string | null;
|
|
220
|
+
scheduledAt?: string | null;
|
|
221
|
+
timeHint?: string | null;
|
|
222
|
+
stage?: string | null;
|
|
223
|
+
context?: string | null;
|
|
224
|
+
source?: string;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Port of `TaskService.add_task` + `Store.create_task`. `id` and `nowIso` are
|
|
229
|
+
* injected by the caller (front door), the same determinism-injection idiom
|
|
230
|
+
* DS4 established: `created_at`/`updated_at` both take the single injected
|
|
231
|
+
* `nowIso`, matching a frozen-clock Python run where every `_now()` call
|
|
232
|
+
* returns the same value. New tasks always start `status: "todo"`,
|
|
233
|
+
* `completed_at: null` — Python's `Task` defaults, never overridden here.
|
|
234
|
+
*/
|
|
235
|
+
export function createTask(
|
|
236
|
+
db: WritableDatabase,
|
|
237
|
+
input: CreateTaskInput,
|
|
238
|
+
id: string,
|
|
239
|
+
nowIso: string,
|
|
240
|
+
): Task {
|
|
241
|
+
const task: Task = {
|
|
242
|
+
id,
|
|
243
|
+
journey: input.journey ?? null,
|
|
244
|
+
title: input.title,
|
|
245
|
+
status: "todo",
|
|
246
|
+
due_date: input.dueDate ?? null,
|
|
247
|
+
scheduled_at: input.scheduledAt ?? null,
|
|
248
|
+
time_hint: input.timeHint ?? null,
|
|
249
|
+
stage: input.stage ?? null,
|
|
250
|
+
context: input.context ?? null,
|
|
251
|
+
source: input.source ?? "manual",
|
|
252
|
+
created_at: nowIso,
|
|
253
|
+
updated_at: nowIso,
|
|
254
|
+
completed_at: null,
|
|
255
|
+
metadata: null,
|
|
256
|
+
};
|
|
257
|
+
db.prepare(
|
|
258
|
+
`INSERT INTO tasks (${TASK_COLUMNS}) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
259
|
+
).run(
|
|
260
|
+
task.id,
|
|
261
|
+
task.journey,
|
|
262
|
+
task.title,
|
|
263
|
+
task.status,
|
|
264
|
+
task.due_date,
|
|
265
|
+
task.scheduled_at,
|
|
266
|
+
task.time_hint,
|
|
267
|
+
task.stage,
|
|
268
|
+
task.context,
|
|
269
|
+
task.source,
|
|
270
|
+
task.created_at,
|
|
271
|
+
task.updated_at,
|
|
272
|
+
task.completed_at,
|
|
273
|
+
task.metadata,
|
|
274
|
+
);
|
|
275
|
+
return task;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Port of `Store.update_task(task_id, status=...)`, the primitive behind
|
|
280
|
+
* `doing`/`block`. Always bumps `updated_at` to the injected `nowIso`,
|
|
281
|
+
* matching Python's `Store.update_task`, which sets `updated_at = _now()` on
|
|
282
|
+
* every call regardless of which other fields are passed.
|
|
283
|
+
*/
|
|
284
|
+
export function updateTaskStatus(
|
|
285
|
+
db: WritableDatabase,
|
|
286
|
+
id: string,
|
|
287
|
+
status: string,
|
|
288
|
+
nowIso: string,
|
|
289
|
+
): void {
|
|
290
|
+
db.prepare("UPDATE tasks SET status = ?, updated_at = ? WHERE id = ?").run(status, nowIso, id);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Port of `TaskService.complete_task` + the `Store.update_task` it calls:
|
|
295
|
+
* sets `status = 'done'`, `completed_at`, and `updated_at` to the single
|
|
296
|
+
* injected `nowIso` (a frozen-clock run returns the same value for both of
|
|
297
|
+
* Python's two independent `_now()` calls here).
|
|
298
|
+
*/
|
|
299
|
+
export function completeTask(db: WritableDatabase, id: string, nowIso: string): void {
|
|
300
|
+
db.prepare("UPDATE tasks SET status = 'done', completed_at = ?, updated_at = ? WHERE id = ?").run(
|
|
301
|
+
nowIso,
|
|
302
|
+
nowIso,
|
|
303
|
+
id,
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/** Port of `Store.delete_task`: returns whether a row was actually removed. */
|
|
308
|
+
export function deleteTaskById(db: WritableDatabase, id: string): boolean {
|
|
309
|
+
const before = getTaskById(db, id);
|
|
310
|
+
db.prepare("DELETE FROM tasks WHERE id = ?").run(id);
|
|
311
|
+
return before !== null;
|
|
312
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// `week view` business logic parity port (CV22.DS7.US2 slice 3b).
|
|
2
|
+
//
|
|
3
|
+
// A faithful port of the non-printing logic inside `cmd_view`
|
|
4
|
+
// (`src/memory/cli/week.py`): computing the Monday-Sunday week window around
|
|
5
|
+
// "now", filtering out tasks scheduled in the past that aren't done, and
|
|
6
|
+
// grouping/sorting the rest by calendar day. Rendering (icons, columns,
|
|
7
|
+
// headers) is a separate concern in `frontDoor/render/week.ts`.
|
|
8
|
+
//
|
|
9
|
+
// `now` is always injected as a `Date` constructed via the LOCAL constructor
|
|
10
|
+
// form (`new Date(y, m, d, h, mi)`), never `Date.UTC`/an ISO `Z` string --
|
|
11
|
+
// Python's `datetime.now()` is naive local time, and JS's local constructor
|
|
12
|
+
// stores the same literal calendar/clock fields with no timezone conversion,
|
|
13
|
+
// so the two agree on any machine regardless of its system timezone. The same
|
|
14
|
+
// applies to parsing `scheduled_at` ("YYYY-MM-DDTHH:MM", no offset): per the
|
|
15
|
+
// JS Date spec, a date-TIME string without a designator parses as local time
|
|
16
|
+
// (unlike a bare date string, which parses as UTC midnight -- a real
|
|
17
|
+
// asymmetry, avoided here because `scheduled_at` always carries the `T`).
|
|
18
|
+
|
|
19
|
+
import type { Task } from "./taskStore.ts";
|
|
20
|
+
|
|
21
|
+
export interface WeekRange {
|
|
22
|
+
/** ISO date (YYYY-MM-DD), Monday of the week containing `now`. */
|
|
23
|
+
start: string;
|
|
24
|
+
/** ISO date (YYYY-MM-DD), the Sunday six days after `start`. */
|
|
25
|
+
end: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function pad2(n: number): string {
|
|
29
|
+
return String(n).padStart(2, "0");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Format y/m(1-based)/d as an ISO date string. */
|
|
33
|
+
export function formatIsoDate(year: number, month: number, day: number): string {
|
|
34
|
+
return `${String(year).padStart(4, "0")}-${pad2(month)}-${pad2(day)}`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Add `days` (may be negative) to an ISO date string, DST-safe (calendar-only, no time component). */
|
|
38
|
+
export function addIsoDays(isoDate: string, days: number): string {
|
|
39
|
+
const [y, m, d] = isoDate.split("-").map(Number);
|
|
40
|
+
const shifted = new Date(y, m - 1, d + days);
|
|
41
|
+
return formatIsoDate(shifted.getFullYear(), shifted.getMonth() + 1, shifted.getDate());
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** The local calendar date (YYYY-MM-DD) of a `Date`, ignoring its time-of-day. */
|
|
45
|
+
export function toIsoDate(date: Date): string {
|
|
46
|
+
return formatIsoDate(date.getFullYear(), date.getMonth() + 1, date.getDate());
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The Monday-Sunday week containing `now`, mirroring Python's
|
|
51
|
+
* `start = today - timedelta(days=today.weekday())`. JS `getDay()` is
|
|
52
|
+
* Sunday=0..Saturday=6; Python's `weekday()` is Monday=0..Sunday=6, so the
|
|
53
|
+
* conversion is `(getDay() + 6) % 7`.
|
|
54
|
+
*/
|
|
55
|
+
export function computeWeekRange(now: Date): WeekRange {
|
|
56
|
+
const pythonWeekday = (now.getDay() + 6) % 7;
|
|
57
|
+
const todayIso = toIsoDate(now);
|
|
58
|
+
const start = addIsoDays(todayIso, -pythonWeekday);
|
|
59
|
+
const end = addIsoDays(start, 6);
|
|
60
|
+
return { start, end };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Drop tasks that are scheduled in the past and not done, matching Python's
|
|
65
|
+
* `if t.scheduled_at and t.status != "done": ... if sched < now: continue`.
|
|
66
|
+
* An unparseable `scheduled_at` is NOT excluded (Python's `except ValueError:
|
|
67
|
+
* pass` falls through to keep the task) -- reproduced here by only excluding
|
|
68
|
+
* on a successfully-parsed, strictly-earlier instant.
|
|
69
|
+
*/
|
|
70
|
+
export function filterVisibleTasks(tasks: readonly Task[], now: Date): Task[] {
|
|
71
|
+
const visible: Task[] = [];
|
|
72
|
+
for (const t of tasks) {
|
|
73
|
+
if (t.scheduled_at && t.status !== "done") {
|
|
74
|
+
const schedMs = new Date(t.scheduled_at).getTime();
|
|
75
|
+
if (!Number.isNaN(schedMs) && schedMs < now.getTime()) {
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
visible.push(t);
|
|
80
|
+
}
|
|
81
|
+
return visible;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Sort key mirroring Python's `(scheduled_at or "99", time_hint or "zz",
|
|
86
|
+
* title)` tuple: a scheduled task sorts before any unscheduled one (any real
|
|
87
|
+
* ISO datetime string is lexically less than the "99" sentinel), then by
|
|
88
|
+
* time-hint, then alphabetically by title.
|
|
89
|
+
*/
|
|
90
|
+
function compareByDaySortKey(a: Task, b: Task): number {
|
|
91
|
+
const aKey = [a.scheduled_at ?? "99", a.time_hint ?? "zz", a.title];
|
|
92
|
+
const bKey = [b.scheduled_at ?? "99", b.time_hint ?? "zz", b.title];
|
|
93
|
+
for (let i = 0; i < aKey.length; i += 1) {
|
|
94
|
+
if (aKey[i] !== bKey[i]) return aKey[i] < bKey[i] ? -1 : 1;
|
|
95
|
+
}
|
|
96
|
+
return 0;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Group tasks by calendar day (`due_date`, falling back to `scheduled_at`'s
|
|
101
|
+
* date portion), sorting each day's bucket by the (scheduled_at, time_hint,
|
|
102
|
+
* title) key. A task with neither `due_date` nor `scheduled_at` has no day
|
|
103
|
+
* and is dropped -- matching Python's `if day:` guard (in practice this never
|
|
104
|
+
* happens for a task `getTasksForWeek` actually returned, since its WHERE
|
|
105
|
+
* clause already requires one of the two to be in range).
|
|
106
|
+
*/
|
|
107
|
+
export function groupTasksByDay(tasks: readonly Task[]): Map<string, Task[]> {
|
|
108
|
+
const byDay = new Map<string, Task[]>();
|
|
109
|
+
for (const t of tasks) {
|
|
110
|
+
const day = t.due_date || (t.scheduled_at ? t.scheduled_at.slice(0, 10) : null);
|
|
111
|
+
if (!day) continue;
|
|
112
|
+
const bucket = byDay.get(day);
|
|
113
|
+
if (bucket) bucket.push(t);
|
|
114
|
+
else byDay.set(day, [t]);
|
|
115
|
+
}
|
|
116
|
+
for (const bucket of byDay.values()) {
|
|
117
|
+
bucket.sort(compareByDaySortKey);
|
|
118
|
+
}
|
|
119
|
+
return byDay;
|
|
120
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// CR018 — a cut that says so.
|
|
2
|
+
//
|
|
3
|
+
// The card primitives cut silently, as Python's did, and a Navigator reading a title
|
|
4
|
+
// cut mid-word had no way to know it went on. This rule marks the cut with `…`, and
|
|
5
|
+
// backs a cut that would split a word up to the word's start, because a code cut short
|
|
6
|
+
// reads as another code: `(per D12)` must not become `(per D1…`. It began as `card.ts`'s
|
|
7
|
+
// own; CR103 moved it here so a status clause can be bounded by the same rule before it
|
|
8
|
+
// reaches any card.
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* `text` cut to fit `width` code points, with `…` in the last place when anything was
|
|
12
|
+
* cut. A cut that would split a word backs up to the word's start, but only while at
|
|
13
|
+
* least half the width stays; past that, the word is cut. A space the cut leaves at the
|
|
14
|
+
* end goes, so the mark sits on the word.
|
|
15
|
+
*/
|
|
16
|
+
export function clipCodePoints(text: string, width: number): string {
|
|
17
|
+
const points = Array.from(text);
|
|
18
|
+
if (points.length <= width) return text;
|
|
19
|
+
let kept = points.slice(0, width - 1);
|
|
20
|
+
const next = points[width - 1] ?? "";
|
|
21
|
+
if (!/\s/u.test(next)) {
|
|
22
|
+
const wordStart = kept.lastIndexOf(" ");
|
|
23
|
+
if (wordStart >= Math.ceil(width / 2)) kept = kept.slice(0, wordStart);
|
|
24
|
+
}
|
|
25
|
+
return `${kept.join("").trimEnd()}…`;
|
|
26
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// Filesystem path helpers for the front door.
|
|
2
|
+
//
|
|
3
|
+
// normalizeProjectPath is the parity crux of routing `journey set-path`: it must
|
|
4
|
+
// canonicalize a path the way Python's _normalize_project_path does
|
|
5
|
+
// (`Path(value).expanduser().resolve()`).
|
|
6
|
+
|
|
7
|
+
import { accessSync, constants, realpathSync } from "node:fs";
|
|
8
|
+
import { homedir } from "node:os";
|
|
9
|
+
import { delimiter, join, resolve } from "node:path";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Expand a leading `~` or `~/` to the home directory. A `~user` path passes
|
|
13
|
+
* through unchanged (Python's expanduser resolves known users; for the front
|
|
14
|
+
* door's purposes pass-through matches the unknown-user behavior and never
|
|
15
|
+
* fabricates a wrong path — pre-CR007 this mangled `~user/x` into home+`ser/x`).
|
|
16
|
+
*/
|
|
17
|
+
export function expandHome(path: string): string {
|
|
18
|
+
if (path === "~") return homedir();
|
|
19
|
+
if (path.startsWith("~/")) return join(homedir(), path.slice(2));
|
|
20
|
+
return path;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Can this process execute the file at `path`? */
|
|
24
|
+
export function isExecutable(path: string): boolean {
|
|
25
|
+
try {
|
|
26
|
+
accessSync(path, constants.X_OK);
|
|
27
|
+
return true;
|
|
28
|
+
} catch {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Where `command` resolves on `env.PATH`, or null: what a shell's `command -v`
|
|
35
|
+
* answers, for the PATH this process was given. The one rule for "is `mirror`
|
|
36
|
+
* reachable", "which runtimes are installed", and the hook wrappers' Node.
|
|
37
|
+
*/
|
|
38
|
+
export function commandOnPath(
|
|
39
|
+
command: string,
|
|
40
|
+
env: NodeJS.ProcessEnv,
|
|
41
|
+
canExecute: (path: string) => boolean = isExecutable,
|
|
42
|
+
): string | null {
|
|
43
|
+
for (const dir of (env.PATH ?? "").split(delimiter).filter(Boolean)) {
|
|
44
|
+
const candidate = join(dir, command);
|
|
45
|
+
if (canExecute(candidate)) return candidate;
|
|
46
|
+
}
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Canonicalize a project path like Python's `Path(value).expanduser().resolve()`:
|
|
52
|
+
* expand `~`, make it absolute, and resolve symlinks. Node's `realpathSync` throws
|
|
53
|
+
* on a missing path, so fall back to the absolute (non-symlink) path — Python
|
|
54
|
+
* resolves non-strict, but `set-path` targets a real directory in the normal case.
|
|
55
|
+
*/
|
|
56
|
+
export function normalizeProjectPath(value: string): string {
|
|
57
|
+
const absolute = resolve(expandHome(value));
|
|
58
|
+
try {
|
|
59
|
+
return realpathSync(absolute);
|
|
60
|
+
} catch {
|
|
61
|
+
return absolute;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// The name the product gives itself in every command it tells a user to run
|
|
2
|
+
// (CV22.DS10.TS5, decisions D2 and D12).
|
|
3
|
+
//
|
|
4
|
+
// `mirror` is not yet a command on anyone's PATH: the npm `bin` is
|
|
5
|
+
// CV22.DS10.US3's to name. Until TS5 the usage lines and hints named the Python
|
|
6
|
+
// program instead, in both its bare and its `uv`-wrapped form, and that program
|
|
7
|
+
// no longer exists. Every one of them reads this constant now, so when US3
|
|
8
|
+
// names the `bin`, this is the one place that changes.
|
|
9
|
+
export const PROGRAM = "mirror";
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
// Deterministic identifier + timestamp generation matching the Python core.
|
|
2
|
+
//
|
|
3
|
+
// During DS4 parity, `id` and `now` were INJECTED so the TS write matched the
|
|
4
|
+
// oracle. For live front-door writes (US4) the TS core must GENERATE them the way
|
|
5
|
+
// Python does: `_uuid()` = uuid4().hex[:8] (8 lowercase hex chars) and `_now()` =
|
|
6
|
+
// datetime.now(timezone.utc).isoformat().replace("+00:00","Z") (microsecond ISO-Z).
|
|
7
|
+
|
|
8
|
+
import { randomUUID } from "node:crypto";
|
|
9
|
+
import { compareByCodePoint } from "./pythonText.ts";
|
|
10
|
+
|
|
11
|
+
/** Match Python `_uuid()` = uuid4().hex[:8]: 8 lowercase hex chars. */
|
|
12
|
+
/**
|
|
13
|
+
* Serialize like Python `json.dumps(value, ensure_ascii=False)`: item
|
|
14
|
+
* separator `", "` and key separator `": "`. Scoped to JSON-decodable values
|
|
15
|
+
* (the shapes Mirror stores in TEXT metadata columns); byte-parity of stored
|
|
16
|
+
* metadata is part of the DS7 golden contract.
|
|
17
|
+
*/
|
|
18
|
+
export function pythonJsonDumps(value: unknown): string {
|
|
19
|
+
if (Array.isArray(value)) {
|
|
20
|
+
return `[${value.map((item) => pythonJsonDumps(item)).join(", ")}]`;
|
|
21
|
+
}
|
|
22
|
+
if (typeof value === "object" && value !== null) {
|
|
23
|
+
const entries = Object.entries(value as Record<string, unknown>).map(
|
|
24
|
+
([key, item]) => `${JSON.stringify(key)}: ${pythonJsonDumps(item)}`,
|
|
25
|
+
);
|
|
26
|
+
return `{${entries.join(", ")}}`;
|
|
27
|
+
}
|
|
28
|
+
return JSON.stringify(value);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Serialize like Python's DEFAULT `json.dumps(value)` -- same `", "`/`": "`
|
|
33
|
+
* separators as above, but with `ensure_ascii=True`, so non-ASCII characters
|
|
34
|
+
* are escaped as `\uXXXX`.
|
|
35
|
+
*
|
|
36
|
+
* Mirror uses both variants, and which one applies is a per-call-site fact, not
|
|
37
|
+
* a preference: conversation `metadata`/`tags` are written with
|
|
38
|
+
* `ensure_ascii=False` (raw UTF-8), while memory `tags` go through
|
|
39
|
+
* `services/memory.py`'s plain `json.dumps(tags)` and are escaped. Storing
|
|
40
|
+
* `["café"]` where Python stored `["caf\u00e9"]` is a silent byte divergence in
|
|
41
|
+
* a column both cores read.
|
|
42
|
+
*
|
|
43
|
+
* JavaScript strings are UTF-16, so astral characters are already surrogate
|
|
44
|
+
* pairs and escaping each unit reproduces Python's surrogate-pair output.
|
|
45
|
+
*/
|
|
46
|
+
export function pythonJsonDumpsEnsureAscii(value: unknown): string {
|
|
47
|
+
return escapeNonAscii(pythonJsonDumps(value));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Serialize like Python `json.dumps(value, indent=2, sort_keys=True)` -- the
|
|
52
|
+
* shape of a file written to be read back by BOTH cores.
|
|
53
|
+
*
|
|
54
|
+
* `ensure_ascii` is left at Python's default (True), because the call site this
|
|
55
|
+
* mirrors leaves it there too: the welcome's update cache is written with a
|
|
56
|
+
* plain `json.dumps(payload, indent=2, sort_keys=True)`. A release title
|
|
57
|
+
* carrying an em dash must land as `\u2014`, not as raw UTF-8, or the two
|
|
58
|
+
* cores write different bytes for the same state and thrash each other's TTL.
|
|
59
|
+
*/
|
|
60
|
+
export function pythonJsonDumpsIndented(value: unknown, indent = 2): string {
|
|
61
|
+
return escapeNonAscii(JSON.stringify(sortKeysDeep(value), null, indent));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Serialize like Python `json.dumps(value, indent=2)` -- indented and
|
|
66
|
+
* ASCII-escaped, but in INSERTION order.
|
|
67
|
+
*
|
|
68
|
+
* Separate from `pythonJsonDumpsIndented` because that one carries
|
|
69
|
+
* `sort_keys=True`, and this call site does not: the extension runtime catalog
|
|
70
|
+
* is written key by key as `schema_version, runtime, target_root,
|
|
71
|
+
* generated_at, extensions`. Sorting would hoist `extensions` to the top and
|
|
72
|
+
* change every byte of a file both cores read and rewrite.
|
|
73
|
+
*/
|
|
74
|
+
export function pythonJsonDumpsIndentedOrdered(value: unknown, indent = 2): string {
|
|
75
|
+
return escapeNonAscii(JSON.stringify(value, null, indent));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Serialize like Python `json.dumps(value, ensure_ascii=True,
|
|
80
|
+
* separators=(",", ":"), sort_keys=True)` -- the canonical form a HASH is taken
|
|
81
|
+
* over.
|
|
82
|
+
*
|
|
83
|
+
* Separate from `pythonJsonDumpsIndented` because the bytes differ in three ways
|
|
84
|
+
* at once (no indent, no spaces after separators, escaped non-ASCII), and this one
|
|
85
|
+
* feeds `scope_fingerprint`. A fingerprint is an equality test on authority: if the
|
|
86
|
+
* two engines canonicalize differently, a receipt Python recorded cannot be
|
|
87
|
+
* consumed by TypeScript and the Navigator is asked to approve a Plan they already
|
|
88
|
+
* authorized.
|
|
89
|
+
*
|
|
90
|
+
* Keys are ordered by CODE POINT, not by JavaScript's default UTF-16 comparison.
|
|
91
|
+
* The fingerprint payload has fixed ASCII keys so the two orders coincide today;
|
|
92
|
+
* using the correct comparator means a future key cannot silently diverge.
|
|
93
|
+
*/
|
|
94
|
+
export function pythonJsonDumpsCanonical(value: unknown): string {
|
|
95
|
+
return escapeNonAscii(JSON.stringify(sortKeysByCodePoint(value)));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function sortKeysByCodePoint(value: unknown): unknown {
|
|
99
|
+
if (Array.isArray(value)) return value.map(sortKeysByCodePoint);
|
|
100
|
+
if (typeof value !== "object" || value === null) return value;
|
|
101
|
+
const sorted: Record<string, unknown> = {};
|
|
102
|
+
for (const key of Object.keys(value as Record<string, unknown>).sort(compareByCodePoint)) {
|
|
103
|
+
sorted[key] = sortKeysByCodePoint((value as Record<string, unknown>)[key]);
|
|
104
|
+
}
|
|
105
|
+
return sorted;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Python's `sort_keys=True`, applied at every level. */
|
|
109
|
+
function sortKeysDeep(value: unknown): unknown {
|
|
110
|
+
if (Array.isArray(value)) return value.map(sortKeysDeep);
|
|
111
|
+
if (typeof value !== "object" || value === null) return value;
|
|
112
|
+
const sorted: Record<string, unknown> = {};
|
|
113
|
+
for (const key of Object.keys(value as Record<string, unknown>).sort()) {
|
|
114
|
+
sorted[key] = sortKeysDeep((value as Record<string, unknown>)[key]);
|
|
115
|
+
}
|
|
116
|
+
return sorted;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function escapeNonAscii(text: string): string {
|
|
120
|
+
return text.replace(/[\u0080-\uffff]/g, (character) => {
|
|
121
|
+
const code = character.charCodeAt(0).toString(16).padStart(4, "0");
|
|
122
|
+
return `\\u${code}`;
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function newId(): string {
|
|
127
|
+
return randomUUID().replace(/-/g, "").slice(0, 8);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Format a Date the way Python's `_now()` does: an ISO-8601 UTC string with a `Z`
|
|
132
|
+
* suffix and 6-digit microseconds. JS Date resolves only to milliseconds, so the
|
|
133
|
+
* final three digits are zero-padded. This keeps the stored string the same length
|
|
134
|
+
* and lexicographic ordering as Python's timestamps — a millisecond `.123Z` would
|
|
135
|
+
* otherwise sort *after* a microsecond `.123456Z`, corrupting recency ordering.
|
|
136
|
+
*/
|
|
137
|
+
export function toMicrosecondIso(date: Date): string {
|
|
138
|
+
return date.toISOString().replace(/\.(\d{3})Z$/, ".$1000Z");
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Match Python `_now()`: the current UTC time as a microsecond ISO-`Z` string. */
|
|
142
|
+
export function nowIso(): string {
|
|
143
|
+
return toMicrosecondIso(new Date());
|
|
144
|
+
}
|