@runcore-sh/runcore 0.3.1 → 0.4.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/README.md +19 -19
- package/brain-template/agents/README.md +20 -0
- package/brain-template/identity/README.md +20 -0
- package/brain-template/identity/brand.md +25 -0
- package/brain-template/identity/principles.md +21 -0
- package/brain-template/identity/tone-of-voice.md +23 -0
- package/brain-template/knowledge/notes/tier-gated-ui-spec.md +1 -1
- package/brain-template/metrics/README.md +13 -0
- package/brain-template/operations/goals.yaml +12 -0
- package/brain-template/operations/todos.md +17 -0
- package/brain-template/ops/README.md +13 -0
- package/brain-template/scheduling/README.md +2 -2
- package/brain-template/skills/README.md +23 -0
- package/brain-template/templates/README.md +9 -0
- package/brain-template/training/README.md +20 -0
- package/dictionary.json +2 -2
- package/dist/activity/log.d.ts +3 -0
- package/dist/activity/log.d.ts.map +1 -1
- package/dist/activity/log.js +12 -0
- package/dist/activity/log.js.map +1 -1
- package/dist/agents/autonomous.js +1 -0
- package/dist/agents/autonomous.js.map +1 -1
- package/dist/agents/commit.d.ts.map +1 -1
- package/dist/agents/commit.js +3 -10
- package/dist/agents/commit.js.map +1 -1
- package/dist/agents/dedup-guard.d.ts.map +1 -1
- package/dist/agents/dedup-guard.js +26 -23
- package/dist/agents/dedup-guard.js.map +1 -1
- package/dist/agents/feed.d.ts +69 -0
- package/dist/agents/feed.d.ts.map +1 -0
- package/dist/agents/feed.js +176 -0
- package/dist/agents/feed.js.map +1 -0
- package/dist/agents/governance.d.ts +14 -0
- package/dist/agents/governance.d.ts.map +1 -1
- package/dist/agents/governance.js +73 -1
- package/dist/agents/governance.js.map +1 -1
- package/dist/agents/governed-spawn.d.ts +12 -0
- package/dist/agents/governed-spawn.d.ts.map +1 -1
- package/dist/agents/governed-spawn.js +8 -2
- package/dist/agents/governed-spawn.js.map +1 -1
- package/dist/agents/index.d.ts +2 -0
- package/dist/agents/index.d.ts.map +1 -1
- package/dist/agents/index.js +1 -0
- package/dist/agents/index.js.map +1 -1
- package/dist/agents/issue-reporter.d.ts +15 -0
- package/dist/agents/issue-reporter.d.ts.map +1 -0
- package/dist/agents/issue-reporter.js +123 -0
- package/dist/agents/issue-reporter.js.map +1 -0
- package/dist/agents/issues.d.ts +33 -0
- package/dist/agents/issues.d.ts.map +1 -0
- package/dist/agents/issues.js +141 -0
- package/dist/agents/issues.js.map +1 -0
- package/dist/agents/reflection.js +3 -3
- package/dist/agents/reflection.js.map +1 -1
- package/dist/agents/runtime/driver.d.ts.map +1 -1
- package/dist/agents/runtime/driver.js +1 -2
- package/dist/agents/runtime/driver.js.map +1 -1
- package/dist/agents/runtime/manager.d.ts.map +1 -1
- package/dist/agents/runtime/manager.js +1 -0
- package/dist/agents/runtime/manager.js.map +1 -1
- package/dist/agents/runtime/types.d.ts +2 -0
- package/dist/agents/runtime/types.d.ts.map +1 -1
- package/dist/agents/spawn.d.ts.map +1 -1
- package/dist/agents/spawn.js +20 -26
- package/dist/agents/spawn.js.map +1 -1
- package/dist/agents/store.d.ts.map +1 -1
- package/dist/agents/store.js +34 -1
- package/dist/agents/store.js.map +1 -1
- package/dist/agents/types.d.ts +27 -0
- package/dist/agents/types.d.ts.map +1 -1
- package/dist/auth/identity.d.ts +3 -0
- package/dist/auth/identity.d.ts.map +1 -1
- package/dist/auth/identity.js +9 -1
- package/dist/auth/identity.js.map +1 -1
- package/dist/auth/middleware.d.ts.map +1 -1
- package/dist/auth/middleware.js +4 -0
- package/dist/auth/middleware.js.map +1 -1
- package/dist/calibration/conversation.d.ts +46 -0
- package/dist/calibration/conversation.d.ts.map +1 -0
- package/dist/calibration/conversation.js +295 -0
- package/dist/calibration/conversation.js.map +1 -0
- package/dist/calibration/index.d.ts +5 -0
- package/dist/calibration/index.d.ts.map +1 -0
- package/dist/calibration/index.js +5 -0
- package/dist/calibration/index.js.map +1 -0
- package/dist/calibration/runner.d.ts +127 -0
- package/dist/calibration/runner.d.ts.map +1 -0
- package/dist/calibration/runner.js +307 -0
- package/dist/calibration/runner.js.map +1 -0
- package/dist/calibration/store.d.ts +49 -0
- package/dist/calibration/store.d.ts.map +1 -0
- package/dist/calibration/store.js +140 -0
- package/dist/calibration/store.js.map +1 -0
- package/dist/calibration/types.d.ts +93 -0
- package/dist/calibration/types.d.ts.map +1 -0
- package/dist/calibration/types.js +53 -0
- package/dist/calibration/types.js.map +1 -0
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +17 -2
- package/dist/cli.js.map +1 -1
- package/dist/config/index.d.ts +5 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +3 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config/loader.d.ts +28 -0
- package/dist/config/loader.d.ts.map +1 -0
- package/dist/config/loader.js +181 -0
- package/dist/config/loader.js.map +1 -0
- package/dist/config/schema.d.ts +76 -0
- package/dist/config/schema.d.ts.map +1 -0
- package/dist/config/schema.js +93 -0
- package/dist/config/schema.js.map +1 -0
- package/dist/dictionary/challenge.d.ts +66 -0
- package/dist/dictionary/challenge.d.ts.map +1 -0
- package/dist/dictionary/challenge.js +145 -0
- package/dist/dictionary/challenge.js.map +1 -0
- package/dist/dictionary/client.d.ts +32 -0
- package/dist/dictionary/client.d.ts.map +1 -0
- package/dist/dictionary/client.js +139 -0
- package/dist/dictionary/client.js.map +1 -0
- package/dist/dictionary/compatibility.d.ts +8 -0
- package/dist/dictionary/compatibility.d.ts.map +1 -0
- package/dist/dictionary/compatibility.js +56 -0
- package/dist/dictionary/compatibility.js.map +1 -0
- package/dist/dictionary/index.d.ts +22 -0
- package/dist/dictionary/index.d.ts.map +1 -0
- package/dist/dictionary/index.js +15 -0
- package/dist/dictionary/index.js.map +1 -0
- package/dist/dictionary/matcher.d.ts +18 -0
- package/dist/dictionary/matcher.d.ts.map +1 -0
- package/dist/dictionary/matcher.js +98 -0
- package/dist/dictionary/matcher.js.map +1 -0
- package/dist/dictionary/publisher.d.ts +17 -0
- package/dist/dictionary/publisher.d.ts.map +1 -0
- package/dist/dictionary/publisher.js +156 -0
- package/dist/dictionary/publisher.js.map +1 -0
- package/dist/dictionary/sync.d.ts +28 -0
- package/dist/dictionary/sync.d.ts.map +1 -0
- package/dist/dictionary/sync.js +268 -0
- package/dist/dictionary/sync.js.map +1 -0
- package/dist/dictionary/types.d.ts +75 -0
- package/dist/dictionary/types.d.ts.map +1 -0
- package/dist/dictionary/types.js +8 -0
- package/dist/dictionary/types.js.map +1 -0
- package/dist/dictionary/updater.d.ts +23 -0
- package/dist/dictionary/updater.d.ts.map +1 -0
- package/dist/dictionary/updater.js +84 -0
- package/dist/dictionary/updater.js.map +1 -0
- package/dist/dictionary/versioning.d.ts +15 -0
- package/dist/dictionary/versioning.d.ts.map +1 -0
- package/dist/dictionary/versioning.js +52 -0
- package/dist/dictionary/versioning.js.map +1 -0
- package/dist/errors.d.ts +36 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +66 -0
- package/dist/errors.js.map +1 -0
- package/dist/events/event-bus.d.ts +37 -0
- package/dist/events/event-bus.d.ts.map +1 -0
- package/dist/events/event-bus.js +261 -0
- package/dist/events/event-bus.js.map +1 -0
- package/dist/events/index.d.ts +3 -0
- package/dist/events/index.d.ts.map +1 -0
- package/dist/events/index.js +2 -0
- package/dist/events/index.js.map +1 -0
- package/dist/events/types.d.ts +71 -0
- package/dist/events/types.d.ts.map +1 -0
- package/dist/events/types.js +7 -0
- package/dist/events/types.js.map +1 -0
- package/dist/feed/client.d.ts +34 -0
- package/dist/feed/client.d.ts.map +1 -0
- package/dist/feed/client.js +206 -0
- package/dist/feed/client.js.map +1 -0
- package/dist/feed/index.d.ts +61 -0
- package/dist/feed/index.d.ts.map +1 -0
- package/dist/feed/index.js +115 -0
- package/dist/feed/index.js.map +1 -0
- package/dist/feed/metrics.d.ts +51 -0
- package/dist/feed/metrics.d.ts.map +1 -0
- package/dist/feed/metrics.js +84 -0
- package/dist/feed/metrics.js.map +1 -0
- package/dist/feed/mixer.d.ts +89 -0
- package/dist/feed/mixer.d.ts.map +1 -0
- package/dist/feed/mixer.js +230 -0
- package/dist/feed/mixer.js.map +1 -0
- package/dist/feed/tiers.d.ts +15 -0
- package/dist/feed/tiers.d.ts.map +1 -0
- package/dist/feed/tiers.js +75 -0
- package/dist/feed/tiers.js.map +1 -0
- package/dist/feed/types.d.ts +76 -0
- package/dist/feed/types.d.ts.map +1 -0
- package/dist/feed/types.js +6 -0
- package/dist/feed/types.js.map +1 -0
- package/dist/files/registry.d.ts +77 -0
- package/dist/files/registry.d.ts.map +1 -0
- package/dist/files/registry.js +222 -0
- package/dist/files/registry.js.map +1 -0
- package/dist/google/plugin.d.ts +17 -0
- package/dist/google/plugin.d.ts.map +1 -0
- package/dist/google/plugin.js +169 -0
- package/dist/google/plugin.js.map +1 -0
- package/dist/health/checker.d.ts.map +1 -1
- package/dist/health/checker.js +14 -4
- package/dist/health/checker.js.map +1 -1
- package/dist/health/checks/openrouter.js +1 -1
- package/dist/health/checks/openrouter.js.map +1 -1
- package/dist/health/checks.d.ts +1 -1
- package/dist/health/checks.d.ts.map +1 -1
- package/dist/health/checks.js +9 -3
- package/dist/health/checks.js.map +1 -1
- package/dist/ledger/distance.d.ts +19 -0
- package/dist/ledger/distance.d.ts.map +1 -0
- package/dist/ledger/distance.js +70 -0
- package/dist/ledger/distance.js.map +1 -0
- package/dist/ledger/index.d.ts +8 -0
- package/dist/ledger/index.d.ts.map +1 -0
- package/dist/ledger/index.js +7 -0
- package/dist/ledger/index.js.map +1 -0
- package/dist/ledger/store.d.ts +27 -0
- package/dist/ledger/store.d.ts.map +1 -0
- package/dist/ledger/store.js +123 -0
- package/dist/ledger/store.js.map +1 -0
- package/dist/ledger/types.d.ts +109 -0
- package/dist/ledger/types.d.ts.map +1 -0
- package/dist/ledger/types.js +57 -0
- package/dist/ledger/types.js.map +1 -0
- package/dist/literacy.d.ts +50 -0
- package/dist/literacy.d.ts.map +1 -0
- package/dist/literacy.js +141 -0
- package/dist/literacy.js.map +1 -0
- package/dist/llm/complete.d.ts.map +1 -1
- package/dist/llm/complete.js +36 -3
- package/dist/llm/complete.js.map +1 -1
- package/dist/llm/fetch-guard.d.ts.map +1 -1
- package/dist/llm/fetch-guard.js +2 -0
- package/dist/llm/fetch-guard.js.map +1 -1
- package/dist/llm/membrane.d.ts +5 -0
- package/dist/llm/membrane.d.ts.map +1 -1
- package/dist/llm/membrane.js +48 -8
- package/dist/llm/membrane.js.map +1 -1
- package/dist/llm/nlp-detect.d.ts +17 -0
- package/dist/llm/nlp-detect.d.ts.map +1 -0
- package/dist/llm/nlp-detect.js +45 -0
- package/dist/llm/nlp-detect.js.map +1 -0
- package/dist/llm/ollama.d.ts +5 -0
- package/dist/llm/ollama.d.ts.map +1 -1
- package/dist/llm/ollama.js +39 -1
- package/dist/llm/ollama.js.map +1 -1
- package/dist/llm/openrouter.d.ts.map +1 -1
- package/dist/llm/openrouter.js +17 -1
- package/dist/llm/openrouter.js.map +1 -1
- package/dist/llm/providers/ollama.d.ts +7 -2
- package/dist/llm/providers/ollama.d.ts.map +1 -1
- package/dist/llm/providers/ollama.js +109 -17
- package/dist/llm/providers/ollama.js.map +1 -1
- package/dist/llm/providers/openrouter.js +1 -1
- package/dist/llm/providers/openrouter.js.map +1 -1
- package/dist/llm/sensitive-registry.d.ts +6 -0
- package/dist/llm/sensitive-registry.d.ts.map +1 -1
- package/dist/llm/sensitive-registry.js +60 -1
- package/dist/llm/sensitive-registry.js.map +1 -1
- package/dist/mcp-server.js +25 -18
- package/dist/mcp-server.js.map +1 -1
- package/dist/metrics/collector.d.ts +6 -0
- package/dist/metrics/collector.d.ts.map +1 -1
- package/dist/metrics/collector.js +150 -0
- package/dist/metrics/collector.js.map +1 -1
- package/dist/metrics/types.d.ts +1 -1
- package/dist/metrics/types.d.ts.map +1 -1
- package/dist/middleware/error-handler.d.ts +13 -0
- package/dist/middleware/error-handler.d.ts.map +1 -0
- package/dist/middleware/error-handler.js +71 -0
- package/dist/middleware/error-handler.js.map +1 -0
- package/dist/notifications/channels/adapter.d.ts +28 -0
- package/dist/notifications/channels/adapter.d.ts.map +1 -0
- package/dist/notifications/channels/adapter.js +55 -0
- package/dist/notifications/channels/adapter.js.map +1 -0
- package/dist/notifications/channels/index.d.ts +6 -0
- package/dist/notifications/channels/index.d.ts.map +1 -0
- package/dist/notifications/channels/index.js +6 -0
- package/dist/notifications/channels/index.js.map +1 -0
- package/dist/notifications/channels/log.d.ts +15 -0
- package/dist/notifications/channels/log.d.ts.map +1 -0
- package/dist/notifications/channels/log.js +29 -0
- package/dist/notifications/channels/log.js.map +1 -0
- package/dist/notifications/engine.d.ts +37 -0
- package/dist/notifications/engine.d.ts.map +1 -0
- package/dist/notifications/engine.js +198 -0
- package/dist/notifications/engine.js.map +1 -0
- package/dist/notifications/index.d.ts +12 -3
- package/dist/notifications/index.d.ts.map +1 -1
- package/dist/notifications/index.js +15 -3
- package/dist/notifications/index.js.map +1 -1
- package/dist/notifications/types.d.ts +97 -0
- package/dist/notifications/types.d.ts.map +1 -0
- package/dist/notifications/types.js +14 -0
- package/dist/notifications/types.js.map +1 -0
- package/dist/onboarding/bootstrap.d.ts +51 -0
- package/dist/onboarding/bootstrap.d.ts.map +1 -0
- package/dist/onboarding/bootstrap.js +92 -0
- package/dist/onboarding/bootstrap.js.map +1 -0
- package/dist/onboarding/conversation.d.ts +131 -0
- package/dist/onboarding/conversation.d.ts.map +1 -0
- package/dist/onboarding/conversation.js +259 -0
- package/dist/onboarding/conversation.js.map +1 -0
- package/dist/onboarding/flow.d.ts +63 -0
- package/dist/onboarding/flow.d.ts.map +1 -0
- package/dist/onboarding/flow.js +287 -0
- package/dist/onboarding/flow.js.map +1 -0
- package/dist/onboarding/index.d.ts +17 -0
- package/dist/onboarding/index.d.ts.map +1 -0
- package/dist/onboarding/index.js +23 -0
- package/dist/onboarding/index.js.map +1 -0
- package/dist/onboarding/name-extraction.d.ts +42 -0
- package/dist/onboarding/name-extraction.d.ts.map +1 -0
- package/dist/onboarding/name-extraction.js +164 -0
- package/dist/onboarding/name-extraction.js.map +1 -0
- package/dist/onboarding/nerve-link.d.ts +23 -0
- package/dist/onboarding/nerve-link.d.ts.map +1 -0
- package/dist/onboarding/nerve-link.js +24 -0
- package/dist/onboarding/nerve-link.js.map +1 -0
- package/dist/onboarding/phases.d.ts +66 -0
- package/dist/onboarding/phases.d.ts.map +1 -0
- package/dist/onboarding/phases.js +167 -0
- package/dist/onboarding/phases.js.map +1 -0
- package/dist/onboarding/safe-word.d.ts +39 -0
- package/dist/onboarding/safe-word.d.ts.map +1 -0
- package/dist/onboarding/safe-word.js +90 -0
- package/dist/onboarding/safe-word.js.map +1 -0
- package/dist/onboarding/types.d.ts +124 -0
- package/dist/onboarding/types.d.ts.map +1 -0
- package/dist/onboarding/types.js +46 -0
- package/dist/onboarding/types.js.map +1 -0
- package/dist/openloop/resolution-scanner.d.ts +1 -1
- package/dist/openloop/resolution-scanner.d.ts.map +1 -1
- package/dist/openloop/resolution-scanner.js +4 -14
- package/dist/openloop/resolution-scanner.js.map +1 -1
- package/dist/plugins/index.d.ts +24 -0
- package/dist/plugins/index.d.ts.map +1 -0
- package/dist/plugins/index.js +91 -0
- package/dist/plugins/index.js.map +1 -0
- package/dist/plugins/status.d.ts +10 -0
- package/dist/plugins/status.d.ts.map +1 -0
- package/dist/plugins/status.js +12 -0
- package/dist/plugins/status.js.map +1 -0
- package/dist/posture/index.d.ts +2 -2
- package/dist/posture/index.d.ts.map +1 -1
- package/dist/posture/index.js +1 -1
- package/dist/posture/index.js.map +1 -1
- package/dist/posture/types.d.ts +34 -0
- package/dist/posture/types.d.ts.map +1 -1
- package/dist/posture/types.js +28 -0
- package/dist/posture/types.js.map +1 -1
- package/dist/pulse/activation-event.d.ts +5 -4
- package/dist/pulse/activation-event.d.ts.map +1 -1
- package/dist/pulse/activation-event.js +31 -8
- package/dist/pulse/activation-event.js.map +1 -1
- package/dist/pulse/activation-log.d.ts.map +1 -1
- package/dist/pulse/activation-log.js.map +1 -1
- package/dist/pulse/index.d.ts +3 -0
- package/dist/pulse/index.d.ts.map +1 -1
- package/dist/pulse/index.js +4 -0
- package/dist/pulse/index.js.map +1 -1
- package/dist/pulse/tier.d.ts +67 -0
- package/dist/pulse/tier.d.ts.map +1 -0
- package/dist/pulse/tier.js +104 -0
- package/dist/pulse/tier.js.map +1 -0
- package/dist/pulse/work.d.ts +66 -0
- package/dist/pulse/work.d.ts.map +1 -0
- package/dist/pulse/work.js +117 -0
- package/dist/pulse/work.js.map +1 -0
- package/dist/runtime-lock.d.ts +51 -0
- package/dist/runtime-lock.d.ts.map +1 -0
- package/dist/runtime-lock.js +147 -0
- package/dist/runtime-lock.js.map +1 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +1244 -188
- package/dist/server.js.map +1 -1
- package/dist/services/whatsapp.js +1 -1
- package/dist/services/whatsapp.js.map +1 -1
- package/dist/settings.d.ts +15 -0
- package/dist/settings.d.ts.map +1 -1
- package/dist/settings.js +48 -1
- package/dist/settings.js.map +1 -1
- package/dist/skills/index.d.ts +2 -2
- package/dist/skills/index.d.ts.map +1 -1
- package/dist/skills/index.js +2 -2
- package/dist/skills/index.js.map +1 -1
- package/dist/skills/registry.d.ts +53 -142
- package/dist/skills/registry.d.ts.map +1 -1
- package/dist/skills/registry.js +249 -611
- package/dist/skills/registry.js.map +1 -1
- package/dist/stream/emitter.d.ts +36 -0
- package/dist/stream/emitter.d.ts.map +1 -0
- package/dist/stream/emitter.js +177 -0
- package/dist/stream/emitter.js.map +1 -0
- package/dist/stream/index.d.ts +4 -0
- package/dist/stream/index.d.ts.map +1 -0
- package/dist/stream/index.js +3 -0
- package/dist/stream/index.js.map +1 -0
- package/dist/stream/types.d.ts +100 -0
- package/dist/stream/types.d.ts.map +1 -0
- package/dist/stream/types.js +19 -0
- package/dist/stream/types.js.map +1 -0
- package/dist/threads/index.d.ts +3 -0
- package/dist/threads/index.d.ts.map +1 -0
- package/dist/threads/index.js +2 -0
- package/dist/threads/index.js.map +1 -0
- package/dist/threads/store.d.ts +36 -0
- package/dist/threads/store.d.ts.map +1 -0
- package/dist/threads/store.js +171 -0
- package/dist/threads/store.js.map +1 -0
- package/dist/threads/types.d.ts +16 -0
- package/dist/threads/types.d.ts.map +1 -0
- package/dist/threads/types.js +6 -0
- package/dist/threads/types.js.map +1 -0
- package/dist/tick/index.d.ts +10 -0
- package/dist/tick/index.d.ts.map +1 -0
- package/dist/tick/index.js +8 -0
- package/dist/tick/index.js.map +1 -0
- package/dist/tick/runner.d.ts +56 -0
- package/dist/tick/runner.d.ts.map +1 -0
- package/dist/tick/runner.js +235 -0
- package/dist/tick/runner.js.map +1 -0
- package/dist/tick/types.d.ts +73 -0
- package/dist/tick/types.d.ts.map +1 -0
- package/dist/tick/types.js +8 -0
- package/dist/tick/types.js.map +1 -0
- package/dist/tier/types.js +3 -3
- package/dist/tier/types.js.map +1 -1
- package/dist/ui-sync.d.ts +34 -0
- package/dist/ui-sync.d.ts.map +1 -0
- package/dist/ui-sync.js +108 -0
- package/dist/ui-sync.js.map +1 -0
- package/dist/utils/git.d.ts +12 -0
- package/dist/utils/git.d.ts.map +1 -0
- package/dist/utils/git.js +44 -0
- package/dist/utils/git.js.map +1 -0
- package/dist/volumes/index.d.ts +3 -0
- package/dist/volumes/index.d.ts.map +1 -0
- package/dist/volumes/index.js +2 -0
- package/dist/volumes/index.js.map +1 -0
- package/dist/volumes/manager.d.ts +83 -0
- package/dist/volumes/manager.d.ts.map +1 -0
- package/dist/volumes/manager.js +462 -0
- package/dist/volumes/manager.js.map +1 -0
- package/dist/volumes/types.d.ts +66 -0
- package/dist/volumes/types.d.ts.map +1 -0
- package/dist/volumes/types.js +8 -0
- package/dist/volumes/types.js.map +1 -0
- package/package.json +8 -5
- package/public/avatar/Hey-Dash_en_windows_v4_0_0.zip +0 -0
- package/public/avatar/README.md +43 -0
- package/public/avatar/cache/06fa55aececcc478.mp4 +0 -0
- package/public/avatar/cache/07a65738ba170827.mp4 +0 -0
- package/public/avatar/cache/08b6f4880f59a385.mp4 +0 -0
- package/public/avatar/cache/0ef9e0e78d715af4.mp4 +0 -0
- package/public/avatar/cache/0fa85e9e8f444a8b.mp4 +0 -0
- package/public/avatar/cache/1185fd491f413406.mp4 +0 -0
- package/public/avatar/cache/1b374d5390258fea.mp4 +0 -0
- package/public/avatar/cache/1e2367029b92f8aa.mp4 +0 -0
- package/public/avatar/cache/272c004a41087de5.mp4 +0 -0
- package/public/avatar/cache/2a0f3ff34d92521a.mp4 +0 -0
- package/public/avatar/cache/307a6f70859aeab8.mp4 +0 -0
- package/public/avatar/cache/332384e088ca214b.mp4 +0 -0
- package/public/avatar/cache/39fc4e81574d14ed.mp4 +0 -0
- package/public/avatar/cache/4a5c6051c1ef6a71.mp4 +0 -0
- package/public/avatar/cache/51f4aa76398c8c29.mp4 +0 -0
- package/public/avatar/cache/5d9a960bbf71732c.mp4 +0 -0
- package/public/avatar/cache/5e0954401e15af89.mp4 +0 -0
- package/public/avatar/cache/884ae6717fcacdd5.mp4 +0 -0
- package/public/avatar/cache/8ea0b7220d139615.mp4 +0 -0
- package/public/avatar/cache/9b9c4f7b8508eecc.mp4 +0 -0
- package/public/avatar/cache/9be1030ec2aa2b01.mp4 +0 -0
- package/public/avatar/cache/b35f7a3d558f22cb.mp4 +0 -0
- package/public/avatar/cache/be89f49970672374.mp4 +0 -0
- package/public/avatar/cache/c11fdc99479492b6.mp4 +0 -0
- package/public/avatar/cache/c900811e3382ac6d.mp4 +0 -0
- package/public/avatar/cache/d42a73667acf5716.mp4 +0 -0
- package/public/avatar/cache/e539f247a8908603.mp4 +0 -0
- package/public/avatar/cache/e78fceae2373b7c1.mp4 +0 -0
- package/public/avatar/cache/ec95af57d33b3f07.mp4 +0 -0
- package/public/avatar/cache/eeb8d775f40dbe2c.mp4 +0 -0
- package/public/avatar/dash_headhshot_v1.png +0 -0
- package/public/avatar/idle.mp4 +0 -0
- package/public/avatar/photo.png +0 -0
- package/public/board.html +6 -0
- package/public/browser.html +6 -2
- package/public/demo-data/Family Photos/2024/christmas-dinner.txt +13 -0
- package/public/demo-data/Family Photos/2025/summer-cookout.txt +13 -0
- package/public/demo-data/Financial/Insurance/auto-policy.txt +26 -0
- package/public/demo-data/Financial/Insurance/homeowners-policy.txt +20 -0
- package/public/demo-data/Financial/Taxes/property-tax-2025.txt +18 -0
- package/public/demo-data/Financial/Taxes/w2-2025.txt +18 -0
- package/public/demo-data/Health Records/lab-results-2026.txt +26 -0
- package/public/demo-data/Health Records/prescription-list.txt +24 -0
- package/public/demo-data/Health Records/vaccination-record.csv +8 -0
- package/public/demo-data/Legal/Estate/beneficiary-contacts.csv +4 -0
- package/public/demo-data/Legal/Estate/will-summary.txt +22 -0
- package/public/demo-data/Recipes/christmas-cookies.md +25 -0
- package/public/demo-data/Recipes/grandmas-chili.md +30 -0
- package/public/demo-data/Work/Contracts/lawn-service-2026.txt +23 -0
- package/public/demo-data/Work/Projects/project-status.md +19 -0
- package/public/demo-data/passwords.txt +13 -0
- package/public/demo-ingest.html +388 -0
- package/public/help.html +4 -1
- package/public/icon-192.png +0 -0
- package/public/icon-512.png +0 -0
- package/public/index.html +2641 -574
- package/public/library.html +51 -29
- package/public/manifest.json +21 -0
- package/public/nerve/icon-192.svg +6 -0
- package/public/nerve/icon-512.svg +6 -0
- package/public/nerve/index.html +698 -0
- package/public/nerve/manifest.json +24 -0
- package/public/nerve/sw.js +84 -0
- package/public/observatory.html +5 -1
- package/public/ops.html +33 -3
- package/public/pulse.html +3 -0
- package/public/registry.html +6 -2
- package/public/roadmap.html +7 -2
- package/public/sw.js +65 -0
- package/brain-template/registry.md +0 -566
- package/brain-template/rest_api-integration.md +0 -522
package/dist/server.js
CHANGED
|
@@ -9,12 +9,17 @@ import { serveStatic } from "@hono/node-server/serve-static";
|
|
|
9
9
|
import { join, dirname } from "node:path";
|
|
10
10
|
import { fileURLToPath } from "node:url";
|
|
11
11
|
import { readFile, writeFile, mkdir } from "node:fs/promises";
|
|
12
|
-
|
|
12
|
+
import { acquireLock, releaseLock } from "./runtime-lock.js";
|
|
13
|
+
// Package root — works whether run from CWD or npx
|
|
13
14
|
const __filename = fileURLToPath(import.meta.url);
|
|
14
15
|
const __dirname = dirname(__filename);
|
|
15
|
-
const PKG_ROOT = join(__dirname, "..");
|
|
16
|
+
const PKG_ROOT = __dirname.endsWith("dist") ? join(__dirname, "..") : __dirname;
|
|
17
|
+
// UI directory — resolved at startup. Prefers CDN-synced, falls back to bundled.
|
|
18
|
+
let UI_DIR = getUiPublicDir(PKG_ROOT);
|
|
16
19
|
import { writeFileSync } from "node:fs";
|
|
20
|
+
import { appendFile } from "node:fs/promises";
|
|
17
21
|
import { initInstanceName, getInstanceName, setInstanceName, getInstanceNameLower, resolveEnv, getAlertEmailFrom } from "./instance.js";
|
|
22
|
+
import { syncUi, getUiPublicDir } from "./ui-sync.js";
|
|
18
23
|
import { readBrainFile, writeBrainFile, appendBrainLine } from "./lib/brain-io.js";
|
|
19
24
|
import { runWithAuditContext } from "./lib/audit.js";
|
|
20
25
|
import { Brain } from "./brain.js";
|
|
@@ -22,7 +27,7 @@ import { FileSystemLongTermMemory } from "./memory/file-backed.js";
|
|
|
22
27
|
import { createLogger } from "./utils/logger.js";
|
|
23
28
|
const log = createLogger("server");
|
|
24
29
|
const agentLog = createLogger("agent");
|
|
25
|
-
import { ensurePairingCode, getStatus, pair, authenticate, getRecoveryQuestion, recover, validateSession, readHuman, restoreSession, cacheSessionKey, } from "./auth/identity.js";
|
|
30
|
+
import { ensurePairingCode, getStatus, pair, authenticate, getRecoveryQuestion, recover, validateSession, readHuman, restoreSession, cacheSessionKey, createSession, } from "./auth/identity.js";
|
|
26
31
|
import { requireSession } from "./auth/middleware.js";
|
|
27
32
|
import { getProvider } from "./llm/providers/index.js";
|
|
28
33
|
import { withStreamRetry } from "./llm/retry.js";
|
|
@@ -31,7 +36,8 @@ import { PrivateModeError, isPrivateMode, checkOllamaHealth } from "./llm/guard.
|
|
|
31
36
|
import { installFetchGuard } from "./llm/fetch-guard.js";
|
|
32
37
|
import { SensitiveRegistry } from "./llm/sensitive-registry.js";
|
|
33
38
|
import { PrivacyMembrane } from "./llm/membrane.js";
|
|
34
|
-
import { setActiveMembrane, rehydrateResponse } from "./llm/redact.js";
|
|
39
|
+
import { setActiveMembrane, getActiveMembrane, rehydrateResponse } from "./llm/redact.js";
|
|
40
|
+
import { VolumeManager } from "./volumes/index.js";
|
|
35
41
|
import { loadSettings, getSettings, updateSettings, resolveProvider, resolveChatModel, resolveUtilityModel, getPulseSettings, getMeshConfig, } from "./settings.js";
|
|
36
42
|
import { startMdns, stopMdns } from "./mdns.js";
|
|
37
43
|
import { ingestDirectory } from "./files/ingest.js";
|
|
@@ -93,7 +99,7 @@ import { tracingMiddleware } from "./tracing/middleware.js";
|
|
|
93
99
|
import { attachOTelToBus } from "./tracing/bridge.js";
|
|
94
100
|
import { HealthChecker, memoryCheck, eventLoopCheck, availabilityCheck, cpuCheck, diskUsageCheck, diskCheck, queueStoreCheck, agentCapacityCheck, agentHealthCheck, boardCheck, RecoveryManager, sidecarRecovery, AlertManager, defaultAlertConfig, } from "./health/index.js";
|
|
95
101
|
import { NotificationDispatcher, EmailChannel, PhoneChannel } from "./notifications/index.js";
|
|
96
|
-
import {
|
|
102
|
+
import { skillRegistry as _skillRegistry } from "./skills/index.js";
|
|
97
103
|
import { createModuleRegistry, getModuleRegistry } from "./modules/index.js";
|
|
98
104
|
import { createCapabilityRegistry, getCapabilityRegistry, calendarCapability, emailCapability, docsCapability, boardCapability, browserCapability, closeBrowser, taskDoneCapability, calendarContextProvider, emailContextProvider, createWebSearchContextProvider, vaultContextProvider } from "./capabilities/index.js";
|
|
99
105
|
import { MetricsStore, startCollector, stopCollector, registerDefaultThresholds, evaluateAlerts, buildDashboard, metricsMiddleware, collectPrometheus, generatePeriodStats, generateComparisonReport, } from "./metrics/index.js";
|
|
@@ -133,7 +139,10 @@ function pickStreamFn() {
|
|
|
133
139
|
return withStreamRetry((options) => provider.streamChat(options), { maxRetries: 3, baseDelayMs: 1_000, maxDelayMs: 30_000 });
|
|
134
140
|
}
|
|
135
141
|
// --- Config ---
|
|
136
|
-
|
|
142
|
+
import { getLastPort } from "./runtime-lock.js";
|
|
143
|
+
const _envPort = parseInt(process.env.CORE_PORT ?? resolveEnv("PORT") ?? "0", 10);
|
|
144
|
+
// Sticky port: if no explicit port set, reuse the last known port from runtime lock
|
|
145
|
+
const PORT = _envPort === 0 ? getLastPort() : _envPort;
|
|
137
146
|
let actualPort = PORT;
|
|
138
147
|
const SIDECAR_PORT = resolveEnv("SEARCH_PORT") ?? "3578";
|
|
139
148
|
import { BRAIN_DIR } from "./lib/paths.js";
|
|
@@ -156,9 +165,25 @@ const alertManager = new AlertManager(health, defaultAlertConfig(), alertDispatc
|
|
|
156
165
|
// --- Metrics ---
|
|
157
166
|
const metricsStore = new MetricsStore(BRAIN_DIR);
|
|
158
167
|
registerDefaultThresholds();
|
|
168
|
+
// --- Volume manager ---
|
|
169
|
+
const volumeManager = new VolumeManager(BRAIN_DIR);
|
|
170
|
+
volumeManager.init().catch((err) => log.warn("Volume manager init failed — single-volume mode", { error: String(err) }));
|
|
159
171
|
const chatSessions = new Map();
|
|
160
172
|
const sessionKeys = new Map();
|
|
161
173
|
let goalTimerStarted = false;
|
|
174
|
+
/** Threads per session. Key = sessionId, value = Map<threadId, ChatThread>. */
|
|
175
|
+
const sessionThreads = new Map();
|
|
176
|
+
function getThreadsForSession(sessionId) {
|
|
177
|
+
let threads = sessionThreads.get(sessionId);
|
|
178
|
+
if (!threads) {
|
|
179
|
+
threads = new Map();
|
|
180
|
+
sessionThreads.set(sessionId, threads);
|
|
181
|
+
}
|
|
182
|
+
return threads;
|
|
183
|
+
}
|
|
184
|
+
function generateThreadId() {
|
|
185
|
+
return Date.now().toString(36) + Math.random().toString(36).slice(2, 8);
|
|
186
|
+
}
|
|
162
187
|
/** One-time startup token for zero-friction local auth. */
|
|
163
188
|
let startupToken = null;
|
|
164
189
|
export function getStartupToken() {
|
|
@@ -170,6 +195,7 @@ const tracer = new Tracer();
|
|
|
170
195
|
let instanceManager = null;
|
|
171
196
|
let agentPool = null;
|
|
172
197
|
let workflowEngine = null;
|
|
198
|
+
let activeSensitiveRegistry = null;
|
|
173
199
|
/** Get the current instance manager (or null if not initialized). */
|
|
174
200
|
function getInstanceManager() {
|
|
175
201
|
return instanceManager;
|
|
@@ -186,6 +212,13 @@ async function getOrCreateChatSession(sessionId, name) {
|
|
|
186
212
|
let cs = chatSessions.get(sessionId);
|
|
187
213
|
if (cs)
|
|
188
214
|
return cs;
|
|
215
|
+
// Single-user system: reuse existing chat session from any prior session ID.
|
|
216
|
+
// This ensures all tabs/devices see the same conversation history.
|
|
217
|
+
if (chatSessions.size > 0) {
|
|
218
|
+
const [existingId, existingCs] = chatSessions.entries().next().value;
|
|
219
|
+
chatSessions.set(sessionId, existingCs);
|
|
220
|
+
return existingCs;
|
|
221
|
+
}
|
|
189
222
|
// Read custom personality instructions (empty string if file doesn't exist)
|
|
190
223
|
let personality = "";
|
|
191
224
|
try {
|
|
@@ -207,18 +240,74 @@ async function getOrCreateChatSession(sessionId, name) {
|
|
|
207
240
|
await ltm.init();
|
|
208
241
|
const brain = new Brain({
|
|
209
242
|
systemPrompt: [
|
|
210
|
-
`
|
|
243
|
+
`IDENTITY:`,
|
|
244
|
+
`- Your name is ${getInstanceName()}.`,
|
|
245
|
+
`- The human you are talking to is named ${name}. When they say "my name" they mean "${name}".`,
|
|
246
|
+
`- You are ${name}'s personal AI agent, running locally on their machine. This conversation is private.`,
|
|
247
|
+
`- Today is ${new Date().toLocaleDateString("en-US", { weekday: "long", year: "numeric", month: "long", day: "numeric" })}. Current tier: ${activeTier}.`,
|
|
211
248
|
``,
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
249
|
+
// Tier-aware capability boundaries
|
|
250
|
+
...(() => {
|
|
251
|
+
const caps = TIER_CAPS[activeTier];
|
|
252
|
+
const can = [];
|
|
253
|
+
const cannot = [];
|
|
254
|
+
// Always available
|
|
255
|
+
can.push("chat and answer questions");
|
|
256
|
+
can.push("remember things and learn from conversations");
|
|
257
|
+
if (caps.ollama)
|
|
258
|
+
can.push("use local AI models via Ollama");
|
|
259
|
+
// Gated capabilities — be explicit about what's off
|
|
260
|
+
if (caps.integrations) {
|
|
261
|
+
can.push("connect to external services (Google, Slack, etc.)");
|
|
262
|
+
}
|
|
263
|
+
else {
|
|
264
|
+
cannot.push("connect to external services (Google, Slack, email)");
|
|
265
|
+
}
|
|
266
|
+
if (caps.vault) {
|
|
267
|
+
can.push("manage API keys and credentials");
|
|
268
|
+
}
|
|
269
|
+
else {
|
|
270
|
+
cannot.push("manage API keys or a credential vault");
|
|
271
|
+
}
|
|
272
|
+
if (caps.spawning) {
|
|
273
|
+
can.push("spawn sub-agents to edit code and run tasks");
|
|
274
|
+
}
|
|
275
|
+
else {
|
|
276
|
+
cannot.push("spawn agents, edit code, or run shell commands");
|
|
277
|
+
}
|
|
278
|
+
if (caps.voice) {
|
|
279
|
+
can.push("speak and listen (voice I/O)");
|
|
280
|
+
}
|
|
281
|
+
else {
|
|
282
|
+
cannot.push("use voice input or output");
|
|
283
|
+
}
|
|
284
|
+
if (caps.mesh) {
|
|
285
|
+
can.push("communicate with other instances on the network");
|
|
286
|
+
}
|
|
287
|
+
else {
|
|
288
|
+
cannot.push("reach other instances or the network");
|
|
289
|
+
}
|
|
290
|
+
if (caps.alerting) {
|
|
291
|
+
can.push("send alerts via SMS, email, or webhooks");
|
|
292
|
+
}
|
|
293
|
+
else {
|
|
294
|
+
cannot.push("send SMS, email, or webhook alerts");
|
|
295
|
+
}
|
|
296
|
+
const lines = [`CAPABILITIES (tier: ${activeTier}):`];
|
|
297
|
+
lines.push(`You CAN: ${can.join("; ")}.`);
|
|
298
|
+
if (cannot.length > 0) {
|
|
299
|
+
lines.push(`You CANNOT: ${cannot.join("; ")}.`);
|
|
300
|
+
lines.push(`Do NOT offer, suggest, or pretend to do things you cannot. If ${name} asks for something outside your capabilities, explain what tier unlocks it and how to upgrade (Settings → API Keys, or run \`runcore register\`).`);
|
|
301
|
+
}
|
|
302
|
+
return lines;
|
|
303
|
+
})(),
|
|
220
304
|
``,
|
|
221
|
-
`
|
|
305
|
+
`RULES:`,
|
|
306
|
+
`- Be warm, honest, and direct. Have personality. Don't be a corporate assistant.`,
|
|
307
|
+
`- If you don't know something, say so. Never invent information.`,
|
|
308
|
+
`- Only reference data that appears in the context below. If nothing is provided, you know nothing yet.`,
|
|
309
|
+
`- NEVER reference board items, tasks, or project work unless they appear verbatim below.`,
|
|
310
|
+
`- NEVER claim you searched the web unless search results appear in your context.`,
|
|
222
311
|
...(personality ? [``, `--- Custom personality ---`, personality, `--- End custom personality ---`] : []),
|
|
223
312
|
isSearchAvailable()
|
|
224
313
|
? `You have web search capability. When search results appear in your context, use them to answer. You don't control when searches happen — the system handles that automatically.`
|
|
@@ -257,32 +346,35 @@ async function getOrCreateChatSession(sessionId, name) {
|
|
|
257
346
|
}
|
|
258
347
|
return fragments;
|
|
259
348
|
})(),
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
349
|
+
...(TIER_CAPS[activeTier].spawning ? [
|
|
350
|
+
`## Agent spawning (CRITICAL — follow exactly)`,
|
|
351
|
+
`When a task requires code editing, file operations, or shell commands, you MUST spawn a Claude Code agent.`,
|
|
352
|
+
`Do NOT describe what you would do — actually spawn the agent by including the block below.`,
|
|
353
|
+
`The block content MUST be valid JSON with "label" and "prompt" keys. No markdown, no backticks, no explanation inside the block.`,
|
|
354
|
+
``,
|
|
355
|
+
`### Agent prompt quality rules (MANDATORY)`,
|
|
356
|
+
`Agents are Claude Code sessions that edit files. They need CONCRETE instructions or they will fail.`,
|
|
357
|
+
`NEVER spawn an agent for a task that lacks a clear spec, requirement, or file to work on. If a board item is vague (e.g. "Rules Engine", "Skills Library"), do NOT spawn an agent — instead tell ${name} the item needs a spec first.`,
|
|
358
|
+
`Every agent prompt MUST include:`,
|
|
359
|
+
`- Real file paths from this project (e.g. "Edit src/queue/store.ts to add...")`,
|
|
360
|
+
`- What specifically to build, change, or fix`,
|
|
361
|
+
`- How it connects to existing code`,
|
|
362
|
+
`WRONG prompt: "Build a comprehensive rules engine with prioritization, conflict resolution, versioning..."`,
|
|
363
|
+
`RIGHT prompt: "In src/agents/spawn.ts, add a timeout retry: when an agent exits with code 1, re-spawn it once with the same prompt. Update the exit handler at line 77."`,
|
|
364
|
+
`If you cannot write a prompt with real file paths and concrete changes, the task is not ready to spawn. Tell ${name} what's missing and either propose a spec or ask what they want. Never go silent — if you can't act, communicate.`,
|
|
365
|
+
``,
|
|
366
|
+
`Format (place at the END of your response, OUTSIDE any code blocks):`,
|
|
367
|
+
`[AGENT_REQUEST]`,
|
|
368
|
+
`{"label": "short task name", "prompt": "Detailed instructions for the agent including file paths and what to do", "taskId": "internal-id-from-board"}`,
|
|
369
|
+
`[/AGENT_REQUEST]`,
|
|
370
|
+
`Include "taskId" when spawning for a specific board item (use the internal id, not the DASH-N identifier). This locks the task so other agents don't pick it up concurrently.`,
|
|
371
|
+
``,
|
|
372
|
+
`You can include multiple [AGENT_REQUEST] blocks to run tasks in parallel.`,
|
|
373
|
+
`WRONG: Describing the agent request in prose. WRONG: Wrapping the block in \`\`\`markdown. WRONG: Putting non-JSON text inside the block.`,
|
|
374
|
+
`RIGHT: Plain [AGENT_REQUEST] tag, one line of JSON, [/AGENT_REQUEST] tag. Nothing else inside.`,
|
|
375
|
+
`Agent failures are normal (auth issues, timeouts, environment mismatches). Never stop spawning agents because of past failures.`,
|
|
376
|
+
`IMPORTANT: Do NOT announce agent spawns in your visible response text. No "Agent spawned to...", no "I'll spawn an agent...", no "Let me run an agent...". The UI shows agent status automatically. Just include the [AGENT_REQUEST] block silently at the end. Your visible text should answer the user's question or continue the conversation naturally.`,
|
|
377
|
+
] : []), // end spawning gate
|
|
286
378
|
// Inject instance-readable vault values (CORE_*/DASH_* prefixed only — never secrets)
|
|
287
379
|
...(() => {
|
|
288
380
|
const readable = getDashReadableVault();
|
|
@@ -297,19 +389,21 @@ async function getOrCreateChatSession(sessionId, name) {
|
|
|
297
389
|
];
|
|
298
390
|
})(),
|
|
299
391
|
``,
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
392
|
+
...(TIER_CAPS[activeTier].spawning ? [
|
|
393
|
+
`## Autonomous work (already running)`,
|
|
394
|
+
`You have a background timer that checks the backlog every 15 minutes.`,
|
|
395
|
+
`When agents are idle and actionable items exist, a planner LLM picks tasks and spawns agents automatically.`,
|
|
396
|
+
`${name} does not need to be in chat for this to work — work continues in the background.`,
|
|
397
|
+
`Key facts:`,
|
|
398
|
+
`- Fires 60s after boot, then every 15 min`,
|
|
399
|
+
`- Only picks items in backlog/todo state, unassigned, not on cooldown`,
|
|
400
|
+
`- Max 5 agents per round, up to 5 continuation rounds per session`,
|
|
401
|
+
`- Failed tasks get escalating cooldowns (30min → 1hr → 2hr → 4hr) so they won't retry immediately`,
|
|
402
|
+
`- All activity logged to brain/ops/activity.jsonl`,
|
|
403
|
+
`- Circuit breaker pauses work for 30min if API credits run out`,
|
|
404
|
+
`When asked about autonomous work, explain this system accurately. You CAN work while ${name} is away.`,
|
|
405
|
+
`The user can type "auto" in chat to see the current autonomous status.`,
|
|
406
|
+
] : []), // end autonomous gate
|
|
313
407
|
``,
|
|
314
408
|
`## Security: encrypted memories`,
|
|
315
409
|
`Some of your memories (experiences, decisions, failures) are encrypted at rest. They are only available when ${name} has authenticated with their password.`,
|
|
@@ -345,17 +439,23 @@ async function getOrCreateChatSession(sessionId, name) {
|
|
|
345
439
|
chatSessions.set(sessionId, cs);
|
|
346
440
|
return cs;
|
|
347
441
|
}
|
|
442
|
+
// --- App ---
|
|
443
|
+
import { TIER_CAPS } from "./tier/types.js";
|
|
348
444
|
let activeTier = "local";
|
|
349
445
|
const app = new Hono();
|
|
350
|
-
// Global error handler —
|
|
446
|
+
// Global error handler — structured JSON errors
|
|
447
|
+
import { errorHandler, ApiError } from "./middleware/error-handler.js";
|
|
351
448
|
app.onError((err, c) => {
|
|
449
|
+
// Use structured handler for ApiErrors; preserve original behavior for others
|
|
450
|
+
if (err instanceof ApiError)
|
|
451
|
+
return errorHandler(err, c);
|
|
352
452
|
const msg = err instanceof Error ? err.message : String(err);
|
|
353
453
|
const stack = err instanceof Error ? err.stack : undefined;
|
|
354
454
|
log.error("Unhandled route error", { error: msg, stack, path: c.req.path, method: c.req.method });
|
|
355
|
-
return c.json({ error: msg }, 500);
|
|
455
|
+
return c.json({ error: msg, code: "INTERNAL_ERROR", status: 500 }, 500);
|
|
356
456
|
});
|
|
357
|
-
// Serve static files from
|
|
358
|
-
app.use("/public/*", serveStatic({ root:
|
|
457
|
+
// Serve static files from UI directory (CDN-synced or bundled fallback)
|
|
458
|
+
app.use("/public/*", serveStatic({ root: join(UI_DIR, ".."), rewriteRequestPath: (p) => p }));
|
|
359
459
|
// --- HTML template cache (replaces {{INSTANCE_NAME}} with configured name) ---
|
|
360
460
|
const htmlCache = new Map();
|
|
361
461
|
async function serveHtmlTemplate(filePath) {
|
|
@@ -373,12 +473,21 @@ async function serveHtmlTemplate(filePath) {
|
|
|
373
473
|
app.use("/api/*", requireSession());
|
|
374
474
|
// Serve index.html at root
|
|
375
475
|
app.get("/", async (c) => {
|
|
376
|
-
const html = await serveHtmlTemplate(join(
|
|
476
|
+
const html = await serveHtmlTemplate(join(UI_DIR, "index.html"));
|
|
377
477
|
return c.html(html);
|
|
378
478
|
});
|
|
479
|
+
// --- PWA assets (must be served from root for scope) ---
|
|
480
|
+
app.get("/manifest.json", async (c) => {
|
|
481
|
+
const data = await readFile(join(UI_DIR, "manifest.json"), "utf-8");
|
|
482
|
+
return c.json(JSON.parse(data));
|
|
483
|
+
});
|
|
484
|
+
app.get("/sw.js", async (c) => {
|
|
485
|
+
const data = await readFile(join(UI_DIR, "sw.js"), "utf-8");
|
|
486
|
+
return c.newResponse(data, 200, { "Content-Type": "application/javascript", "Service-Worker-Allowed": "/" });
|
|
487
|
+
});
|
|
379
488
|
// --- Nerve endpoint (PWA) ---
|
|
380
489
|
app.get("/nerve", async (c) => {
|
|
381
|
-
const html = await serveHtmlTemplate(join(
|
|
490
|
+
const html = await serveHtmlTemplate(join(UI_DIR, "nerve", "index.html"));
|
|
382
491
|
return c.html(html);
|
|
383
492
|
});
|
|
384
493
|
// --- Audit context middleware ---
|
|
@@ -396,6 +505,7 @@ app.use("/api/*", metricsMiddleware());
|
|
|
396
505
|
app.use("/api/pair", rateLimit({ windowMs: 15 * 60_000, max: 10 }));
|
|
397
506
|
app.use("/api/auth", rateLimit({ windowMs: 15 * 60_000, max: 10 }));
|
|
398
507
|
app.use("/api/recover", rateLimit({ windowMs: 15 * 60_000, max: 5 }));
|
|
508
|
+
app.use("/api/mobile/redeem", rateLimit({ windowMs: 15 * 60_000, max: 5 }));
|
|
399
509
|
// Dashboard endpoints get a separate, more generous limit — they poll frequently.
|
|
400
510
|
app.use("/api/ops/*", rateLimit({ windowMs: 60_000, max: 300 }));
|
|
401
511
|
// General API rate limit — skip paths that already have their own limiter.
|
|
@@ -469,6 +579,8 @@ app.get("/api/tier", async (c) => {
|
|
|
469
579
|
return c.json({
|
|
470
580
|
tier,
|
|
471
581
|
capabilities: TIER_CAPS[tier] ?? TIER_CAPS.local,
|
|
582
|
+
model: resolveChatModel() ?? "auto",
|
|
583
|
+
provider: resolveProvider(),
|
|
472
584
|
});
|
|
473
585
|
});
|
|
474
586
|
// Pairing ceremony
|
|
@@ -583,6 +695,378 @@ app.post("/api/recover", async (c) => {
|
|
|
583
695
|
await loadVault(result.sessionKey);
|
|
584
696
|
return c.json({ sessionId: result.session.id, name: result.name });
|
|
585
697
|
});
|
|
698
|
+
const deviceVouchers = new Map();
|
|
699
|
+
const pairedDevices = new Map();
|
|
700
|
+
// Load paired devices from brain on startup (called later in init)
|
|
701
|
+
async function loadPairedDevices() {
|
|
702
|
+
try {
|
|
703
|
+
const raw = await readFile(join(BRAIN_DIR, "identity", "devices.json"), "utf-8");
|
|
704
|
+
const devices = JSON.parse(raw);
|
|
705
|
+
for (const d of devices) {
|
|
706
|
+
pairedDevices.set(d.deviceToken, d);
|
|
707
|
+
// Re-create session so phone doesn't need to re-pair after Core restart
|
|
708
|
+
if (d.sessionId) {
|
|
709
|
+
createSession(d.humanName || d.label, d.sessionId);
|
|
710
|
+
log.debug("Restored session for paired device", { label: d.label, humanName: d.humanName });
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
catch { /* no devices yet */ }
|
|
715
|
+
}
|
|
716
|
+
async function savePairedDevices() {
|
|
717
|
+
try {
|
|
718
|
+
await mkdir(join(BRAIN_DIR, "identity"), { recursive: true });
|
|
719
|
+
await writeFile(join(BRAIN_DIR, "identity", "devices.json"), JSON.stringify([...pairedDevices.values()], null, 2), "utf-8");
|
|
720
|
+
}
|
|
721
|
+
catch { /* best effort */ }
|
|
722
|
+
}
|
|
723
|
+
// Issue a device voucher (requires active session — you're on the PC)
|
|
724
|
+
app.post("/api/mobile/voucher", async (c) => {
|
|
725
|
+
const sessionId = c.req.query("sessionId") || c.req.header("x-session-id");
|
|
726
|
+
if (!sessionId || !validateSession(sessionId)) {
|
|
727
|
+
return c.json({ error: "Unauthorized" }, 401);
|
|
728
|
+
}
|
|
729
|
+
const { randomBytes: rng } = await import("node:crypto");
|
|
730
|
+
const { createHash } = await import("node:crypto");
|
|
731
|
+
const token = `dv_${rng(8).toString("hex")}`;
|
|
732
|
+
const instanceHash = createHash("sha256")
|
|
733
|
+
.update(getInstanceName() + BRAIN_DIR)
|
|
734
|
+
.digest("hex")
|
|
735
|
+
.slice(0, 16);
|
|
736
|
+
const voucher = {
|
|
737
|
+
token,
|
|
738
|
+
instanceHash,
|
|
739
|
+
instanceName: getInstanceName(),
|
|
740
|
+
createdAt: Date.now(),
|
|
741
|
+
expiresAt: Date.now() + 10 * 60 * 1000, // 10 minutes
|
|
742
|
+
consumed: false,
|
|
743
|
+
};
|
|
744
|
+
deviceVouchers.set(token, voucher);
|
|
745
|
+
// Clean expired vouchers
|
|
746
|
+
for (const [k, v] of deviceVouchers) {
|
|
747
|
+
if (v.expiresAt < Date.now())
|
|
748
|
+
deviceVouchers.delete(k);
|
|
749
|
+
}
|
|
750
|
+
// The QR payload — a URL the phone opens directly
|
|
751
|
+
const voucherPayload = encodeURIComponent(JSON.stringify({
|
|
752
|
+
relay: "https://runcore.sh",
|
|
753
|
+
token,
|
|
754
|
+
instance: instanceHash,
|
|
755
|
+
name: voucher.instanceName,
|
|
756
|
+
}));
|
|
757
|
+
const qrUrl = `https://runcore.sh/pair#${voucherPayload}`;
|
|
758
|
+
// Generate QR code as data URL (server-side, proven library)
|
|
759
|
+
let qrDataUrl = "";
|
|
760
|
+
try {
|
|
761
|
+
const QRCode = (await import("qrcode")).default;
|
|
762
|
+
qrDataUrl = await QRCode.toDataURL(qrUrl, {
|
|
763
|
+
width: 250,
|
|
764
|
+
margin: 2,
|
|
765
|
+
color: { dark: "#000000", light: "#ffffff" },
|
|
766
|
+
errorCorrectionLevel: "L",
|
|
767
|
+
});
|
|
768
|
+
}
|
|
769
|
+
catch (err) {
|
|
770
|
+
log.warn("QR generation failed", { error: err instanceof Error ? err.message : String(err) });
|
|
771
|
+
}
|
|
772
|
+
return c.json({
|
|
773
|
+
token,
|
|
774
|
+
expiresIn: 600,
|
|
775
|
+
qrData: qrUrl,
|
|
776
|
+
qrImage: qrDataUrl,
|
|
777
|
+
instanceName: voucher.instanceName,
|
|
778
|
+
});
|
|
779
|
+
});
|
|
780
|
+
// Get voucher info (phone hits this to personalize before asking for safe word)
|
|
781
|
+
// Public — returns only display info, nothing secret
|
|
782
|
+
app.get("/api/mobile/info/:token", async (c) => {
|
|
783
|
+
const token = c.req.param("token");
|
|
784
|
+
const voucher = deviceVouchers.get(token);
|
|
785
|
+
if (!voucher || voucher.consumed || voucher.expiresAt < Date.now()) {
|
|
786
|
+
return c.json({ error: "Invalid or expired voucher" }, 404);
|
|
787
|
+
}
|
|
788
|
+
return c.json({
|
|
789
|
+
instanceName: voucher.instanceName,
|
|
790
|
+
expiresIn: Math.max(0, Math.round((voucher.expiresAt - Date.now()) / 1000)),
|
|
791
|
+
});
|
|
792
|
+
});
|
|
793
|
+
// Redeem voucher with safe word → device token
|
|
794
|
+
app.post("/api/mobile/redeem", async (c) => {
|
|
795
|
+
const body = await c.req.json();
|
|
796
|
+
const { token, password } = body;
|
|
797
|
+
if (!token || !password) {
|
|
798
|
+
return c.json({ error: "Voucher token and password required" }, 400);
|
|
799
|
+
}
|
|
800
|
+
const voucher = deviceVouchers.get(token);
|
|
801
|
+
if (!voucher || voucher.consumed || voucher.expiresAt < Date.now()) {
|
|
802
|
+
return c.json({ error: "Invalid or expired voucher" }, 404);
|
|
803
|
+
}
|
|
804
|
+
// Validate safe word via existing auth
|
|
805
|
+
const authResult = await authenticate(password);
|
|
806
|
+
if ("error" in authResult) {
|
|
807
|
+
return c.json({ error: "Invalid safe word" }, 401);
|
|
808
|
+
}
|
|
809
|
+
// Consume voucher
|
|
810
|
+
voucher.consumed = true;
|
|
811
|
+
// Issue device token
|
|
812
|
+
const { randomBytes: rng } = await import("node:crypto");
|
|
813
|
+
const deviceToken = `dt_${rng(16).toString("hex")}`;
|
|
814
|
+
const label = body.label || "Phone";
|
|
815
|
+
const device = {
|
|
816
|
+
deviceToken,
|
|
817
|
+
sessionId: authResult.session.id,
|
|
818
|
+
humanName: authResult.name,
|
|
819
|
+
label,
|
|
820
|
+
pairedAt: new Date().toISOString(),
|
|
821
|
+
lastSeen: new Date().toISOString(),
|
|
822
|
+
};
|
|
823
|
+
pairedDevices.set(deviceToken, device);
|
|
824
|
+
await savePairedDevices();
|
|
825
|
+
// Return session + device token
|
|
826
|
+
sessionKeys.set(authResult.session.id, authResult.sessionKey);
|
|
827
|
+
setEncryptionKey(authResult.sessionKey);
|
|
828
|
+
return c.json({
|
|
829
|
+
deviceToken,
|
|
830
|
+
sessionId: authResult.session.id,
|
|
831
|
+
instanceName: getInstanceName(),
|
|
832
|
+
});
|
|
833
|
+
});
|
|
834
|
+
// List paired devices (requires session)
|
|
835
|
+
app.get("/api/mobile/devices", async (c) => {
|
|
836
|
+
const sessionId = c.req.query("sessionId") || c.req.header("x-session-id");
|
|
837
|
+
if (!sessionId || !validateSession(sessionId)) {
|
|
838
|
+
return c.json({ error: "Unauthorized" }, 401);
|
|
839
|
+
}
|
|
840
|
+
return c.json({
|
|
841
|
+
devices: [...pairedDevices.values()].map((d) => ({
|
|
842
|
+
label: d.label,
|
|
843
|
+
pairedAt: d.pairedAt,
|
|
844
|
+
lastSeen: d.lastSeen,
|
|
845
|
+
})),
|
|
846
|
+
});
|
|
847
|
+
});
|
|
848
|
+
// Revoke a device (requires session)
|
|
849
|
+
app.delete("/api/mobile/devices/:label", async (c) => {
|
|
850
|
+
const sessionId = c.req.query("sessionId") || c.req.header("x-session-id");
|
|
851
|
+
if (!sessionId || !validateSession(sessionId)) {
|
|
852
|
+
return c.json({ error: "Unauthorized" }, 401);
|
|
853
|
+
}
|
|
854
|
+
const label = c.req.param("label");
|
|
855
|
+
for (const [token, d] of pairedDevices) {
|
|
856
|
+
if (d.label === label) {
|
|
857
|
+
pairedDevices.delete(token);
|
|
858
|
+
await savePairedDevices();
|
|
859
|
+
return c.json({ ok: true });
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
return c.json({ error: "Device not found" }, 404);
|
|
863
|
+
});
|
|
864
|
+
// --- Relay polling (receive messages from paired phones) ---
|
|
865
|
+
let relayPollInterval = null;
|
|
866
|
+
function startRelayPoll(instanceHash) {
|
|
867
|
+
if (relayPollInterval)
|
|
868
|
+
return;
|
|
869
|
+
const POLL_MS = 1_500; // 1.5 seconds
|
|
870
|
+
relayPollInterval = setInterval(async () => {
|
|
871
|
+
// Check for chat envelopes
|
|
872
|
+
try {
|
|
873
|
+
const res = await fetch(`https://runcore.sh/api/relay/envelope?recipient=${encodeURIComponent(instanceHash)}`, { signal: AbortSignal.timeout(8_000) });
|
|
874
|
+
if (res.ok) {
|
|
875
|
+
const data = await res.json();
|
|
876
|
+
if (data.envelopes && data.envelopes.length > 0) {
|
|
877
|
+
for (const env of data.envelopes) {
|
|
878
|
+
try {
|
|
879
|
+
const decoded = JSON.parse(Buffer.from(env.payload, "base64").toString("utf-8"));
|
|
880
|
+
if (decoded.type === "chat" && decoded.sessionId && decoded.message) {
|
|
881
|
+
log.info("Relay message received", { from: env.from, messageLen: decoded.message.length });
|
|
882
|
+
await handleRelayChat(decoded.sessionId, decoded.message, env.from, instanceHash);
|
|
883
|
+
}
|
|
884
|
+
else if (decoded.type === "sync" && decoded.sessionId) {
|
|
885
|
+
log.info("Relay sync request", { from: env.from });
|
|
886
|
+
await handleRelaySync(decoded.sessionId, env.from, instanceHash);
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
catch (err) {
|
|
890
|
+
log.debug("Failed to process relay envelope", { error: err instanceof Error ? err.message : String(err) });
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
catch {
|
|
897
|
+
// Network error — will retry on next poll
|
|
898
|
+
}
|
|
899
|
+
// Check for pending pair requests (independent of envelope check)
|
|
900
|
+
try {
|
|
901
|
+
const pairRes = await fetch(`https://runcore.sh/api/relay/pair?instance=${encodeURIComponent(instanceHash)}`, { signal: AbortSignal.timeout(8_000) });
|
|
902
|
+
if (pairRes.ok) {
|
|
903
|
+
const pairData = await pairRes.json();
|
|
904
|
+
if (pairData.requests && pairData.requests.length > 0) {
|
|
905
|
+
log.info("Relay pair requests received", { count: pairData.requests.length });
|
|
906
|
+
for (const req of pairData.requests) {
|
|
907
|
+
await handleRelayPair(req.token, req.password, req.label, instanceHash);
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
catch {
|
|
913
|
+
// Pair poll failed — will retry on next cycle
|
|
914
|
+
}
|
|
915
|
+
}, POLL_MS);
|
|
916
|
+
}
|
|
917
|
+
/**
|
|
918
|
+
* Handle a chat message received via relay from a paired phone.
|
|
919
|
+
* Processes through the same LLM pipeline as a local chat, sends response back through relay.
|
|
920
|
+
*/
|
|
921
|
+
/**
|
|
922
|
+
* Handle a sync request from a paired phone — send chat history back through relay.
|
|
923
|
+
*/
|
|
924
|
+
async function handleRelaySync(sid, senderHash, instanceHash) {
|
|
925
|
+
try {
|
|
926
|
+
// Find chat session — check this sessionId or grab the single existing one
|
|
927
|
+
let history = [];
|
|
928
|
+
const cs = chatSessions.get(sid) || (chatSessions.size > 0 ? chatSessions.values().next().value : null);
|
|
929
|
+
if (cs) {
|
|
930
|
+
// Send last 50 messages to keep payload reasonable
|
|
931
|
+
history = cs.history.slice(-50).map((m) => ({ role: m.role, content: m.content }));
|
|
932
|
+
}
|
|
933
|
+
await fetch("https://runcore.sh/api/relay/envelope", {
|
|
934
|
+
method: "POST",
|
|
935
|
+
headers: { "Content-Type": "application/json" },
|
|
936
|
+
body: JSON.stringify({
|
|
937
|
+
recipientHash: senderHash,
|
|
938
|
+
senderHash: instanceHash,
|
|
939
|
+
payload: Buffer.from(JSON.stringify({
|
|
940
|
+
type: "history",
|
|
941
|
+
messages: history,
|
|
942
|
+
timestamp: new Date().toISOString(),
|
|
943
|
+
})).toString("base64"),
|
|
944
|
+
}),
|
|
945
|
+
signal: AbortSignal.timeout(10_000),
|
|
946
|
+
});
|
|
947
|
+
log.info("Sent chat history to phone", { messageCount: history.length });
|
|
948
|
+
}
|
|
949
|
+
catch (err) {
|
|
950
|
+
log.warn("Relay sync failed", { error: err instanceof Error ? err.message : String(err) });
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
async function handleRelayChat(sid, message, senderHash, instanceHash) {
|
|
954
|
+
log.info("handleRelayChat start", { sid: sid.slice(0, 8), senderHash, messageLen: message.length });
|
|
955
|
+
// Get the user name — check session first, then paired device, then fallback
|
|
956
|
+
const session = validateSession(sid);
|
|
957
|
+
let userName = session?.name || "User";
|
|
958
|
+
// Look up paired device for real name
|
|
959
|
+
for (const d of pairedDevices.values()) {
|
|
960
|
+
if (d.sessionId === sid && d.humanName) {
|
|
961
|
+
userName = d.humanName;
|
|
962
|
+
break;
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
log.info("handleRelayChat session", { userName, sessionValid: !!session });
|
|
966
|
+
const cs = await getOrCreateChatSession(sid, userName);
|
|
967
|
+
log.info("handleRelayChat chatSession ready", { turnCount: cs.turnCount, historyLen: cs.history.length });
|
|
968
|
+
// Add user message to history (tagged with source device)
|
|
969
|
+
cs.history.push({ role: "user", content: message, source: "phone" });
|
|
970
|
+
try {
|
|
971
|
+
const provider = resolveProvider();
|
|
972
|
+
const model = resolveChatModel() ?? undefined;
|
|
973
|
+
log.info("handleRelayChat calling LLM", { provider, model });
|
|
974
|
+
const llmProvider = getProvider(provider);
|
|
975
|
+
const response = await llmProvider.completeChat(cs.history, model);
|
|
976
|
+
log.info("handleRelayChat LLM responded", { responseLen: response.length });
|
|
977
|
+
// Add response to history
|
|
978
|
+
cs.history.push({ role: "assistant", content: response });
|
|
979
|
+
cs.turnCount++;
|
|
980
|
+
// Send response back through relay to the phone
|
|
981
|
+
await fetch("https://runcore.sh/api/relay/envelope", {
|
|
982
|
+
method: "POST",
|
|
983
|
+
headers: { "Content-Type": "application/json" },
|
|
984
|
+
body: JSON.stringify({
|
|
985
|
+
recipientHash: senderHash,
|
|
986
|
+
senderHash: instanceHash,
|
|
987
|
+
payload: Buffer.from(JSON.stringify({
|
|
988
|
+
type: "chat_response",
|
|
989
|
+
message: response,
|
|
990
|
+
timestamp: new Date().toISOString(),
|
|
991
|
+
})).toString("base64"),
|
|
992
|
+
}),
|
|
993
|
+
signal: AbortSignal.timeout(10_000),
|
|
994
|
+
});
|
|
995
|
+
}
|
|
996
|
+
catch (err) {
|
|
997
|
+
log.warn("Relay chat failed", { error: err instanceof Error ? err.message : String(err) });
|
|
998
|
+
// Send error back to phone
|
|
999
|
+
await fetch("https://runcore.sh/api/relay/envelope", {
|
|
1000
|
+
method: "POST",
|
|
1001
|
+
headers: { "Content-Type": "application/json" },
|
|
1002
|
+
body: JSON.stringify({
|
|
1003
|
+
recipientHash: senderHash,
|
|
1004
|
+
senderHash: instanceHash,
|
|
1005
|
+
payload: Buffer.from(JSON.stringify({
|
|
1006
|
+
type: "status",
|
|
1007
|
+
message: "Error: " + (err instanceof Error ? err.message : String(err)),
|
|
1008
|
+
timestamp: new Date().toISOString(),
|
|
1009
|
+
})).toString("base64"),
|
|
1010
|
+
}),
|
|
1011
|
+
signal: AbortSignal.timeout(10_000),
|
|
1012
|
+
}).catch(() => { });
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
/**
|
|
1016
|
+
* Handle a pairing request received via relay.
|
|
1017
|
+
* Validates the safe word, issues device token, sends result back through relay.
|
|
1018
|
+
*/
|
|
1019
|
+
async function handleRelayPair(token, password, label, instanceHash) {
|
|
1020
|
+
let result;
|
|
1021
|
+
try {
|
|
1022
|
+
const voucher = deviceVouchers.get(token);
|
|
1023
|
+
if (!voucher || voucher.consumed || voucher.expiresAt < Date.now()) {
|
|
1024
|
+
result = { ok: false, error: "Invalid or expired voucher" };
|
|
1025
|
+
}
|
|
1026
|
+
else {
|
|
1027
|
+
const authResult = await authenticate(password);
|
|
1028
|
+
if ("error" in authResult) {
|
|
1029
|
+
result = { ok: false, error: "Invalid safe word" };
|
|
1030
|
+
}
|
|
1031
|
+
else {
|
|
1032
|
+
// Consume voucher
|
|
1033
|
+
voucher.consumed = true;
|
|
1034
|
+
// Issue device token
|
|
1035
|
+
const { randomBytes: rng } = await import("node:crypto");
|
|
1036
|
+
const deviceToken = `dt_${rng(16).toString("hex")}`;
|
|
1037
|
+
const device = {
|
|
1038
|
+
deviceToken,
|
|
1039
|
+
sessionId: authResult.session.id,
|
|
1040
|
+
humanName: authResult.name,
|
|
1041
|
+
label,
|
|
1042
|
+
pairedAt: new Date().toISOString(),
|
|
1043
|
+
lastSeen: new Date().toISOString(),
|
|
1044
|
+
};
|
|
1045
|
+
pairedDevices.set(deviceToken, device);
|
|
1046
|
+
await savePairedDevices();
|
|
1047
|
+
sessionKeys.set(authResult.session.id, authResult.sessionKey);
|
|
1048
|
+
setEncryptionKey(authResult.sessionKey);
|
|
1049
|
+
log.info("Device paired via relay", { label, token: token.slice(0, 8) + "..." });
|
|
1050
|
+
result = { ok: true, deviceToken, sessionId: authResult.session.id, instanceName: getInstanceName() };
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
catch (err) {
|
|
1055
|
+
result = { ok: false, error: err instanceof Error ? err.message : "Pairing failed" };
|
|
1056
|
+
}
|
|
1057
|
+
// Send result back through relay
|
|
1058
|
+
try {
|
|
1059
|
+
await fetch("https://runcore.sh/api/relay/pair", {
|
|
1060
|
+
method: "POST",
|
|
1061
|
+
headers: { "Content-Type": "application/json" },
|
|
1062
|
+
body: JSON.stringify({ type: "result", token, result }),
|
|
1063
|
+
signal: AbortSignal.timeout(10_000),
|
|
1064
|
+
});
|
|
1065
|
+
}
|
|
1066
|
+
catch {
|
|
1067
|
+
log.warn("Failed to send pair result to relay", { token: token.slice(0, 8) + "..." });
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
586
1070
|
// --- Vault routes ---
|
|
587
1071
|
// List vault keys (names + labels only, no values)
|
|
588
1072
|
app.get("/api/vault", async (c) => {
|
|
@@ -1267,6 +1751,61 @@ app.put("/api/prompt", async (c) => {
|
|
|
1267
1751
|
await writeBrainFile(PERSONALITY_PATH, prompt ?? "");
|
|
1268
1752
|
return c.json({ ok: true });
|
|
1269
1753
|
});
|
|
1754
|
+
// --- Model discovery ---
|
|
1755
|
+
app.get("/api/models", async (c) => {
|
|
1756
|
+
const ollamaUrl = process.env.OLLAMA_URL ?? "http://localhost:11434";
|
|
1757
|
+
try {
|
|
1758
|
+
const res = await fetch(`${ollamaUrl}/api/tags`, { signal: AbortSignal.timeout(3000) });
|
|
1759
|
+
if (!res.ok)
|
|
1760
|
+
return c.json({ models: [], error: "Ollama not responding" });
|
|
1761
|
+
const data = await res.json();
|
|
1762
|
+
const models = (data.models ?? []).map((m) => ({
|
|
1763
|
+
name: m.name,
|
|
1764
|
+
size: m.size,
|
|
1765
|
+
modified: m.modified_at,
|
|
1766
|
+
}));
|
|
1767
|
+
return c.json({ models });
|
|
1768
|
+
}
|
|
1769
|
+
catch {
|
|
1770
|
+
return c.json({ models: [], error: "Ollama not reachable" });
|
|
1771
|
+
}
|
|
1772
|
+
});
|
|
1773
|
+
// --- Sensitivity trainer ---
|
|
1774
|
+
app.post("/api/sensitive/flag", async (c) => {
|
|
1775
|
+
const body = await c.req.json().catch(() => null);
|
|
1776
|
+
if (!body?.value || typeof body.value !== "string") {
|
|
1777
|
+
return c.json({ error: "value required" }, 400);
|
|
1778
|
+
}
|
|
1779
|
+
const category = (body.category || "FLAGGED").toUpperCase();
|
|
1780
|
+
const value = body.value.trim();
|
|
1781
|
+
if (value.length < 2) {
|
|
1782
|
+
return c.json({ error: "value too short" }, 400);
|
|
1783
|
+
}
|
|
1784
|
+
if (!activeSensitiveRegistry) {
|
|
1785
|
+
return c.json({ error: "registry not initialized" }, 503);
|
|
1786
|
+
}
|
|
1787
|
+
const isNew = await activeSensitiveRegistry.addTerm(value, category);
|
|
1788
|
+
// Append-only exposure log: who saw this, when, which model, which turn
|
|
1789
|
+
const exposure = {
|
|
1790
|
+
timestamp: new Date().toISOString(),
|
|
1791
|
+
category,
|
|
1792
|
+
valueLength: value.length,
|
|
1793
|
+
model: body.model || null,
|
|
1794
|
+
threadId: body.threadId || null,
|
|
1795
|
+
turnIndex: body.turnIndex ?? null,
|
|
1796
|
+
provider: body.provider || null,
|
|
1797
|
+
action: "flag",
|
|
1798
|
+
isNew,
|
|
1799
|
+
};
|
|
1800
|
+
try {
|
|
1801
|
+
const logPath = join(BRAIN_DIR, "memory", "sensitivity-flags.jsonl");
|
|
1802
|
+
await appendFile(logPath, JSON.stringify(exposure) + "\n", "utf-8");
|
|
1803
|
+
}
|
|
1804
|
+
catch {
|
|
1805
|
+
// best-effort logging
|
|
1806
|
+
}
|
|
1807
|
+
return c.json({ ok: true, isNew, category });
|
|
1808
|
+
});
|
|
1270
1809
|
// --- Settings routes ---
|
|
1271
1810
|
app.get("/api/settings", async (c) => {
|
|
1272
1811
|
const settings = getSettings();
|
|
@@ -1281,6 +1820,29 @@ app.get("/api/settings", async (c) => {
|
|
|
1281
1820
|
});
|
|
1282
1821
|
app.put("/api/settings", async (c) => {
|
|
1283
1822
|
const body = await c.req.json();
|
|
1823
|
+
// Handle human name change — updates identity file, session, and paired devices
|
|
1824
|
+
if (typeof body.humanName === "string" && body.humanName.trim()) {
|
|
1825
|
+
const newName = body.humanName.trim();
|
|
1826
|
+
const sid = c.req.query("sessionId") || c.req.header("x-session-id") || "";
|
|
1827
|
+
// Update identity file
|
|
1828
|
+
try {
|
|
1829
|
+
const { updateHumanName } = await import("./auth/identity.js");
|
|
1830
|
+
await updateHumanName(newName);
|
|
1831
|
+
}
|
|
1832
|
+
catch (err) {
|
|
1833
|
+
log.warn("Failed to update human name in identity file");
|
|
1834
|
+
}
|
|
1835
|
+
// Update current session
|
|
1836
|
+
const session = validateSession(sid);
|
|
1837
|
+
if (session)
|
|
1838
|
+
session.name = newName;
|
|
1839
|
+
// Update all paired devices
|
|
1840
|
+
for (const d of pairedDevices.values()) {
|
|
1841
|
+
d.humanName = newName;
|
|
1842
|
+
}
|
|
1843
|
+
await savePairedDevices();
|
|
1844
|
+
return c.json({ ok: true, humanName: newName });
|
|
1845
|
+
}
|
|
1284
1846
|
const updated = await updateSettings(body);
|
|
1285
1847
|
return c.json({
|
|
1286
1848
|
...updated,
|
|
@@ -1391,7 +1953,7 @@ app.get("/api/avatar/video/:hash", async (c) => {
|
|
|
1391
1953
|
if (!/^[a-f0-9]+\.mp4$/.test(hash)) {
|
|
1392
1954
|
return c.json({ error: "Invalid hash" }, 400);
|
|
1393
1955
|
}
|
|
1394
|
-
const filePath = join(
|
|
1956
|
+
const filePath = join(UI_DIR, "avatar", "cache", hash);
|
|
1395
1957
|
try {
|
|
1396
1958
|
const mp4 = await readFile(filePath);
|
|
1397
1959
|
return new Response(mp4, {
|
|
@@ -1421,7 +1983,7 @@ app.post("/api/avatar/photo", async (c) => {
|
|
|
1421
1983
|
return c.json({ error: "Photo body required" }, 400);
|
|
1422
1984
|
const avatarConfig = getAvatarConfig();
|
|
1423
1985
|
const photoPath = join(process.cwd(), avatarConfig.photoPath);
|
|
1424
|
-
await mkdir(join(
|
|
1986
|
+
await mkdir(join(UI_DIR, "avatar"), { recursive: true });
|
|
1425
1987
|
await writeFile(photoPath, Buffer.from(body));
|
|
1426
1988
|
const ok = await preparePhoto(photoPath);
|
|
1427
1989
|
if (ok) {
|
|
@@ -1480,7 +2042,139 @@ app.get("/api/history", async (c) => {
|
|
|
1480
2042
|
.map((m) => ({ role: m.role, content: m.content }));
|
|
1481
2043
|
return c.json({ messages });
|
|
1482
2044
|
});
|
|
2045
|
+
// Persist intro message so it appears in all tabs/devices
|
|
2046
|
+
app.post("/api/history/intro", async (c) => {
|
|
2047
|
+
const body = await c.req.json();
|
|
2048
|
+
const { sessionId, message } = body;
|
|
2049
|
+
if (!sessionId || !message)
|
|
2050
|
+
return c.json({ error: "sessionId and message required" }, 400);
|
|
2051
|
+
const session = validateSession(sessionId);
|
|
2052
|
+
if (!session)
|
|
2053
|
+
return c.json({ error: "Invalid session" }, 401);
|
|
2054
|
+
const cs = await getOrCreateChatSession(sessionId, session.name);
|
|
2055
|
+
// Only add if history is empty (first run)
|
|
2056
|
+
if (cs.history.length === 0) {
|
|
2057
|
+
cs.history.push({ role: "assistant", content: message });
|
|
2058
|
+
}
|
|
2059
|
+
return c.json({ ok: true });
|
|
2060
|
+
});
|
|
2061
|
+
// --- Thread routes ---
|
|
2062
|
+
app.get("/api/threads", async (c) => {
|
|
2063
|
+
const sessionId = c.req.query("sessionId");
|
|
2064
|
+
if (!sessionId)
|
|
2065
|
+
return c.json({ error: "sessionId required" }, 400);
|
|
2066
|
+
const session = validateSession(sessionId);
|
|
2067
|
+
if (!session)
|
|
2068
|
+
return c.json({ error: "Invalid session" }, 401);
|
|
2069
|
+
const threads = getThreadsForSession(sessionId);
|
|
2070
|
+
const list = [...threads.values()]
|
|
2071
|
+
.sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime())
|
|
2072
|
+
.map(t => ({ id: t.id, title: t.title, createdAt: t.createdAt, updatedAt: t.updatedAt }));
|
|
2073
|
+
return c.json({ threads: list });
|
|
2074
|
+
});
|
|
2075
|
+
app.post("/api/threads", async (c) => {
|
|
2076
|
+
const body = await c.req.json();
|
|
2077
|
+
const sessionId = body.sessionId;
|
|
2078
|
+
if (!sessionId)
|
|
2079
|
+
return c.json({ error: "sessionId required" }, 400);
|
|
2080
|
+
const session = validateSession(sessionId);
|
|
2081
|
+
if (!session)
|
|
2082
|
+
return c.json({ error: "Invalid session" }, 401);
|
|
2083
|
+
const threads = getThreadsForSession(sessionId);
|
|
2084
|
+
const now = new Date().toISOString();
|
|
2085
|
+
const thread = {
|
|
2086
|
+
id: generateThreadId(),
|
|
2087
|
+
title: body.title || "New thread",
|
|
2088
|
+
history: [],
|
|
2089
|
+
historySummary: "",
|
|
2090
|
+
createdAt: now,
|
|
2091
|
+
updatedAt: now,
|
|
2092
|
+
};
|
|
2093
|
+
threads.set(thread.id, thread);
|
|
2094
|
+
// Save current main history as the previous thread, then clear for fresh conversation
|
|
2095
|
+
const cs = chatSessions.get(sessionId);
|
|
2096
|
+
if (cs) {
|
|
2097
|
+
cs.history = [];
|
|
2098
|
+
cs.historySummary = "";
|
|
2099
|
+
cs.foldedBack = false;
|
|
2100
|
+
cs.turnCount = 0;
|
|
2101
|
+
}
|
|
2102
|
+
return c.json({ thread: { id: thread.id, title: thread.title, createdAt: thread.createdAt, updatedAt: thread.updatedAt } });
|
|
2103
|
+
});
|
|
2104
|
+
app.get("/api/threads/:id/history", async (c) => {
|
|
2105
|
+
const sessionId = c.req.query("sessionId");
|
|
2106
|
+
if (!sessionId)
|
|
2107
|
+
return c.json({ error: "sessionId required" }, 400);
|
|
2108
|
+
const session = validateSession(sessionId);
|
|
2109
|
+
if (!session)
|
|
2110
|
+
return c.json({ error: "Invalid session" }, 401);
|
|
2111
|
+
const threads = getThreadsForSession(sessionId);
|
|
2112
|
+
const thread = threads.get(c.req.param("id"));
|
|
2113
|
+
if (!thread)
|
|
2114
|
+
return c.json({ error: "Thread not found" }, 404);
|
|
2115
|
+
const messages = thread.history
|
|
2116
|
+
.filter((m) => m.role === "user" || m.role === "assistant")
|
|
2117
|
+
.map((m) => ({ role: m.role, content: m.content }));
|
|
2118
|
+
return c.json({ messages });
|
|
2119
|
+
});
|
|
2120
|
+
app.patch("/api/threads/:id", async (c) => {
|
|
2121
|
+
const body = await c.req.json();
|
|
2122
|
+
const sessionId = body.sessionId;
|
|
2123
|
+
if (!sessionId)
|
|
2124
|
+
return c.json({ error: "sessionId required" }, 400);
|
|
2125
|
+
const session = validateSession(sessionId);
|
|
2126
|
+
if (!session)
|
|
2127
|
+
return c.json({ error: "Invalid session" }, 401);
|
|
2128
|
+
const threads = getThreadsForSession(sessionId);
|
|
2129
|
+
const thread = threads.get(c.req.param("id"));
|
|
2130
|
+
if (!thread)
|
|
2131
|
+
return c.json({ error: "Thread not found" }, 404);
|
|
2132
|
+
if (body.title)
|
|
2133
|
+
thread.title = body.title;
|
|
2134
|
+
thread.updatedAt = new Date().toISOString();
|
|
2135
|
+
return c.json({ ok: true });
|
|
2136
|
+
});
|
|
2137
|
+
app.delete("/api/threads/:id", async (c) => {
|
|
2138
|
+
const sessionId = c.req.query("sessionId");
|
|
2139
|
+
if (!sessionId)
|
|
2140
|
+
return c.json({ error: "sessionId required" }, 400);
|
|
2141
|
+
const session = validateSession(sessionId);
|
|
2142
|
+
if (!session)
|
|
2143
|
+
return c.json({ error: "Invalid session" }, 401);
|
|
2144
|
+
const threads = getThreadsForSession(sessionId);
|
|
2145
|
+
threads.delete(c.req.param("id"));
|
|
2146
|
+
return c.json({ ok: true });
|
|
2147
|
+
});
|
|
1483
2148
|
// Activity log: poll for background actions
|
|
2149
|
+
// SSE stream — real-time activity push
|
|
2150
|
+
app.get("/api/activity/stream", async (c) => {
|
|
2151
|
+
const sessionId = c.req.query("sessionId");
|
|
2152
|
+
if (!sessionId)
|
|
2153
|
+
return c.json({ error: "sessionId required" }, 400);
|
|
2154
|
+
const session = validateSession(sessionId);
|
|
2155
|
+
if (!session)
|
|
2156
|
+
return c.json({ error: "Invalid or expired session" }, 401);
|
|
2157
|
+
const { onActivity } = await import("./activity/log.js");
|
|
2158
|
+
return streamSSE(c, async (stream) => {
|
|
2159
|
+
// Send heartbeat immediately
|
|
2160
|
+
await stream.writeSSE({ data: JSON.stringify({ type: "heartbeat" }) });
|
|
2161
|
+
const unsub = onActivity((entry) => {
|
|
2162
|
+
stream.writeSSE({
|
|
2163
|
+
data: JSON.stringify({ type: "action", action: entry }),
|
|
2164
|
+
}).catch(() => { });
|
|
2165
|
+
});
|
|
2166
|
+
// Heartbeat every 30s to keep connection alive
|
|
2167
|
+
const heartbeat = setInterval(() => {
|
|
2168
|
+
stream.writeSSE({ data: JSON.stringify({ type: "heartbeat" }) }).catch(() => { });
|
|
2169
|
+
}, 30_000);
|
|
2170
|
+
stream.onAbort(() => {
|
|
2171
|
+
unsub();
|
|
2172
|
+
clearInterval(heartbeat);
|
|
2173
|
+
});
|
|
2174
|
+
// Keep stream open
|
|
2175
|
+
await new Promise(() => { });
|
|
2176
|
+
});
|
|
2177
|
+
});
|
|
1484
2178
|
app.get("/api/activity", async (c) => {
|
|
1485
2179
|
const sessionId = c.req.query("sessionId");
|
|
1486
2180
|
if (!sessionId)
|
|
@@ -1547,13 +2241,24 @@ app.post("/api/branch", async (c) => {
|
|
|
1547
2241
|
throw err;
|
|
1548
2242
|
}
|
|
1549
2243
|
const reqSignal = c.req.raw.signal;
|
|
2244
|
+
// Apply membrane to branch messages before they reach the LLM
|
|
2245
|
+
const branchMembrane = getActiveMembrane();
|
|
2246
|
+
const redactedBranchMessages = messages.map((msg) => {
|
|
2247
|
+
if (!branchMembrane)
|
|
2248
|
+
return msg;
|
|
2249
|
+
const copy = { ...msg };
|
|
2250
|
+
if (typeof copy.content === "string") {
|
|
2251
|
+
copy.content = branchMembrane.apply(copy.content);
|
|
2252
|
+
}
|
|
2253
|
+
return copy;
|
|
2254
|
+
});
|
|
1550
2255
|
return streamSSE(c, async (stream) => {
|
|
1551
2256
|
// Send branch trace metadata so the UI can track lineage
|
|
1552
2257
|
await stream.writeSSE({
|
|
1553
2258
|
data: JSON.stringify({
|
|
1554
2259
|
meta: {
|
|
1555
2260
|
provider: activeProvider,
|
|
1556
|
-
model: activeChatModel ?? (activeProvider === "ollama" ? "llama3.
|
|
2261
|
+
model: activeChatModel ?? (activeProvider === "ollama" ? "llama3.2:3b" : "claude-sonnet-4"),
|
|
1557
2262
|
traceId: branchTraceId,
|
|
1558
2263
|
backref: primaryBackref,
|
|
1559
2264
|
},
|
|
@@ -1576,7 +2281,7 @@ app.post("/api/branch", async (c) => {
|
|
|
1576
2281
|
stream.writeSSE({ data: JSON.stringify({ token: rehydrated }) }).catch(() => { });
|
|
1577
2282
|
};
|
|
1578
2283
|
stream_fn({
|
|
1579
|
-
messages,
|
|
2284
|
+
messages: redactedBranchMessages,
|
|
1580
2285
|
model: activeChatModel,
|
|
1581
2286
|
signal: reqSignal,
|
|
1582
2287
|
onToken: (token) => {
|
|
@@ -1697,6 +2402,12 @@ app.post("/api/agents/locks/prune", async (_c) => {
|
|
|
1697
2402
|
const pruned = await pruneAllStaleLocks();
|
|
1698
2403
|
return _c.json({ pruned });
|
|
1699
2404
|
});
|
|
2405
|
+
// --- Self-reported issues (autonomous agent findings) ---
|
|
2406
|
+
app.get("/api/agents/issues", async (c) => {
|
|
2407
|
+
const { listIssues } = await import("./agents/issues.js");
|
|
2408
|
+
const issues = await listIssues();
|
|
2409
|
+
return c.json({ issues });
|
|
2410
|
+
});
|
|
1700
2411
|
// --- Agent runtime routes ---
|
|
1701
2412
|
app.get("/api/runtime/status", async (c) => {
|
|
1702
2413
|
const rt = getRuntime();
|
|
@@ -2630,82 +3341,216 @@ app.post("/api/board/review/trigger", async (c) => {
|
|
|
2630
3341
|
// ---------------------------------------------------------------------------
|
|
2631
3342
|
// List all registered skills (metadata only)
|
|
2632
3343
|
app.get("/api/skills", async (c) => {
|
|
2633
|
-
const
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
name: s.meta.name,
|
|
2644
|
-
description: s.meta.description,
|
|
2645
|
-
slot: s.meta.slot,
|
|
2646
|
-
state: s.state,
|
|
2647
|
-
userInvocable: s.meta.userInvocable,
|
|
2648
|
-
source: s.meta.source.type,
|
|
2649
|
-
version: s.meta.version,
|
|
2650
|
-
registeredAt: s.registeredAt,
|
|
3344
|
+
const typeFilter = c.req.query("type");
|
|
3345
|
+
const skills = await _skillRegistry.list();
|
|
3346
|
+
const filtered = typeFilter ? skills.filter((s) => s.type === typeFilter) : skills;
|
|
3347
|
+
return c.json(filtered.map((s) => ({
|
|
3348
|
+
id: s.id,
|
|
3349
|
+
name: s.name,
|
|
3350
|
+
type: s.type,
|
|
3351
|
+
description: s.description,
|
|
3352
|
+
triggers: s.triggers,
|
|
3353
|
+
loads: s.loads,
|
|
2651
3354
|
})));
|
|
2652
3355
|
});
|
|
2653
|
-
// Get a single skill (metadata +
|
|
3356
|
+
// Get a single skill (metadata + full content)
|
|
2654
3357
|
app.get("/api/skills/:name", async (c) => {
|
|
2655
|
-
const registry = getSkillRegistry();
|
|
2656
|
-
if (!registry)
|
|
2657
|
-
return c.json({ error: "Skills registry not initialized" }, 503);
|
|
2658
3358
|
const name = c.req.param("name");
|
|
2659
|
-
const skill =
|
|
3359
|
+
const skill = await _skillRegistry.get(name);
|
|
2660
3360
|
if (!skill)
|
|
2661
3361
|
return c.json({ error: "Skill not found" }, 404);
|
|
2662
|
-
|
|
2663
|
-
await registry.loadBody(name);
|
|
3362
|
+
const content = await _skillRegistry.getContent(name);
|
|
2664
3363
|
return c.json({
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
slot: skill.meta.slot,
|
|
2668
|
-
state: skill.state,
|
|
2669
|
-
userInvocable: skill.meta.userInvocable,
|
|
2670
|
-
disableModelInvocation: skill.meta.disableModelInvocation,
|
|
2671
|
-
source: skill.meta.source,
|
|
2672
|
-
version: skill.meta.version,
|
|
2673
|
-
body: skill.body,
|
|
2674
|
-
referencedFiles: skill.referencedFiles,
|
|
2675
|
-
registeredAt: skill.registeredAt,
|
|
2676
|
-
refreshedAt: skill.refreshedAt,
|
|
3364
|
+
...skill,
|
|
3365
|
+
content,
|
|
2677
3366
|
});
|
|
2678
3367
|
});
|
|
2679
|
-
// Resolve
|
|
3368
|
+
// Resolve trigger → matching skill
|
|
2680
3369
|
app.post("/api/skills/resolve", async (c) => {
|
|
2681
|
-
const
|
|
2682
|
-
if (!
|
|
2683
|
-
return c.json({ error: "
|
|
2684
|
-
const
|
|
2685
|
-
if (!
|
|
2686
|
-
return c.json({ error: "
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
name:
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
priority: r.priority,
|
|
2695
|
-
})));
|
|
3370
|
+
const { trigger } = await c.req.json();
|
|
3371
|
+
if (!trigger)
|
|
3372
|
+
return c.json({ error: "trigger is required" }, 400);
|
|
3373
|
+
const skill = await _skillRegistry.findByTrigger(trigger);
|
|
3374
|
+
if (!skill)
|
|
3375
|
+
return c.json({ error: "No matching skill" }, 404);
|
|
3376
|
+
return c.json({
|
|
3377
|
+
id: skill.id,
|
|
3378
|
+
name: skill.name,
|
|
3379
|
+
type: skill.type,
|
|
3380
|
+
description: skill.description,
|
|
3381
|
+
triggers: skill.triggers,
|
|
3382
|
+
});
|
|
2696
3383
|
});
|
|
2697
|
-
//
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
3384
|
+
// --- Plugin status routes ---
|
|
3385
|
+
import { getPluginStatusSummary } from "./plugins/status.js";
|
|
3386
|
+
import { initPlugins, shutdownPlugins } from "./plugins/index.js";
|
|
3387
|
+
// --- File management routes ---
|
|
3388
|
+
import { fileRegistry, computeChecksum } from "./files/registry.js";
|
|
3389
|
+
import { validateUpload } from "./files/validate.js";
|
|
3390
|
+
import { slugify } from "./files/validate.js";
|
|
3391
|
+
app.get("/api/plugins", (c) => {
|
|
3392
|
+
return c.json(getPluginStatusSummary());
|
|
3393
|
+
});
|
|
3394
|
+
// --- File management routes ---
|
|
3395
|
+
// Upload file — persist to brain/files/data/, register in JSONL
|
|
3396
|
+
app.post("/api/files/upload", async (c) => {
|
|
3397
|
+
try {
|
|
3398
|
+
const formData = await c.req.formData();
|
|
3399
|
+
const file = formData.get("file");
|
|
3400
|
+
if (!file)
|
|
3401
|
+
return c.json({ error: "No file provided" }, 400);
|
|
3402
|
+
const source = formData.get("source") || "user-upload";
|
|
3403
|
+
const tagsRaw = formData.get("tags");
|
|
3404
|
+
const tags = tagsRaw ? tagsRaw.split(",").map(t => t.trim()).filter(Boolean) : [];
|
|
3405
|
+
// Folder path from directory upload — preserved as virtual folder tag
|
|
3406
|
+
const folder = formData.get("folder");
|
|
3407
|
+
if (folder)
|
|
3408
|
+
tags.push("folder:" + folder);
|
|
3409
|
+
const buffer = Buffer.from(await file.arrayBuffer());
|
|
3410
|
+
const maxUploadBytes = 50 * 1024 * 1024; // 50 MB
|
|
3411
|
+
// Validate: extension allowlist, magic bytes, size, content scan
|
|
3412
|
+
const validation = await validateUpload(buffer, file.name, file.type, maxUploadBytes);
|
|
3413
|
+
if (!validation.valid) {
|
|
3414
|
+
return c.json({ error: validation.rejected }, 400);
|
|
3415
|
+
}
|
|
3416
|
+
// Generate storage path: brain/files/data/YYYY-MM-DD/slug_id.ext
|
|
3417
|
+
const dateDir = new Date().toISOString().slice(0, 10);
|
|
3418
|
+
const slug = slugify(file.name.replace(/\.[^.]+$/, ""));
|
|
3419
|
+
const checksum = computeChecksum(buffer);
|
|
3420
|
+
// Check for duplicate by checksum
|
|
3421
|
+
const existing = await fileRegistry.list({});
|
|
3422
|
+
const dup = existing.find(r => r.checksum === checksum && r.status === "active");
|
|
3423
|
+
if (dup) {
|
|
3424
|
+
return c.json({ file: dup, duplicate: true });
|
|
3425
|
+
}
|
|
3426
|
+
const storageDir = join(BRAIN_DIR, "files", "data", dateDir);
|
|
3427
|
+
await mkdir(storageDir, { recursive: true });
|
|
3428
|
+
const storedName = `${slug}_${Date.now()}${validation.detectedExt}`;
|
|
3429
|
+
const storagePath = join("files", "data", dateDir, storedName);
|
|
3430
|
+
const fullPath = join(BRAIN_DIR, storagePath);
|
|
3431
|
+
await writeFile(fullPath, buffer);
|
|
3432
|
+
// Extract text preview for searchability
|
|
3433
|
+
let textPreview;
|
|
3434
|
+
const textExts = new Set([".txt", ".md", ".csv", ".json", ".yaml", ".yml", ".xml", ".log"]);
|
|
3435
|
+
if (textExts.has(validation.detectedExt)) {
|
|
3436
|
+
textPreview = buffer.toString("utf-8").slice(0, 500);
|
|
3437
|
+
}
|
|
3438
|
+
else if (validation.detectedExt === ".pdf") {
|
|
3439
|
+
try {
|
|
3440
|
+
const { extractPdfText } = await import("./files/extract.js");
|
|
3441
|
+
textPreview = (await extractPdfText(buffer)).slice(0, 500);
|
|
3442
|
+
}
|
|
3443
|
+
catch { /* PDF extraction optional */ }
|
|
3444
|
+
}
|
|
3445
|
+
// Register in file registry
|
|
3446
|
+
const record = await fileRegistry.register({
|
|
3447
|
+
filename: validation.sanitizedName,
|
|
3448
|
+
storagePath,
|
|
3449
|
+
mimeType: validation.detectedMime || file.type,
|
|
3450
|
+
sizeBytes: buffer.length,
|
|
3451
|
+
checksum,
|
|
3452
|
+
tags,
|
|
3453
|
+
source,
|
|
3454
|
+
status: "active",
|
|
3455
|
+
});
|
|
3456
|
+
// Trigger volume replication (on-write event)
|
|
3457
|
+
volumeManager.handleEvent({ type: "write", fileId: record.id, volume: "primary" }).catch((err) => log.warn("Volume on-write event failed", { error: String(err) }));
|
|
3458
|
+
return c.json({ file: record, duplicate: false });
|
|
3459
|
+
}
|
|
3460
|
+
catch (err) {
|
|
3461
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
3462
|
+
log.warn("File upload failed", { error: msg });
|
|
3463
|
+
return c.json({ error: `Upload failed: ${msg}` }, 500);
|
|
3464
|
+
}
|
|
3465
|
+
});
|
|
3466
|
+
// Download / serve a stored file
|
|
3467
|
+
app.get("/api/files/:id/download", async (c) => {
|
|
3468
|
+
const record = await fileRegistry.get(c.req.param("id"));
|
|
3469
|
+
if (!record)
|
|
3470
|
+
return c.json({ error: "File not found" }, 404);
|
|
3471
|
+
const fullPath = join(BRAIN_DIR, record.storagePath);
|
|
3472
|
+
try {
|
|
3473
|
+
const data = await readFile(fullPath);
|
|
3474
|
+
return c.newResponse(data, 200, {
|
|
3475
|
+
"Content-Type": record.mimeType,
|
|
3476
|
+
"Content-Disposition": `inline; filename="${record.filename}"`,
|
|
3477
|
+
"Content-Length": String(data.length),
|
|
3478
|
+
});
|
|
3479
|
+
}
|
|
3480
|
+
catch {
|
|
3481
|
+
return c.json({ error: "File data not found on disk" }, 404);
|
|
3482
|
+
}
|
|
3483
|
+
});
|
|
3484
|
+
// List virtual folders (must be before :id route)
|
|
3485
|
+
app.get("/api/files/folders", async (c) => {
|
|
3486
|
+
const folders = await fileRegistry.getFolders();
|
|
3487
|
+
return c.json({ folders });
|
|
3488
|
+
});
|
|
3489
|
+
app.get("/api/files", async (c) => {
|
|
3490
|
+
const status = c.req.query("status");
|
|
3491
|
+
const source = c.req.query("source");
|
|
3492
|
+
const q = c.req.query("q");
|
|
3493
|
+
if (q) {
|
|
3494
|
+
const results = await fileRegistry.search(q);
|
|
3495
|
+
return c.json({ files: results, total: results.length });
|
|
3496
|
+
}
|
|
3497
|
+
const results = await fileRegistry.list({ status, source });
|
|
3498
|
+
return c.json({ files: results, total: results.length });
|
|
3499
|
+
});
|
|
3500
|
+
app.get("/api/files/:id", async (c) => {
|
|
3501
|
+
const record = await fileRegistry.get(c.req.param("id"));
|
|
3502
|
+
if (!record)
|
|
3503
|
+
return c.json({ error: "File not found", code: "NOT_FOUND", status: 404 }, 404);
|
|
3504
|
+
return c.json(record);
|
|
3505
|
+
});
|
|
3506
|
+
app.post("/api/files/:id/archive", async (c) => {
|
|
3507
|
+
const result = await fileRegistry.archive(c.req.param("id"));
|
|
3508
|
+
if (!result)
|
|
3509
|
+
return c.json({ error: "File not found", code: "NOT_FOUND", status: 404 }, 404);
|
|
2707
3510
|
return c.json(result);
|
|
2708
3511
|
});
|
|
3512
|
+
// Update file tags / move to virtual folder
|
|
3513
|
+
app.put("/api/files/:id", async (c) => {
|
|
3514
|
+
const body = await c.req.json();
|
|
3515
|
+
const { tags, source, folder } = body;
|
|
3516
|
+
const id = c.req.param("id");
|
|
3517
|
+
const record = await fileRegistry.get(id);
|
|
3518
|
+
if (!record)
|
|
3519
|
+
return c.json({ error: "File not found" }, 404);
|
|
3520
|
+
// Handle virtual folder: stored as tag "folder:Name"
|
|
3521
|
+
let updatedTags = tags ?? [...(record.tags ?? [])];
|
|
3522
|
+
if (folder !== undefined) {
|
|
3523
|
+
// Remove existing folder tags, add new one
|
|
3524
|
+
updatedTags = updatedTags.filter(t => !t.startsWith("folder:"));
|
|
3525
|
+
if (folder)
|
|
3526
|
+
updatedTags.push("folder:" + folder);
|
|
3527
|
+
}
|
|
3528
|
+
const result = await fileRegistry.update(id, { tags: updatedTags, ...(source ? { source } : {}) });
|
|
3529
|
+
return c.json(result);
|
|
3530
|
+
});
|
|
3531
|
+
// --- Volume management routes ---
|
|
3532
|
+
app.get("/api/volumes", async (c) => {
|
|
3533
|
+
const states = volumeManager.getStates();
|
|
3534
|
+
const configs = volumeManager.getConfigs();
|
|
3535
|
+
return c.json({
|
|
3536
|
+
volumes: configs.map((cfg) => {
|
|
3537
|
+
const state = states.find((s) => s.name === cfg.name);
|
|
3538
|
+
return { ...cfg, ...state };
|
|
3539
|
+
}),
|
|
3540
|
+
pendingReplications: volumeManager.getPendingCount(),
|
|
3541
|
+
});
|
|
3542
|
+
});
|
|
3543
|
+
app.post("/api/volumes/probe", async (c) => {
|
|
3544
|
+
const states = await volumeManager.probeAll();
|
|
3545
|
+
return c.json({ volumes: states });
|
|
3546
|
+
});
|
|
3547
|
+
app.post("/api/volumes/event", async (c) => {
|
|
3548
|
+
const event = await c.req.json();
|
|
3549
|
+
if (!event?.type)
|
|
3550
|
+
return c.json({ error: "Missing event type" }, 400);
|
|
3551
|
+
await volumeManager.handleEvent(event);
|
|
3552
|
+
return c.json({ ok: true });
|
|
3553
|
+
});
|
|
2709
3554
|
// --- Module discovery routes ---
|
|
2710
3555
|
app.get("/api/modules", (c) => {
|
|
2711
3556
|
const registry = getModuleRegistry();
|
|
@@ -3198,6 +4043,67 @@ app.get("/api/metrics/names", async (c) => {
|
|
|
3198
4043
|
const names = await metricsStore.metricNames();
|
|
3199
4044
|
return c.json({ names });
|
|
3200
4045
|
});
|
|
4046
|
+
// Time-series bucketed data for a named metric.
|
|
4047
|
+
app.get("/api/metrics/series", async (c) => {
|
|
4048
|
+
const name = c.req.query("name");
|
|
4049
|
+
if (!name)
|
|
4050
|
+
return c.json({ error: "name parameter required" }, 400);
|
|
4051
|
+
const now = Date.now();
|
|
4052
|
+
const since = c.req.query("since") ?? new Date(now - 60 * 60 * 1000).toISOString();
|
|
4053
|
+
const until = c.req.query("until") ?? new Date(now).toISOString();
|
|
4054
|
+
const bucketCount = parseInt(c.req.query("buckets") ?? "60", 10);
|
|
4055
|
+
const points = await metricsStore.query({ name, since, until });
|
|
4056
|
+
const sinceMs = new Date(since).getTime();
|
|
4057
|
+
const untilMs = new Date(until).getTime();
|
|
4058
|
+
const intervalMs = (untilMs - sinceMs) / bucketCount;
|
|
4059
|
+
const buckets = [];
|
|
4060
|
+
for (let i = 0; i < bucketCount; i++) {
|
|
4061
|
+
const bucketStart = sinceMs + i * intervalMs;
|
|
4062
|
+
const bucketEnd = bucketStart + intervalMs;
|
|
4063
|
+
const bucketStartISO = new Date(bucketStart).toISOString();
|
|
4064
|
+
const bucketEndISO = new Date(bucketEnd).toISOString();
|
|
4065
|
+
const inBucket = points.filter((p) => {
|
|
4066
|
+
const t = p.timestamp;
|
|
4067
|
+
return t >= bucketStartISO && (i === bucketCount - 1 ? t <= bucketEndISO : t < bucketEndISO);
|
|
4068
|
+
});
|
|
4069
|
+
if (inBucket.length === 0) {
|
|
4070
|
+
buckets.push({ time: bucketStartISO, count: 0, avg: 0, min: 0, max: 0 });
|
|
4071
|
+
}
|
|
4072
|
+
else {
|
|
4073
|
+
const values = inBucket.map((p) => p.value);
|
|
4074
|
+
const sum = values.reduce((a, v) => a + v, 0);
|
|
4075
|
+
buckets.push({
|
|
4076
|
+
time: bucketStartISO,
|
|
4077
|
+
count: inBucket.length,
|
|
4078
|
+
avg: sum / inBucket.length,
|
|
4079
|
+
min: Math.min(...values),
|
|
4080
|
+
max: Math.max(...values),
|
|
4081
|
+
});
|
|
4082
|
+
}
|
|
4083
|
+
}
|
|
4084
|
+
return c.json({ name, since, until, buckets, interval: intervalMs });
|
|
4085
|
+
});
|
|
4086
|
+
// Export raw metric points as JSON or CSV.
|
|
4087
|
+
app.get("/api/metrics/export", async (c) => {
|
|
4088
|
+
const now = Date.now();
|
|
4089
|
+
const since = c.req.query("since") ?? new Date(now - 24 * 60 * 60 * 1000).toISOString();
|
|
4090
|
+
const until = c.req.query("until") ?? new Date(now).toISOString();
|
|
4091
|
+
const name = c.req.query("name") || undefined;
|
|
4092
|
+
const format = c.req.query("format") ?? "json";
|
|
4093
|
+
const points = await metricsStore.query({ name, since, until });
|
|
4094
|
+
if (format === "csv") {
|
|
4095
|
+
const header = "timestamp,name,value,unit,tags";
|
|
4096
|
+
const rows = points.map((p) => {
|
|
4097
|
+
const tags = p.tags ? JSON.stringify(p.tags).replace(/"/g, '""') : "";
|
|
4098
|
+
return `${p.timestamp},${p.name},${p.value},${p.unit ?? ""},\"${tags}\"`;
|
|
4099
|
+
});
|
|
4100
|
+
const csv = [header, ...rows].join("\n");
|
|
4101
|
+
c.header("Content-Type", "text/csv");
|
|
4102
|
+
c.header("Content-Disposition", 'attachment; filename="metrics-export.csv"');
|
|
4103
|
+
return c.body(csv);
|
|
4104
|
+
}
|
|
4105
|
+
return c.json({ points, count: points.length });
|
|
4106
|
+
});
|
|
3201
4107
|
// Evaluate alert thresholds and return any fired alerts.
|
|
3202
4108
|
app.post("/api/metrics/alerts/evaluate", async (c) => {
|
|
3203
4109
|
const alerts = await evaluateAlerts(metricsStore);
|
|
@@ -3233,7 +4139,7 @@ app.get("/api/cache", (c) => {
|
|
|
3233
4139
|
});
|
|
3234
4140
|
// --- Help page routes (no auth — knowledge exchange for other AIs/humans) ---
|
|
3235
4141
|
app.get("/help", async (c) => {
|
|
3236
|
-
const html = await serveHtmlTemplate(join(
|
|
4142
|
+
const html = await serveHtmlTemplate(join(UI_DIR, "help.html"));
|
|
3237
4143
|
return c.html(html);
|
|
3238
4144
|
});
|
|
3239
4145
|
app.get("/api/help/context", async (c) => {
|
|
@@ -3276,33 +4182,33 @@ app.get("/api/help/context", async (c) => {
|
|
|
3276
4182
|
// --- Ops dashboard routes (posture-gated: board level) ---
|
|
3277
4183
|
// Board-level pages — only assembled when user has shown intent for full visibility
|
|
3278
4184
|
app.get("/observatory", requireSurface("pages"), async (c) => {
|
|
3279
|
-
const html = await serveHtmlTemplate(join(
|
|
4185
|
+
const html = await serveHtmlTemplate(join(UI_DIR, "observatory.html"));
|
|
3280
4186
|
return c.html(html);
|
|
3281
4187
|
});
|
|
3282
4188
|
app.get("/ops", requireSurface("pages"), async (c) => {
|
|
3283
|
-
const html = await serveHtmlTemplate(join(
|
|
4189
|
+
const html = await serveHtmlTemplate(join(UI_DIR, "ops.html"));
|
|
3284
4190
|
return c.html(html);
|
|
3285
4191
|
});
|
|
3286
4192
|
app.get("/board", requireSurface("pages"), async (c) => {
|
|
3287
|
-
const html = await serveHtmlTemplate(join(
|
|
4193
|
+
const html = await serveHtmlTemplate(join(UI_DIR, "board.html"));
|
|
3288
4194
|
return c.html(html);
|
|
3289
4195
|
});
|
|
3290
4196
|
app.get("/library", requireSurface("pages"), async (c) => {
|
|
3291
|
-
const html = await serveHtmlTemplate(join(
|
|
4197
|
+
const html = await serveHtmlTemplate(join(UI_DIR, "library.html"));
|
|
3292
4198
|
return c.html(html);
|
|
3293
4199
|
});
|
|
3294
4200
|
app.get("/browser", requireSurface("pages"), async (c) => {
|
|
3295
|
-
const html = await serveHtmlTemplate(join(
|
|
4201
|
+
const html = await serveHtmlTemplate(join(UI_DIR, "browser.html"));
|
|
3296
4202
|
return c.html(html);
|
|
3297
4203
|
});
|
|
3298
4204
|
// Registry is always available — it's the entry point
|
|
3299
4205
|
app.get("/registry", async (c) => {
|
|
3300
|
-
const html = await serveHtmlTemplate(join(
|
|
4206
|
+
const html = await serveHtmlTemplate(join(UI_DIR, "registry.html"));
|
|
3301
4207
|
return c.html(html);
|
|
3302
4208
|
});
|
|
3303
4209
|
// Serve roadmap.html (strategic roadmap & rearview)
|
|
3304
4210
|
app.get("/roadmap", async (c) => {
|
|
3305
|
-
const html = await serveHtmlTemplate(join(
|
|
4211
|
+
const html = await serveHtmlTemplate(join(UI_DIR, "roadmap.html"));
|
|
3306
4212
|
return c.html(html);
|
|
3307
4213
|
});
|
|
3308
4214
|
// Roadmap API — parse brain/operations/roadmap.yaml and return as JSON
|
|
@@ -3317,12 +4223,17 @@ app.get("/api/roadmap", async (c) => {
|
|
|
3317
4223
|
}
|
|
3318
4224
|
});
|
|
3319
4225
|
// Roadmap rearview API — recent git commits grouped by hour
|
|
4226
|
+
// Git is an optional signal source — returns empty when unavailable.
|
|
3320
4227
|
app.get("/api/roadmap/recent", async (c) => {
|
|
3321
4228
|
const hours = parseInt(c.req.query("hours") || "24", 10);
|
|
3322
4229
|
if (isNaN(hours) || hours < 1 || hours > 168) {
|
|
3323
4230
|
return c.json({ error: "hours must be between 1 and 168" }, 400);
|
|
3324
4231
|
}
|
|
3325
4232
|
try {
|
|
4233
|
+
const { gitAvailable } = await import("./utils/git.js");
|
|
4234
|
+
if (!gitAvailable()) {
|
|
4235
|
+
return c.json({ commits: [], groups: [], hours, total: 0 });
|
|
4236
|
+
}
|
|
3326
4237
|
const { execSync } = await import("child_process");
|
|
3327
4238
|
const since = new Date(Date.now() - hours * 60 * 60 * 1000).toISOString();
|
|
3328
4239
|
const raw = execSync(`git log --after="${since}" --format="%H||%an||%ai||%s" --no-merges`, { cwd: process.cwd(), encoding: "utf-8", timeout: 10000 }).trim();
|
|
@@ -3364,7 +4275,7 @@ app.get("/api/roadmap/recent", async (c) => {
|
|
|
3364
4275
|
// Serve personal.html (placeholder — instances populate this)
|
|
3365
4276
|
app.get("/personal", async (c) => {
|
|
3366
4277
|
try {
|
|
3367
|
-
const html = await serveHtmlTemplate(join(
|
|
4278
|
+
const html = await serveHtmlTemplate(join(UI_DIR, "personal.html"));
|
|
3368
4279
|
return c.html(html);
|
|
3369
4280
|
}
|
|
3370
4281
|
catch {
|
|
@@ -3374,7 +4285,7 @@ app.get("/personal", async (c) => {
|
|
|
3374
4285
|
// Serve life.html (placeholder — instances populate this)
|
|
3375
4286
|
app.get("/life", async (c) => {
|
|
3376
4287
|
try {
|
|
3377
|
-
const html = await serveHtmlTemplate(join(
|
|
4288
|
+
const html = await serveHtmlTemplate(join(UI_DIR, "life.html"));
|
|
3378
4289
|
return c.html(html);
|
|
3379
4290
|
}
|
|
3380
4291
|
catch {
|
|
@@ -3832,6 +4743,26 @@ app.post("/api/nerve/accept-update", async (c) => {
|
|
|
3832
4743
|
return c.json({ error: err.message }, 500);
|
|
3833
4744
|
}
|
|
3834
4745
|
});
|
|
4746
|
+
// Poll for new chat messages (phone → PC live feed)
|
|
4747
|
+
app.get("/api/chat/poll", async (c) => {
|
|
4748
|
+
const sessionId = c.req.query("sessionId") || c.req.header("x-session-id");
|
|
4749
|
+
if (!sessionId)
|
|
4750
|
+
return c.json({ error: "sessionId required" }, 400);
|
|
4751
|
+
const since = parseInt(c.req.query("since") || "0", 10);
|
|
4752
|
+
const cs = chatSessions.get(sessionId) || (chatSessions.size > 0 ? chatSessions.values().next().value : null);
|
|
4753
|
+
if (!cs)
|
|
4754
|
+
return c.json({ messages: [], total: 0 });
|
|
4755
|
+
const total = cs.history.length;
|
|
4756
|
+
if (since >= total)
|
|
4757
|
+
return c.json({ messages: [], total });
|
|
4758
|
+
const newMsgs = cs.history.slice(since).map((m, i) => ({
|
|
4759
|
+
index: since + i,
|
|
4760
|
+
role: m.role,
|
|
4761
|
+
content: typeof m.content === "string" ? m.content : JSON.stringify(m.content),
|
|
4762
|
+
source: m.source || "pc",
|
|
4763
|
+
}));
|
|
4764
|
+
return c.json({ messages: newMsgs, total });
|
|
4765
|
+
});
|
|
3835
4766
|
// Chat: streamed response (or learn command)
|
|
3836
4767
|
app.post("/api/chat", async (c) => {
|
|
3837
4768
|
const body = await c.req.json();
|
|
@@ -4164,33 +5095,30 @@ app.post("/api/chat", async (c) => {
|
|
|
4164
5095
|
}
|
|
4165
5096
|
}
|
|
4166
5097
|
}
|
|
4167
|
-
// Inject resolved skill content (reference skills auto-load by
|
|
5098
|
+
// Inject resolved skill content (reference skills auto-load by trigger match)
|
|
4168
5099
|
try {
|
|
4169
|
-
const
|
|
4170
|
-
if (
|
|
4171
|
-
const
|
|
4172
|
-
|
|
4173
|
-
|
|
4174
|
-
|
|
4175
|
-
|
|
4176
|
-
|
|
4177
|
-
|
|
4178
|
-
|
|
4179
|
-
|
|
4180
|
-
const content = await readBrainFile(join(process.cwd(), refPath));
|
|
4181
|
-
refContents.push(`--- ${refPath} ---\n${content}\n--- end ${refPath} ---`);
|
|
4182
|
-
}
|
|
4183
|
-
catch { /* skip missing files */ }
|
|
5100
|
+
const matched = await _skillRegistry.findByTrigger(chatMessage);
|
|
5101
|
+
if (matched) {
|
|
5102
|
+
const body = await _skillRegistry.getContent(matched.id);
|
|
5103
|
+
if (body) {
|
|
5104
|
+
// Load files referenced by the skill (brain/ paths)
|
|
5105
|
+
const refPaths = body.match(/(?:brain|docs)\/[\w\-\/]+\.\w+/g) ?? [];
|
|
5106
|
+
const refContents = [];
|
|
5107
|
+
for (const refPath of refPaths) {
|
|
5108
|
+
try {
|
|
5109
|
+
const content = await readBrainFile(join(process.cwd(), refPath));
|
|
5110
|
+
refContents.push(`--- ${refPath} ---\n${content}\n--- end ${refPath} ---`);
|
|
4184
5111
|
}
|
|
4185
|
-
|
|
4186
|
-
`--- Skill: ${res.skill.meta.name} (${res.reason}, confidence ${res.confidence.toFixed(2)}) ---`,
|
|
4187
|
-
body,
|
|
4188
|
-
...refContents,
|
|
4189
|
-
`--- end skill ---`,
|
|
4190
|
-
].join("\n");
|
|
4191
|
-
ctx.messages.splice(1, 0, { role: "system", content: skillSection });
|
|
4192
|
-
logActivity({ source: "system", summary: `Loaded skill: ${res.skill.meta.name} (${res.reason})` });
|
|
5112
|
+
catch { /* skip missing files */ }
|
|
4193
5113
|
}
|
|
5114
|
+
const skillSection = [
|
|
5115
|
+
`--- Skill: ${matched.name} (${matched.type}) ---`,
|
|
5116
|
+
body,
|
|
5117
|
+
...refContents,
|
|
5118
|
+
`--- end skill ---`,
|
|
5119
|
+
].join("\n");
|
|
5120
|
+
ctx.messages.splice(1, 0, { role: "system", content: skillSection });
|
|
5121
|
+
logActivity({ source: "system", summary: `Loaded skill: ${matched.name} (${matched.type})` });
|
|
4194
5122
|
}
|
|
4195
5123
|
}
|
|
4196
5124
|
}
|
|
@@ -4545,7 +5473,55 @@ app.post("/api/chat", async (c) => {
|
|
|
4545
5473
|
const reqSignal = c.req.raw.signal;
|
|
4546
5474
|
return streamSSE(c, async (stream) => {
|
|
4547
5475
|
// Send metadata first so UI can show which model is responding
|
|
4548
|
-
await stream.writeSSE({ data: JSON.stringify({ meta: { provider: activeProvider, model: activeChatModel ?? (activeProvider === "ollama" ? "llama3.
|
|
5476
|
+
await stream.writeSSE({ data: JSON.stringify({ meta: { provider: activeProvider, model: activeChatModel ?? (activeProvider === "ollama" ? "llama3.2:3b" : "claude-sonnet-4") } }) });
|
|
5477
|
+
// --- Apply membrane: redact messages BEFORE they reach the LLM ---
|
|
5478
|
+
const membrane = getActiveMembrane();
|
|
5479
|
+
const redactedMessages = ctx.messages.map((msg) => {
|
|
5480
|
+
if (!membrane)
|
|
5481
|
+
return msg;
|
|
5482
|
+
const copy = { ...msg };
|
|
5483
|
+
if (typeof copy.content === "string") {
|
|
5484
|
+
copy.content = membrane.apply(copy.content);
|
|
5485
|
+
}
|
|
5486
|
+
else if (Array.isArray(copy.content)) {
|
|
5487
|
+
copy.content = copy.content.map((block) => {
|
|
5488
|
+
if (block.type === "text" && typeof block.text === "string") {
|
|
5489
|
+
return { ...block, text: membrane.apply(block.text) };
|
|
5490
|
+
}
|
|
5491
|
+
return block;
|
|
5492
|
+
});
|
|
5493
|
+
}
|
|
5494
|
+
return copy;
|
|
5495
|
+
});
|
|
5496
|
+
// --- Membrane view: emit what the LLM will see (redacted) ---
|
|
5497
|
+
try {
|
|
5498
|
+
const membraneView = [];
|
|
5499
|
+
for (const msg of redactedMessages) {
|
|
5500
|
+
const raw = typeof msg.content === "string" ? msg.content
|
|
5501
|
+
: Array.isArray(msg.content) ? msg.content.map((b) => b.text || b.type || "").join(" ") : "";
|
|
5502
|
+
if (!raw)
|
|
5503
|
+
continue;
|
|
5504
|
+
// Count redactions by counting placeholders (already redacted)
|
|
5505
|
+
const placeholders = raw.match(/<<[A-Z_]+_\d+>>|\[REDACTED:[^\]]+\]/g);
|
|
5506
|
+
membraneView.push({
|
|
5507
|
+
role: msg.role,
|
|
5508
|
+
preview: raw.slice(0, 300) + (raw.length > 300 ? "..." : ""),
|
|
5509
|
+
redactions: placeholders ? placeholders.length : 0,
|
|
5510
|
+
});
|
|
5511
|
+
}
|
|
5512
|
+
const totalRedactions = membraneView.reduce((sum, m) => sum + m.redactions, 0);
|
|
5513
|
+
await stream.writeSSE({ data: JSON.stringify({
|
|
5514
|
+
membrane: {
|
|
5515
|
+
messageCount: redactedMessages.length,
|
|
5516
|
+
totalRedactions,
|
|
5517
|
+
messages: membraneView,
|
|
5518
|
+
sealValues: membrane ? membrane.knownValues.map(v => v.value) : [],
|
|
5519
|
+
},
|
|
5520
|
+
}) });
|
|
5521
|
+
}
|
|
5522
|
+
catch (memErr) {
|
|
5523
|
+
log.warn("membrane view error", { error: String(memErr) });
|
|
5524
|
+
}
|
|
4549
5525
|
let fullResponse = "";
|
|
4550
5526
|
const savePartial = () => {
|
|
4551
5527
|
if (fullResponse) {
|
|
@@ -4571,16 +5547,17 @@ app.post("/api/chat", async (c) => {
|
|
|
4571
5547
|
const flushBuf2 = () => {
|
|
4572
5548
|
if (!tokenBuf2)
|
|
4573
5549
|
return;
|
|
5550
|
+
// Debug: trace rehydration
|
|
4574
5551
|
const rehydrated = rehydrateResponse(tokenBuf2);
|
|
5552
|
+
fullResponse += rehydrated;
|
|
4575
5553
|
tokenBuf2 = "";
|
|
4576
5554
|
stream.writeSSE({ data: JSON.stringify({ token: rehydrated }) }).catch(() => { });
|
|
4577
5555
|
};
|
|
4578
5556
|
stream_fn({
|
|
4579
|
-
messages:
|
|
5557
|
+
messages: redactedMessages,
|
|
4580
5558
|
model: activeChatModel,
|
|
4581
5559
|
signal: reqSignal,
|
|
4582
5560
|
onToken: (token) => {
|
|
4583
|
-
fullResponse += token;
|
|
4584
5561
|
tokenBuf2 += token;
|
|
4585
5562
|
// Hold if buffer ends with partial placeholder
|
|
4586
5563
|
const lastOpen = tokenBuf2.lastIndexOf("<<");
|
|
@@ -4588,7 +5565,7 @@ app.post("/api/chat", async (c) => {
|
|
|
4588
5565
|
return;
|
|
4589
5566
|
flushBuf2();
|
|
4590
5567
|
},
|
|
4591
|
-
onDone: () => {
|
|
5568
|
+
onDone: async () => {
|
|
4592
5569
|
flushBuf2(); // flush remainder
|
|
4593
5570
|
reqSignal?.removeEventListener("abort", onAbort);
|
|
4594
5571
|
// Process action blocks BEFORE sending done — ensures SSE events reach client before stream closes.
|
|
@@ -4640,20 +5617,23 @@ app.post("/api/chat", async (c) => {
|
|
|
4640
5617
|
}
|
|
4641
5618
|
spawnCount++;
|
|
4642
5619
|
agentLog.info(` Spawning: ${label}${(isVague || isWishList) ? " (grounded)" : ""}`);
|
|
4643
|
-
|
|
4644
|
-
|
|
4645
|
-
|
|
4646
|
-
|
|
4647
|
-
|
|
4648
|
-
|
|
4649
|
-
|
|
4650
|
-
|
|
5620
|
+
// Await task submission so we can send the real task ID to the client
|
|
5621
|
+
try {
|
|
5622
|
+
const task = await submitTask({
|
|
5623
|
+
label,
|
|
5624
|
+
prompt: finalPrompt,
|
|
5625
|
+
origin: "ai",
|
|
5626
|
+
sessionId,
|
|
5627
|
+
boardTaskId: req.taskId,
|
|
5628
|
+
});
|
|
5629
|
+
stream.writeSSE({ data: JSON.stringify({ agentSpawned: { label, taskId: task.id } }) }).catch(() => { });
|
|
4651
5630
|
logActivity({ source: "agent", summary: `AI-triggered agent: ${task.label}`, detail: `Task ${task.id}, PID ${task.pid}`, actionLabel: "PROMPTED", reason: "user chat triggered agent" });
|
|
4652
|
-
}
|
|
5631
|
+
}
|
|
5632
|
+
catch (err) {
|
|
4653
5633
|
agentLog.error(`Spawn failed for "${label}": ${err.message}`);
|
|
4654
5634
|
logActivity({ source: "agent", summary: `AI agent spawn failed: ${err.message}`, actionLabel: "PROMPTED", reason: "user chat triggered agent spawn failed" });
|
|
4655
5635
|
stream.writeSSE({ data: JSON.stringify({ agentError: { label, error: err.message } }) }).catch(() => { });
|
|
4656
|
-
}
|
|
5636
|
+
}
|
|
4657
5637
|
}
|
|
4658
5638
|
else {
|
|
4659
5639
|
agentLog.warn(`Parsed JSON but missing "prompt" field: ${jsonMatch[0].slice(0, 200)}`);
|
|
@@ -4819,12 +5799,16 @@ app.post("/api/chat", async (c) => {
|
|
|
4819
5799
|
}
|
|
4820
5800
|
else {
|
|
4821
5801
|
let errorMsg = err instanceof LLMError ? err.userMessage : (err.message || "Stream error");
|
|
5802
|
+
// Include raw detail for health drilldown — client shows this, not the friendly version
|
|
5803
|
+
const rawDetail = err instanceof LLMError
|
|
5804
|
+
? `${err.provider} ${err.statusCode || ""}: ${err.message}`.trim()
|
|
5805
|
+
: (err.message || "Stream error");
|
|
4822
5806
|
if (isPrivateMode() && /ECONNREFUSED|fetch failed|network|socket/i.test(errorMsg)) {
|
|
4823
5807
|
const health = await checkOllamaHealth();
|
|
4824
5808
|
if (!health.ok)
|
|
4825
5809
|
errorMsg += " — Check that Ollama is running. " + health.message;
|
|
4826
5810
|
}
|
|
4827
|
-
stream.writeSSE({ data: JSON.stringify({ error: errorMsg }) }).catch(() => { });
|
|
5811
|
+
stream.writeSSE({ data: JSON.stringify({ error: errorMsg, errorDetail: rawDetail }) }).catch(() => { });
|
|
4828
5812
|
}
|
|
4829
5813
|
resolve(); // Still resolve so stream closes
|
|
4830
5814
|
},
|
|
@@ -4954,9 +5938,9 @@ async function start(opts) {
|
|
|
4954
5938
|
// Install fetch guard before any routes or outbound calls
|
|
4955
5939
|
installFetchGuard();
|
|
4956
5940
|
// Initialize PrivacyMembrane for reversible redaction
|
|
4957
|
-
|
|
4958
|
-
await
|
|
4959
|
-
const membrane = new PrivacyMembrane(
|
|
5941
|
+
activeSensitiveRegistry = new SensitiveRegistry();
|
|
5942
|
+
await activeSensitiveRegistry.load(BRAIN_DIR);
|
|
5943
|
+
const membrane = new PrivacyMembrane(activeSensitiveRegistry);
|
|
4960
5944
|
setActiveMembrane(membrane);
|
|
4961
5945
|
// Run independent initialization in parallel: LLM cache, auth, and sidecars
|
|
4962
5946
|
const [, pairingCode, sidecarResults] = await Promise.all([
|
|
@@ -4979,6 +5963,33 @@ async function start(opts) {
|
|
|
4979
5963
|
]);
|
|
4980
5964
|
const code = pairingCode;
|
|
4981
5965
|
const [searchAvailable, ttsAvailable, sttAvailable] = sidecarResults;
|
|
5966
|
+
// Load paired mobile devices
|
|
5967
|
+
await loadPairedDevices();
|
|
5968
|
+
// Register with runcore.sh relay (fire-and-forget, non-blocking)
|
|
5969
|
+
(async () => {
|
|
5970
|
+
try {
|
|
5971
|
+
const { createHash } = await import("node:crypto");
|
|
5972
|
+
const instanceHash = createHash("sha256")
|
|
5973
|
+
.update(getInstanceName() + BRAIN_DIR)
|
|
5974
|
+
.digest("hex")
|
|
5975
|
+
.slice(0, 16);
|
|
5976
|
+
await fetch("https://runcore.sh/api/relay/register", {
|
|
5977
|
+
method: "POST",
|
|
5978
|
+
headers: { "Content-Type": "application/json" },
|
|
5979
|
+
body: JSON.stringify({
|
|
5980
|
+
instanceHash,
|
|
5981
|
+
displayName: getInstanceName(),
|
|
5982
|
+
}),
|
|
5983
|
+
signal: AbortSignal.timeout(10_000),
|
|
5984
|
+
});
|
|
5985
|
+
log.info("Registered with runcore.sh relay", { instanceHash });
|
|
5986
|
+
// Start polling relay for incoming phone messages
|
|
5987
|
+
startRelayPoll(instanceHash);
|
|
5988
|
+
}
|
|
5989
|
+
catch {
|
|
5990
|
+
log.debug("Relay registration skipped (offline or unreachable)");
|
|
5991
|
+
}
|
|
5992
|
+
})();
|
|
4982
5993
|
// Pre-warm notification queue from disk (encryption key is set by now)
|
|
4983
5994
|
const notifCount = await initNotifications();
|
|
4984
5995
|
if (notifCount > 0)
|
|
@@ -5030,12 +6041,13 @@ async function start(opts) {
|
|
|
5030
6041
|
// of skills/module init, so running them concurrently improves startup time (DASH-60).
|
|
5031
6042
|
// Note: initAgents() only creates directories — recovery runs after the runtime
|
|
5032
6043
|
// is ready so the monitor can skip runtime-managed tasks (DASH-82 fix).
|
|
5033
|
-
const [
|
|
5034
|
-
|
|
6044
|
+
const [, moduleRegistry, , runtime] = await Promise.all([
|
|
6045
|
+
_skillRegistry.refresh(),
|
|
5035
6046
|
Promise.resolve(createModuleRegistry(BRAIN_DIR)),
|
|
5036
6047
|
initAgents(),
|
|
5037
6048
|
createRuntime(),
|
|
5038
6049
|
]);
|
|
6050
|
+
const skillRegistry = _skillRegistry;
|
|
5039
6051
|
// Recover tasks from previous session AFTER runtime is initialized.
|
|
5040
6052
|
// The monitor checks the runtime registry to skip tasks that RuntimeManager
|
|
5041
6053
|
// already handles, preventing the double-recovery race (DASH-82).
|
|
@@ -5110,6 +6122,10 @@ async function start(opts) {
|
|
|
5110
6122
|
}
|
|
5111
6123
|
if (isTasksAvailable())
|
|
5112
6124
|
startTasksTimer();
|
|
6125
|
+
// Initialize plugin registry (authenticate + start all registered plugins)
|
|
6126
|
+
await initPlugins().catch((err) => {
|
|
6127
|
+
log.warn("Plugin init failed", { error: err instanceof Error ? err.message : String(err) });
|
|
6128
|
+
});
|
|
5113
6129
|
// Wire batch continuation: when all agents finish, commit + decide what's next (direct LLM, no HTTP)
|
|
5114
6130
|
setOnBatchComplete(async (sessionId, results) => {
|
|
5115
6131
|
try {
|
|
@@ -5279,9 +6295,9 @@ async function start(opts) {
|
|
|
5279
6295
|
log.info("Google: add GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET in vault to enable");
|
|
5280
6296
|
}
|
|
5281
6297
|
// Show skills registry status
|
|
5282
|
-
|
|
5283
|
-
const
|
|
5284
|
-
log.info(`Skills: ${
|
|
6298
|
+
{
|
|
6299
|
+
const allSkills = await skillRegistry.list();
|
|
6300
|
+
log.info(`Skills: ${allSkills.length} registered`);
|
|
5285
6301
|
}
|
|
5286
6302
|
// Show module registry status
|
|
5287
6303
|
if (moduleRegistry) {
|
|
@@ -5294,9 +6310,21 @@ async function start(opts) {
|
|
|
5294
6310
|
const taskCount = await queueProvider.getStore().count();
|
|
5295
6311
|
log.info(`Board: ${board.name} (local, ${taskCount} tasks)`);
|
|
5296
6312
|
}
|
|
6313
|
+
// Sync UI from CDN (non-blocking — falls back to bundled if offline)
|
|
6314
|
+
syncUi().then(({ source, revision }) => {
|
|
6315
|
+
if (source === "cdn") {
|
|
6316
|
+
UI_DIR = getUiPublicDir(PKG_ROOT);
|
|
6317
|
+
log.info(`UI synced from CDN: revision ${revision}`);
|
|
6318
|
+
}
|
|
6319
|
+
else {
|
|
6320
|
+
log.info(`UI source: ${source}${revision ? ` (revision ${revision})` : ""}`);
|
|
6321
|
+
}
|
|
6322
|
+
}).catch(() => { });
|
|
5297
6323
|
// Generate startup token for zero-friction local auth
|
|
5298
6324
|
const { randomBytes: rng } = await import("node:crypto");
|
|
5299
6325
|
startupToken = rng(32).toString("hex");
|
|
6326
|
+
// Warm up local model in background (non-blocking)
|
|
6327
|
+
import("./llm/ollama.js").then(({ warmupOllama }) => warmupOllama()).catch(() => { });
|
|
5300
6328
|
if (code) {
|
|
5301
6329
|
log.info(`First launch detected. Pairing code: ${code}`);
|
|
5302
6330
|
}
|
|
@@ -5319,6 +6347,7 @@ async function start(opts) {
|
|
|
5319
6347
|
const emailBrain = new Brain({
|
|
5320
6348
|
systemPrompt: [
|
|
5321
6349
|
`You are ${getInstanceName()}, a personal AI agent paired with ${name}. You run locally on ${name}'s machine.`,
|
|
6350
|
+
`Today is ${new Date().toLocaleDateString("en-US", { weekday: "long", year: "numeric", month: "long", day: "numeric" })}.`,
|
|
5322
6351
|
`You are responding to an email that was sent to you. This is a working email channel — you received this email and your reply will be sent back automatically via Gmail.`,
|
|
5323
6352
|
``,
|
|
5324
6353
|
`Your capabilities — USE THEM when the email requests action:`,
|
|
@@ -5385,12 +6414,15 @@ async function start(opts) {
|
|
|
5385
6414
|
});
|
|
5386
6415
|
log.info(`${getInstanceName()} email handler registered — emails with '${getInstanceName()}' in subject will be auto-replied`);
|
|
5387
6416
|
await new Promise((resolve) => {
|
|
5388
|
-
|
|
6417
|
+
function onListening(server) {
|
|
5389
6418
|
const addr = server.address();
|
|
5390
6419
|
if (typeof addr === "object" && addr) {
|
|
5391
6420
|
actualPort = addr.port;
|
|
5392
6421
|
}
|
|
5393
6422
|
log.info(`Listening on http://localhost:${actualPort}`);
|
|
6423
|
+
acquireLock(actualPort, getInstanceName());
|
|
6424
|
+
console.log(`\n ${getInstanceName()} is running:\n`);
|
|
6425
|
+
console.log(` → http://localhost:${actualPort}\n`);
|
|
5394
6426
|
// Show LAN IP for phone access
|
|
5395
6427
|
try {
|
|
5396
6428
|
import("node:os").then(({ networkInterfaces }) => {
|
|
@@ -5398,7 +6430,7 @@ async function start(opts) {
|
|
|
5398
6430
|
for (const name of Object.keys(nets)) {
|
|
5399
6431
|
for (const net of nets[name] ?? []) {
|
|
5400
6432
|
if (net.family === "IPv4" && !net.internal) {
|
|
5401
|
-
log
|
|
6433
|
+
console.log(` → http://${net.address}:${actualPort} (LAN)\n`);
|
|
5402
6434
|
}
|
|
5403
6435
|
}
|
|
5404
6436
|
}
|
|
@@ -5417,7 +6449,29 @@ async function start(opts) {
|
|
|
5417
6449
|
}
|
|
5418
6450
|
}
|
|
5419
6451
|
resolve();
|
|
5420
|
-
}
|
|
6452
|
+
}
|
|
6453
|
+
try {
|
|
6454
|
+
const server = serve({ fetch: app.fetch, port: PORT }, () => onListening(server));
|
|
6455
|
+
server.on("error", (err) => {
|
|
6456
|
+
if (err.code === "EADDRINUSE" && PORT !== 0) {
|
|
6457
|
+
log.warn(`Port ${PORT} in use, falling back to random port`);
|
|
6458
|
+
const fallback = serve({ fetch: app.fetch, port: 0 }, () => onListening(fallback));
|
|
6459
|
+
}
|
|
6460
|
+
else {
|
|
6461
|
+
throw err;
|
|
6462
|
+
}
|
|
6463
|
+
});
|
|
6464
|
+
}
|
|
6465
|
+
catch (err) {
|
|
6466
|
+
// If serve() throws synchronously (unlikely but safe)
|
|
6467
|
+
if (PORT !== 0) {
|
|
6468
|
+
log.warn(`Port ${PORT} failed, falling back to random port`);
|
|
6469
|
+
const fallback = serve({ fetch: app.fetch, port: 0 }, () => onListening(fallback));
|
|
6470
|
+
}
|
|
6471
|
+
else {
|
|
6472
|
+
throw err;
|
|
6473
|
+
}
|
|
6474
|
+
}
|
|
5421
6475
|
});
|
|
5422
6476
|
}
|
|
5423
6477
|
/** Returns the port the server is actually listening on (resolves port 0). */
|
|
@@ -5554,8 +6608,10 @@ async function gracefulShutdown(signal) {
|
|
|
5554
6608
|
stopSidecar();
|
|
5555
6609
|
await closeBrowser();
|
|
5556
6610
|
shutdownAgents();
|
|
6611
|
+
await shutdownPlugins().catch(() => { });
|
|
5557
6612
|
await shutdownLLMCache();
|
|
5558
6613
|
await shutdownTracing();
|
|
6614
|
+
releaseLock();
|
|
5559
6615
|
process.exit(0);
|
|
5560
6616
|
}
|
|
5561
6617
|
process.on("SIGINT", () => { gracefulShutdown("SIGINT"); });
|