@ouro.bot/cli 0.1.0-alpha.56 → 0.1.0-alpha.560
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 +127 -23
- package/RepairGuide.ouro/agent.json +5 -0
- package/RepairGuide.ouro/psyche/IDENTITY.md +19 -0
- package/RepairGuide.ouro/psyche/SOUL.md +55 -0
- package/RepairGuide.ouro/skills/diagnose-broken-remote.md +63 -0
- package/RepairGuide.ouro/skills/diagnose-stacked-typed-issues.md +35 -0
- package/RepairGuide.ouro/skills/diagnose-sync-blocked.md +54 -0
- package/RepairGuide.ouro/skills/diagnose-vault-expired.md +60 -0
- package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/agent.json +4 -2
- package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/SOUL.md +2 -2
- package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/the-serpent.md +1 -1
- package/changelog.json +3596 -0
- package/dist/arc/attention-types.js +8 -0
- package/dist/arc/cares.js +140 -0
- package/dist/arc/episodes.js +117 -0
- package/dist/arc/intentions.js +133 -0
- package/dist/arc/json-store.js +117 -0
- package/dist/arc/obligations.js +237 -0
- package/dist/arc/packets.js +193 -0
- package/dist/arc/presence.js +185 -0
- package/dist/arc/task-lifecycle.js +65 -0
- package/dist/heart/active-work.js +837 -26
- package/dist/heart/agent-entry.js +58 -3
- package/dist/heart/attachments/image-normalize.js +194 -0
- package/dist/heart/attachments/materialize.js +97 -0
- package/dist/heart/attachments/originals.js +88 -0
- package/dist/heart/attachments/render.js +29 -0
- package/dist/heart/attachments/sources/adapter.js +2 -0
- package/dist/heart/attachments/sources/bluebubbles.js +156 -0
- package/dist/heart/attachments/sources/cli-local-file.js +78 -0
- package/dist/heart/attachments/sources/index.js +16 -0
- package/dist/heart/attachments/store.js +103 -0
- package/dist/heart/attachments/types.js +93 -0
- package/dist/heart/auth/auth-flow.js +479 -0
- package/dist/heart/background-operations.js +281 -0
- package/dist/heart/bundle-state.js +168 -0
- package/dist/heart/commitments.js +111 -0
- package/dist/heart/config-registry.js +322 -0
- package/dist/heart/config.js +114 -118
- package/dist/heart/core.js +913 -246
- package/dist/heart/cross-chat-delivery.js +3 -18
- package/dist/heart/daemon/agent-config-check.js +419 -0
- package/dist/heart/daemon/agent-discovery.js +102 -3
- package/dist/heart/daemon/agent-service.js +522 -0
- package/dist/heart/daemon/agentic-repair.js +547 -0
- package/dist/heart/daemon/bluebubbles-health-diagnostics.js +122 -0
- package/dist/heart/daemon/boot-sync-probe.js +197 -0
- package/dist/heart/daemon/cadence.js +70 -0
- package/dist/heart/daemon/cli-defaults.js +776 -0
- package/dist/heart/daemon/cli-exec.js +7457 -0
- package/dist/heart/daemon/cli-help.js +498 -0
- package/dist/heart/daemon/cli-parse.js +1592 -0
- package/dist/heart/daemon/cli-render-doctor.js +57 -0
- package/dist/heart/daemon/cli-render.js +763 -0
- package/dist/heart/daemon/cli-types.js +8 -0
- package/dist/heart/daemon/connect-bay.js +323 -0
- package/dist/heart/daemon/daemon-cli.js +29 -1698
- package/dist/heart/daemon/daemon-entry.js +387 -2
- package/dist/heart/daemon/daemon-health.js +176 -0
- package/dist/heart/daemon/daemon-rollup.js +57 -0
- package/dist/heart/daemon/daemon-runtime-sync.js +88 -13
- package/dist/heart/daemon/daemon-tombstone.js +236 -0
- package/dist/heart/daemon/daemon.js +796 -71
- package/dist/heart/daemon/dns-workflow.js +394 -0
- package/dist/heart/daemon/doctor-types.js +8 -0
- package/dist/heart/daemon/doctor.js +826 -0
- package/dist/heart/daemon/health-monitor.js +122 -1
- package/dist/heart/daemon/hooks/agent-config-v2.js +33 -0
- package/dist/heart/daemon/hooks/bundle-meta.js +115 -1
- package/dist/heart/daemon/http-health-probe.js +80 -0
- package/dist/heart/daemon/human-command-screens.js +234 -0
- package/dist/heart/daemon/human-readiness.js +114 -0
- package/dist/heart/daemon/inner-status.js +89 -0
- package/dist/heart/daemon/interactive-repair.js +394 -0
- package/dist/heart/daemon/launchd.js +37 -8
- package/dist/heart/daemon/log-tailer.js +82 -12
- package/dist/heart/daemon/logs-prune.js +110 -0
- package/dist/heart/daemon/mcp-canary.js +297 -0
- package/dist/heart/daemon/message-router.js +2 -2
- package/dist/heart/daemon/os-cron-deps.js +135 -0
- package/dist/heart/daemon/os-cron.js +14 -12
- package/dist/heart/daemon/ouro-bot-entry.js +4 -2
- package/dist/heart/daemon/ouro-entry.js +3 -1
- package/dist/heart/daemon/process-manager.js +375 -33
- package/dist/heart/daemon/provider-discovery.js +137 -0
- package/dist/heart/daemon/provider-ping-progress.js +83 -0
- package/dist/heart/daemon/pulse.js +475 -0
- package/dist/heart/daemon/readiness-repair.js +365 -0
- package/dist/heart/daemon/run-hooks.js +2 -0
- package/dist/heart/daemon/runtime-logging.js +67 -16
- package/dist/heart/daemon/runtime-metadata.js +3 -31
- package/dist/heart/daemon/safe-mode.js +161 -0
- package/dist/heart/daemon/sense-manager.js +389 -38
- package/dist/heart/daemon/session-id-resolver.js +131 -0
- package/dist/heart/daemon/skill-management-installer.js +94 -0
- package/dist/heart/daemon/socket-client.js +158 -11
- package/dist/heart/daemon/stale-bundle-prune.js +96 -0
- package/dist/heart/daemon/startup-tui.js +330 -0
- package/dist/heart/daemon/task-scheduler.js +3 -25
- package/dist/heart/daemon/terminal-ui.js +499 -0
- package/dist/heart/daemon/thoughts.js +162 -17
- package/dist/heart/daemon/up-progress.js +366 -0
- package/dist/heart/daemon/vault-items.js +56 -0
- package/dist/heart/delegation.js +1 -1
- package/dist/heart/habits/habit-migration.js +189 -0
- package/dist/heart/habits/habit-parser.js +140 -0
- package/dist/heart/habits/habit-runtime-state.js +100 -0
- package/dist/heart/habits/habit-scheduler.js +372 -0
- package/dist/heart/{daemon → hatch}/hatch-flow.js +32 -56
- package/dist/heart/{daemon → hatch}/hatch-specialist.js +6 -8
- package/dist/heart/{daemon → hatch}/specialist-prompt.js +12 -9
- package/dist/heart/{daemon → hatch}/specialist-tools.js +35 -12
- package/dist/heart/identity.js +203 -57
- package/dist/heart/kept-notes.js +357 -0
- package/dist/heart/kicks.js +1 -1
- package/dist/heart/machine-identity.js +161 -0
- package/dist/heart/mail-import-discovery.js +353 -0
- package/dist/heart/mailbox/mailbox-http-hooks.js +66 -0
- package/dist/heart/mailbox/mailbox-http-response.js +7 -0
- package/dist/heart/mailbox/mailbox-http-routes.js +246 -0
- package/dist/heart/mailbox/mailbox-http-static.js +103 -0
- package/dist/heart/mailbox/mailbox-http-transport.js +116 -0
- package/dist/heart/mailbox/mailbox-http.js +99 -0
- package/dist/heart/mailbox/mailbox-read.js +31 -0
- package/dist/heart/mailbox/mailbox-types.js +27 -0
- package/dist/heart/mailbox/mailbox-view.js +195 -0
- package/dist/heart/mailbox/readers/agent-machine.js +382 -0
- package/dist/heart/mailbox/readers/continuity-readers.js +338 -0
- package/dist/heart/mailbox/readers/mail.js +362 -0
- package/dist/heart/mailbox/readers/runtime-readers.js +651 -0
- package/dist/heart/mailbox/readers/sessions.js +232 -0
- package/dist/heart/mailbox/readers/shared.js +111 -0
- package/dist/heart/mcp/mcp-server.js +683 -0
- package/dist/heart/migrate-config.js +100 -0
- package/dist/heart/model-capabilities.js +19 -0
- package/dist/heart/platform.js +81 -0
- package/dist/heart/provider-attempt.js +134 -0
- package/dist/heart/provider-binding-resolver.js +267 -0
- package/dist/heart/provider-credentials.js +425 -0
- package/dist/heart/provider-failover.js +301 -0
- package/dist/heart/provider-models.js +81 -0
- package/dist/heart/provider-ping.js +262 -0
- package/dist/heart/provider-readiness-cache.js +40 -0
- package/dist/heart/provider-visibility.js +188 -0
- package/dist/heart/providers/anthropic-token.js +131 -0
- package/dist/heart/providers/anthropic.js +139 -52
- package/dist/heart/providers/azure.js +97 -13
- package/dist/heart/providers/error-classification.js +127 -0
- package/dist/heart/providers/github-copilot.js +145 -0
- package/dist/heart/providers/minimax-vlm.js +189 -0
- package/dist/heart/providers/minimax.js +26 -8
- package/dist/heart/providers/openai-codex.js +55 -40
- package/dist/heart/runtime-capability-check.js +170 -0
- package/dist/heart/runtime-credentials.js +367 -0
- package/dist/heart/runtime-cwd.js +87 -0
- package/dist/heart/sense-truth.js +13 -4
- package/dist/heart/session-activity.js +43 -22
- package/dist/heart/session-events.js +1149 -0
- package/dist/heart/session-playback-cli-main.js +5 -0
- package/dist/heart/session-playback-cli.js +36 -0
- package/dist/heart/session-playback.js +231 -0
- package/dist/heart/session-stats-cli-main.js +5 -0
- package/dist/heart/session-stats.js +182 -0
- package/dist/heart/session-transcript.js +243 -0
- package/dist/heart/start-of-turn-packet.js +345 -0
- package/dist/heart/streaming.js +44 -27
- package/dist/heart/sync-classification.js +176 -0
- package/dist/heart/sync.js +449 -0
- package/dist/heart/target-resolution.js +9 -5
- package/dist/heart/tempo.js +93 -0
- package/dist/heart/temporal-view.js +41 -0
- package/dist/heart/timeouts.js +101 -0
- package/dist/heart/tool-activity-callbacks.js +59 -0
- package/dist/heart/tool-description.js +139 -0
- package/dist/heart/tool-friction.js +55 -0
- package/dist/heart/tool-loop.js +200 -0
- package/dist/heart/turn-context.js +389 -0
- package/dist/heart/{daemon → versioning}/ouro-bot-global-installer.js +6 -5
- package/dist/heart/{daemon → versioning}/ouro-bot-wrapper.js +1 -1
- package/dist/heart/versioning/ouro-path-installer.js +426 -0
- package/dist/heart/versioning/ouro-version-manager.js +295 -0
- package/dist/heart/{daemon → versioning}/staged-restart.js +40 -8
- package/dist/heart/{daemon → versioning}/update-checker.js +6 -1
- package/dist/heart/{daemon → versioning}/update-hooks.js +63 -59
- package/dist/mailbox-ui/assets/index-B-461hes.js +61 -0
- package/dist/mailbox-ui/assets/index-BPr5vNuM.css +1 -0
- package/dist/mailbox-ui/index.html +15 -0
- package/dist/mailroom/attention.js +167 -0
- package/dist/mailroom/autonomy.js +209 -0
- package/dist/mailroom/blob-store.js +674 -0
- package/dist/mailroom/body-cache.js +61 -0
- package/dist/mailroom/core.js +720 -0
- package/dist/mailroom/entry.js +160 -0
- package/dist/mailroom/file-store.js +430 -0
- package/dist/mailroom/mbox-import.js +383 -0
- package/dist/mailroom/outbound.js +380 -0
- package/dist/mailroom/policy.js +263 -0
- package/dist/mailroom/reader.js +233 -0
- package/dist/mailroom/search-cache.js +256 -0
- package/dist/mailroom/search-relevance.js +319 -0
- package/dist/mailroom/smtp-ingress.js +176 -0
- package/dist/mailroom/source-state.js +176 -0
- package/dist/mailroom/thread.js +109 -0
- package/dist/mailroom/travel-extract.js +89 -0
- package/dist/mind/bundle-manifest.js +7 -1
- package/dist/mind/context.js +165 -101
- package/dist/mind/diary-integrity.js +60 -0
- package/dist/mind/{memory.js → diary.js} +62 -75
- package/dist/mind/embedding-provider.js +60 -0
- package/dist/mind/file-state.js +179 -0
- package/dist/mind/friends/channel.js +39 -0
- package/dist/mind/friends/resolver.js +54 -2
- package/dist/mind/friends/store-file.js +39 -3
- package/dist/mind/friends/types.js +2 -2
- package/dist/mind/journal-index.js +161 -0
- package/dist/mind/note-search.js +268 -0
- package/dist/mind/obligation-steering.js +221 -0
- package/dist/mind/pending.js +4 -0
- package/dist/mind/prompt-refresh.js +3 -2
- package/dist/mind/prompt.js +1011 -123
- package/dist/mind/provenance-trust.js +26 -0
- package/dist/mind/scrutiny.js +173 -0
- package/dist/nerves/cli-logging.js +7 -1
- package/dist/nerves/coverage/audit-rules.js +15 -6
- package/dist/nerves/coverage/audit.js +28 -2
- package/dist/nerves/coverage/cli.js +1 -1
- package/dist/nerves/coverage/contract.js +5 -5
- package/dist/nerves/coverage/file-completeness.js +129 -5
- package/dist/nerves/coverage/run-artifacts.js +1 -1
- package/dist/nerves/event-buffer.js +111 -0
- package/dist/nerves/index.js +224 -4
- package/dist/nerves/observation.js +20 -0
- package/dist/nerves/redact.js +79 -0
- package/dist/nerves/review/cli-main.js +5 -0
- package/dist/nerves/review/cli.js +156 -0
- package/dist/nerves/review/core.js +152 -0
- package/dist/nerves/runtime.js +5 -1
- package/dist/repertoire/ado-client.js +15 -56
- package/dist/repertoire/ado-semantic.js +11 -10
- package/dist/repertoire/api-client.js +97 -0
- package/dist/repertoire/bitwarden-store.js +963 -0
- package/dist/repertoire/bundle-templates.js +72 -0
- package/dist/repertoire/bw-installer.js +180 -0
- package/dist/repertoire/coding/codex-jsonl.js +64 -0
- package/dist/repertoire/coding/context-pack.js +330 -0
- package/dist/repertoire/coding/feedback.js +197 -30
- package/dist/repertoire/coding/manager.js +158 -9
- package/dist/repertoire/coding/spawner.js +55 -9
- package/dist/repertoire/coding/tools.js +170 -7
- package/dist/repertoire/commerce-errors.js +109 -0
- package/dist/repertoire/commerce-self-test.js +156 -0
- package/dist/repertoire/credential-access.js +178 -0
- package/dist/repertoire/duffel-client.js +185 -0
- package/dist/repertoire/github-client.js +14 -55
- package/dist/repertoire/graph-client.js +11 -52
- package/dist/repertoire/guardrails.js +396 -0
- package/dist/repertoire/mcp-client.js +295 -0
- package/dist/repertoire/mcp-manager.js +362 -0
- package/dist/repertoire/mcp-tools.js +63 -0
- package/dist/repertoire/shell-sessions.js +133 -0
- package/dist/repertoire/skills.js +15 -24
- package/dist/repertoire/stripe-client.js +131 -0
- package/dist/repertoire/tasks/board.js +31 -5
- package/dist/repertoire/tasks/fix.js +182 -0
- package/dist/repertoire/tasks/index.js +16 -4
- package/dist/repertoire/tasks/lifecycle.js +2 -2
- package/dist/repertoire/tasks/parser.js +3 -2
- package/dist/repertoire/tasks/scanner.js +194 -37
- package/dist/repertoire/tasks/transitions.js +16 -78
- package/dist/repertoire/tool-results.js +29 -0
- package/dist/repertoire/tools-attachments.js +317 -0
- package/dist/repertoire/tools-base.js +47 -1075
- package/dist/repertoire/tools-bluebubbles.js +1 -0
- package/dist/repertoire/tools-bridge.js +142 -0
- package/dist/repertoire/tools-bundle.js +984 -0
- package/dist/repertoire/tools-config.js +185 -0
- package/dist/repertoire/tools-continuity.js +248 -0
- package/dist/repertoire/tools-credential.js +381 -0
- package/dist/repertoire/tools-files.js +342 -0
- package/dist/repertoire/tools-flight.js +224 -0
- package/dist/repertoire/tools-flow.js +119 -0
- package/dist/repertoire/tools-github.js +1 -7
- package/dist/repertoire/tools-mail.js +1857 -0
- package/dist/repertoire/tools-notes.js +421 -0
- package/dist/repertoire/tools-session.js +750 -0
- package/dist/repertoire/tools-shell.js +120 -0
- package/dist/repertoire/tools-stripe.js +180 -0
- package/dist/repertoire/tools-surface.js +243 -0
- package/dist/repertoire/tools-teams.js +9 -39
- package/dist/repertoire/tools-travel.js +125 -0
- package/dist/repertoire/tools-trip.js +604 -0
- package/dist/repertoire/tools-user-profile.js +144 -0
- package/dist/repertoire/tools-vault.js +40 -0
- package/dist/repertoire/tools.js +108 -100
- package/dist/repertoire/travel-api-client.js +360 -0
- package/dist/repertoire/user-profile.js +131 -0
- package/dist/repertoire/vault-setup.js +246 -0
- package/dist/repertoire/vault-unlock.js +594 -0
- package/dist/scripts/claude-code-hook.js +41 -0
- package/dist/scripts/claude-code-stop-hook.js +47 -0
- package/dist/senses/attention-queue.js +116 -0
- package/dist/senses/bluebubbles/active-turns.js +216 -0
- package/dist/senses/bluebubbles/attachment-cache.js +53 -0
- package/dist/senses/bluebubbles/attachment-download.js +137 -0
- package/dist/senses/{bluebubbles-client.js → bluebubbles/client.js} +219 -18
- package/dist/senses/bluebubbles/entry.js +77 -0
- package/dist/senses/{bluebubbles-inbound-log.js → bluebubbles/inbound-log.js} +20 -3
- package/dist/senses/bluebubbles/index.js +2305 -0
- package/dist/senses/{bluebubbles-media.js → bluebubbles/media.js} +121 -70
- package/dist/senses/{bluebubbles-model.js → bluebubbles/model.js} +33 -12
- package/dist/senses/{bluebubbles-mutation-log.js → bluebubbles/mutation-log.js} +3 -3
- package/dist/senses/bluebubbles/processed-log.js +133 -0
- package/dist/senses/bluebubbles/replay.js +137 -0
- package/dist/senses/{bluebubbles-runtime-state.js → bluebubbles/runtime-state.js} +30 -2
- package/dist/senses/{bluebubbles-session-cleanup.js → bluebubbles/session-cleanup.js} +1 -1
- package/dist/senses/cli/bracketed-paste.js +82 -0
- package/dist/senses/cli/image-paste.js +287 -0
- package/dist/senses/cli/image-ref-navigation.js +75 -0
- package/dist/senses/cli/ink-app.js +156 -0
- package/dist/senses/cli/inline-diff.js +64 -0
- package/dist/senses/cli/input-keys.js +174 -0
- package/dist/senses/cli/kill-ring.js +86 -0
- package/dist/senses/cli/message-list.js +51 -0
- package/dist/senses/cli/ouro-tui.js +607 -0
- package/dist/senses/cli/spinner-imperative.js +135 -0
- package/dist/senses/cli/spinner.js +101 -0
- package/dist/senses/cli/status-line.js +60 -0
- package/dist/senses/cli/streaming-markdown.js +526 -0
- package/dist/senses/cli/tool-display.js +85 -0
- package/dist/senses/cli/tool-render.js +85 -0
- package/dist/senses/cli/tui-store.js +240 -0
- package/dist/senses/cli/virtual-list.js +35 -0
- package/dist/senses/cli-entry.js +60 -8
- package/dist/senses/cli-layout.js +187 -0
- package/dist/senses/cli.js +520 -209
- package/dist/senses/commands.js +66 -3
- package/dist/senses/habit-turn-message.js +108 -0
- package/dist/senses/inner-dialog-worker.js +175 -21
- package/dist/senses/inner-dialog.js +330 -27
- package/dist/senses/mail-entry.js +66 -0
- package/dist/senses/mail.js +379 -0
- package/dist/senses/pipeline.js +549 -181
- package/dist/senses/proactive-content-guard.js +51 -0
- package/dist/senses/shared-turn.js +248 -0
- package/dist/senses/surface-tool.js +68 -0
- package/dist/senses/teams-entry.js +60 -8
- package/dist/senses/teams.js +387 -98
- package/dist/senses/trust-gate.js +100 -5
- package/dist/senses/voice/elevenlabs.js +125 -0
- package/dist/senses/voice/index.js +22 -0
- package/dist/senses/voice/transcript.js +70 -0
- package/dist/senses/voice/turn.js +85 -0
- package/dist/senses/voice/types.js +2 -0
- package/dist/senses/voice/whisper.js +133 -0
- package/dist/senses/voice-entry.js +80 -0
- package/dist/trips/core.js +138 -0
- package/dist/trips/store.js +146 -0
- package/package.json +38 -7
- package/skills/agent-commerce.md +106 -0
- package/skills/browser-navigation.md +117 -0
- package/skills/commerce-setup-guide.md +116 -0
- package/skills/commerce-setup.md +84 -0
- package/skills/configure-dev-tools.md +101 -0
- package/skills/travel-planning.md +138 -0
- package/dist/heart/daemon/auth-flow.js +0 -351
- package/dist/heart/daemon/ouro-path-installer.js +0 -178
- package/dist/heart/daemon/subagent-installer.js +0 -166
- package/dist/heart/session-recall.js +0 -116
- package/dist/mind/associative-recall.js +0 -209
- package/dist/senses/bluebubbles-entry.js +0 -13
- package/dist/senses/bluebubbles.js +0 -1177
- package/dist/senses/debug-activity.js +0 -148
- package/subagents/README.md +0 -86
- package/subagents/work-doer.md +0 -237
- package/subagents/work-merger.md +0 -618
- package/subagents/work-planner.md +0 -390
- /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/basilisk.md +0 -0
- /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/jafar.md +0 -0
- /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/jormungandr.md +0 -0
- /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/kaa.md +0 -0
- /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/medusa.md +0 -0
- /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/monty.md +0 -0
- /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/nagini.md +0 -0
- /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/ouroboros.md +0 -0
- /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/python.md +0 -0
- /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/quetzalcoatl.md +0 -0
- /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/sir-hiss.md +0 -0
- /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/the-snake.md +0 -0
- /package/dist/heart/{daemon → hatch}/hatch-animation.js +0 -0
- /package/dist/heart/{daemon → hatch}/specialist-orchestrator.js +0 -0
- /package/dist/heart/{daemon → versioning}/ouro-uti.js +0 -0
- /package/dist/heart/{daemon → versioning}/wrapper-publish-guard.js +0 -0
|
@@ -0,0 +1,826 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* System health check ("ouro doctor") — runs all diagnostic categories
|
|
4
|
+
* and aggregates results into a structured DoctorResult.
|
|
5
|
+
*
|
|
6
|
+
* Each category checker is isolated: if one throws, it produces a single
|
|
7
|
+
* "fail" check and the remaining categories still run.
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.KNOWN_DOCTOR_CATEGORIES = void 0;
|
|
11
|
+
exports.checkCliPath = checkCliPath;
|
|
12
|
+
exports.checkDaemon = checkDaemon;
|
|
13
|
+
exports.checkAgents = checkAgents;
|
|
14
|
+
exports.checkSenses = checkSenses;
|
|
15
|
+
exports.checkHabits = checkHabits;
|
|
16
|
+
exports.checkSecurity = checkSecurity;
|
|
17
|
+
exports.checkTrips = checkTrips;
|
|
18
|
+
exports.checkMailroom = checkMailroom;
|
|
19
|
+
exports.checkFriends = checkFriends;
|
|
20
|
+
exports.checkDisk = checkDisk;
|
|
21
|
+
exports.checkLifecycle = checkLifecycle;
|
|
22
|
+
exports.runDoctorChecks = runDoctorChecks;
|
|
23
|
+
const runtime_1 = require("../../nerves/runtime");
|
|
24
|
+
const bluebubbles_health_diagnostics_1 = require("./bluebubbles-health-diagnostics");
|
|
25
|
+
const ouro_path_installer_1 = require("../versioning/ouro-path-installer");
|
|
26
|
+
const runtime_credentials_1 = require("../runtime-credentials");
|
|
27
|
+
const machine_identity_1 = require("../machine-identity");
|
|
28
|
+
const DEFAULT_BLUEBUBBLES_REQUEST_TIMEOUT_MS = 30_000;
|
|
29
|
+
// ── Category checkers ──
|
|
30
|
+
function checkCliPath(deps) {
|
|
31
|
+
const resolution = (0, ouro_path_installer_1.diagnoseOuroPath)({
|
|
32
|
+
homeDir: deps.homedir,
|
|
33
|
+
envPath: deps.envPath ?? "",
|
|
34
|
+
existsSync: deps.existsSync,
|
|
35
|
+
readFileSync: (p) => deps.readFileSync(p),
|
|
36
|
+
});
|
|
37
|
+
const status = resolution.status === "ok"
|
|
38
|
+
? "pass"
|
|
39
|
+
: resolution.status === "shadowed"
|
|
40
|
+
? "fail"
|
|
41
|
+
: "warn";
|
|
42
|
+
return {
|
|
43
|
+
name: "CLI",
|
|
44
|
+
checks: [{
|
|
45
|
+
label: "ouro PATH resolution",
|
|
46
|
+
status,
|
|
47
|
+
detail: resolution.remediation
|
|
48
|
+
? `${resolution.detail}; fix: ${resolution.remediation}`
|
|
49
|
+
: resolution.detail,
|
|
50
|
+
}],
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
async function checkDaemon(deps) {
|
|
54
|
+
const checks = [];
|
|
55
|
+
const socketExists = deps.existsSync(deps.socketPath);
|
|
56
|
+
checks.push({
|
|
57
|
+
label: "daemon socket exists",
|
|
58
|
+
status: socketExists ? "pass" : "fail",
|
|
59
|
+
detail: socketExists ? deps.socketPath : `not found at ${deps.socketPath}`,
|
|
60
|
+
});
|
|
61
|
+
if (socketExists) {
|
|
62
|
+
const alive = await deps.checkSocketAlive(deps.socketPath);
|
|
63
|
+
checks.push({
|
|
64
|
+
label: "daemon is responsive",
|
|
65
|
+
status: alive ? "pass" : "fail",
|
|
66
|
+
detail: alive ? "socket responded" : "socket exists but daemon unresponsive",
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
checks.push({
|
|
71
|
+
label: "daemon is responsive",
|
|
72
|
+
status: "fail",
|
|
73
|
+
detail: "skipped — socket missing",
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
return { name: "Daemon", checks };
|
|
77
|
+
}
|
|
78
|
+
/** Discover all *.ouro directories under bundlesRoot. */
|
|
79
|
+
function discoverAgents(deps) {
|
|
80
|
+
if (!deps.existsSync(deps.bundlesRoot))
|
|
81
|
+
return [];
|
|
82
|
+
return deps.readdirSync(deps.bundlesRoot).filter((name) => name.endsWith(".ouro"));
|
|
83
|
+
}
|
|
84
|
+
function asRecord(value) {
|
|
85
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
86
|
+
? value
|
|
87
|
+
: null;
|
|
88
|
+
}
|
|
89
|
+
function textField(record, key) {
|
|
90
|
+
const value = record?.[key];
|
|
91
|
+
return typeof value === "string" ? value.trim() : "";
|
|
92
|
+
}
|
|
93
|
+
function numberField(record, key, fallback) {
|
|
94
|
+
const value = record?.[key];
|
|
95
|
+
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
96
|
+
}
|
|
97
|
+
function hasStringRecordValue(value) {
|
|
98
|
+
const record = asRecord(value);
|
|
99
|
+
return !!record && Object.values(record).some((entry) => typeof entry === "string" && entry.trim().length > 0);
|
|
100
|
+
}
|
|
101
|
+
function mailAutonomyDetail(mailroom) {
|
|
102
|
+
const policy = asRecord(mailroom?.autonomousSendPolicy);
|
|
103
|
+
const autonomy = policy?.enabled === true ? "autonomy enabled" : "autonomy disabled";
|
|
104
|
+
const killSwitch = policy?.killSwitch === true ? "kill switch on" : "kill switch off";
|
|
105
|
+
return `${autonomy}; ${killSwitch}`;
|
|
106
|
+
}
|
|
107
|
+
const SENSITIVE_CONFIG_KEYS = ["apiKey", "token", "secret", "password"];
|
|
108
|
+
function credentialKeyLeaks(raw) {
|
|
109
|
+
return SENSITIVE_CONFIG_KEYS.filter((key) => raw.includes(`"${key}"`));
|
|
110
|
+
}
|
|
111
|
+
function checkAgents(deps) {
|
|
112
|
+
const checks = [];
|
|
113
|
+
if (!deps.existsSync(deps.bundlesRoot)) {
|
|
114
|
+
checks.push({ label: "bundles directory", status: "fail", detail: `${deps.bundlesRoot} not found` });
|
|
115
|
+
return { name: "Agents", checks };
|
|
116
|
+
}
|
|
117
|
+
const agents = discoverAgents(deps);
|
|
118
|
+
if (agents.length === 0) {
|
|
119
|
+
checks.push({ label: "agent bundles", status: "warn", detail: "no *.ouro bundles found" });
|
|
120
|
+
return { name: "Agents", checks };
|
|
121
|
+
}
|
|
122
|
+
for (const agentDir of agents) {
|
|
123
|
+
const agentPath = `${deps.bundlesRoot}/${agentDir}`;
|
|
124
|
+
const configPath = `${agentPath}/agent.json`;
|
|
125
|
+
if (!deps.existsSync(configPath)) {
|
|
126
|
+
checks.push({ label: `${agentDir}/agent.json`, status: "fail", detail: "missing" });
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
let config;
|
|
130
|
+
try {
|
|
131
|
+
config = JSON.parse(deps.readFileSync(configPath));
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
checks.push({ label: `${agentDir}/agent.json`, status: "fail", detail: "unparseable JSON" });
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
const missing = [];
|
|
138
|
+
if (!config.version)
|
|
139
|
+
missing.push("version");
|
|
140
|
+
if (!config.humanFacing || typeof config.humanFacing !== "object") {
|
|
141
|
+
missing.push("humanFacing");
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
const hf = config.humanFacing;
|
|
145
|
+
if (!hf.provider)
|
|
146
|
+
missing.push("humanFacing.provider");
|
|
147
|
+
if (!hf.model)
|
|
148
|
+
missing.push("humanFacing.model");
|
|
149
|
+
}
|
|
150
|
+
if (!config.agentFacing || typeof config.agentFacing !== "object") {
|
|
151
|
+
missing.push("agentFacing");
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
const af = config.agentFacing;
|
|
155
|
+
if (!af.provider)
|
|
156
|
+
missing.push("agentFacing.provider");
|
|
157
|
+
if (!af.model)
|
|
158
|
+
missing.push("agentFacing.model");
|
|
159
|
+
}
|
|
160
|
+
if (missing.length > 0) {
|
|
161
|
+
checks.push({ label: `${agentDir}/agent.json`, status: "warn", detail: `missing fields: ${missing.join(", ")}` });
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
checks.push({ label: `${agentDir}/agent.json`, status: "pass", detail: "valid" });
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return { name: "Agents", checks };
|
|
168
|
+
}
|
|
169
|
+
async function checkSenses(deps) {
|
|
170
|
+
const checks = [];
|
|
171
|
+
const agents = discoverAgents(deps);
|
|
172
|
+
for (const agentDir of agents) {
|
|
173
|
+
const agentName = agentDir.replace(/\.ouro$/, "");
|
|
174
|
+
const configPath = `${deps.bundlesRoot}/${agentDir}/agent.json`;
|
|
175
|
+
if (!deps.existsSync(configPath))
|
|
176
|
+
continue;
|
|
177
|
+
let config;
|
|
178
|
+
try {
|
|
179
|
+
config = JSON.parse(deps.readFileSync(configPath));
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
checks.push({ label: `${agentDir} senses`, status: "fail", detail: "agent.json unparseable" });
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
if (!config.senses || typeof config.senses !== "object") {
|
|
186
|
+
checks.push({ label: `${agentDir} senses`, status: "warn", detail: "no senses config block" });
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
const senses = config.senses;
|
|
190
|
+
const senseNames = ["cli", "teams", "bluebubbles", "mail"];
|
|
191
|
+
for (const sense of senseNames) {
|
|
192
|
+
if (!(sense in senses))
|
|
193
|
+
continue;
|
|
194
|
+
const entry = senses[sense];
|
|
195
|
+
if (!entry || typeof entry !== "object") {
|
|
196
|
+
checks.push({ label: `${agentDir} ${sense}`, status: "fail", detail: "malformed sense entry" });
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
const senseObj = entry;
|
|
200
|
+
if (typeof senseObj.enabled !== "boolean") {
|
|
201
|
+
checks.push({ label: `${agentDir} ${sense}`, status: "warn", detail: "missing enabled boolean" });
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
checks.push({
|
|
205
|
+
label: `${agentDir} ${sense}`,
|
|
206
|
+
status: "pass",
|
|
207
|
+
detail: senseObj.enabled ? "enabled" : "disabled",
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
if (sense === "bluebubbles" && senseObj.enabled === true) {
|
|
211
|
+
const machineId = (0, machine_identity_1.loadOrCreateMachineIdentity)({ homeDir: deps.homedir }).machineId;
|
|
212
|
+
const runtimeConfig = await (0, runtime_credentials_1.refreshMachineRuntimeCredentialConfig)(agentName, machineId, { preserveCachedOnFailure: true });
|
|
213
|
+
if (!runtimeConfig.ok) {
|
|
214
|
+
if (runtimeConfig.reason === "missing") {
|
|
215
|
+
checks.push({
|
|
216
|
+
label: `${agentDir} bluebubbles config`,
|
|
217
|
+
status: "pass",
|
|
218
|
+
detail: "not attached on this machine",
|
|
219
|
+
});
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
checks.push({
|
|
223
|
+
label: `${agentDir} bluebubbles config`,
|
|
224
|
+
status: "fail",
|
|
225
|
+
detail: `machine runtime config unavailable: ${runtimeConfig.error}`,
|
|
226
|
+
});
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
const bluebubbles = asRecord(runtimeConfig.config.bluebubbles);
|
|
230
|
+
const bluebubblesChannel = asRecord(runtimeConfig.config.bluebubblesChannel);
|
|
231
|
+
const serverUrl = textField(bluebubbles, "serverUrl");
|
|
232
|
+
const password = textField(bluebubbles, "password");
|
|
233
|
+
const missing = [];
|
|
234
|
+
if (!serverUrl)
|
|
235
|
+
missing.push("bluebubbles.serverUrl");
|
|
236
|
+
if (!password)
|
|
237
|
+
missing.push("bluebubbles.password");
|
|
238
|
+
if (missing.length > 0) {
|
|
239
|
+
checks.push({
|
|
240
|
+
label: `${agentDir} bluebubbles config`,
|
|
241
|
+
status: "fail",
|
|
242
|
+
detail: `missing ${missing.join("/")}`,
|
|
243
|
+
});
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
checks.push({
|
|
247
|
+
label: `${agentDir} bluebubbles config`,
|
|
248
|
+
status: "pass",
|
|
249
|
+
detail: serverUrl,
|
|
250
|
+
});
|
|
251
|
+
if (deps.fetchImpl) {
|
|
252
|
+
const probe = await (0, bluebubbles_health_diagnostics_1.probeBlueBubblesHealth)({
|
|
253
|
+
serverUrl,
|
|
254
|
+
password,
|
|
255
|
+
requestTimeoutMs: numberField(bluebubblesChannel, "requestTimeoutMs", DEFAULT_BLUEBUBBLES_REQUEST_TIMEOUT_MS),
|
|
256
|
+
fetchImpl: deps.fetchImpl,
|
|
257
|
+
});
|
|
258
|
+
checks.push({
|
|
259
|
+
label: `${agentDir} bluebubbles upstream`,
|
|
260
|
+
status: probe.ok ? "pass" : "fail",
|
|
261
|
+
detail: probe.detail,
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
if (sense === "mail" && senseObj.enabled === true) {
|
|
266
|
+
const runtimeConfig = await (0, runtime_credentials_1.refreshRuntimeCredentialConfig)(agentName, { preserveCachedOnFailure: true });
|
|
267
|
+
if (!runtimeConfig.ok) {
|
|
268
|
+
checks.push({
|
|
269
|
+
label: `${agentDir} mail config`,
|
|
270
|
+
status: "fail",
|
|
271
|
+
detail: `runtime config unavailable: ${runtimeConfig.error}`,
|
|
272
|
+
});
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
const mailroom = asRecord(runtimeConfig.config.mailroom);
|
|
276
|
+
const workSubstrate = asRecord(runtimeConfig.config.workSubstrate);
|
|
277
|
+
const mailboxAddress = textField(mailroom, "mailboxAddress");
|
|
278
|
+
const hosted = textField(workSubstrate, "mode") === "hosted";
|
|
279
|
+
const azureAccountUrl = textField(mailroom, "azureAccountUrl");
|
|
280
|
+
const azureContainer = textField(mailroom, "azureContainer") || "mailroom";
|
|
281
|
+
const missing = [];
|
|
282
|
+
if (!mailboxAddress)
|
|
283
|
+
missing.push("mailroom.mailboxAddress");
|
|
284
|
+
if (!hasStringRecordValue(mailroom?.privateKeys))
|
|
285
|
+
missing.push("mailroom.privateKeys");
|
|
286
|
+
if (hosted && !azureAccountUrl)
|
|
287
|
+
missing.push("mailroom.azureAccountUrl for hosted Blob reader");
|
|
288
|
+
if (missing.length > 0) {
|
|
289
|
+
checks.push({
|
|
290
|
+
label: `${agentDir} mail config`,
|
|
291
|
+
status: "fail",
|
|
292
|
+
detail: `missing ${missing.join("/")}`,
|
|
293
|
+
});
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
checks.push({
|
|
297
|
+
label: `${agentDir} mail config`,
|
|
298
|
+
status: "pass",
|
|
299
|
+
detail: [
|
|
300
|
+
mailboxAddress,
|
|
301
|
+
hosted ? `hosted azure-blob ${azureAccountUrl}/${azureContainer}` : "local file Mailroom",
|
|
302
|
+
mailAutonomyDetail(mailroom),
|
|
303
|
+
].join("; "),
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
if (checks.length === 0) {
|
|
309
|
+
checks.push({ label: "senses", status: "warn", detail: "no agents with senses config found" });
|
|
310
|
+
}
|
|
311
|
+
return { name: "Senses", checks };
|
|
312
|
+
}
|
|
313
|
+
function checkHabits(deps) {
|
|
314
|
+
const checks = [];
|
|
315
|
+
const agents = discoverAgents(deps);
|
|
316
|
+
for (const agentDir of agents) {
|
|
317
|
+
const agentName = agentDir.replace(/\.ouro$/, "");
|
|
318
|
+
const habitsDir = `${deps.bundlesRoot}/${agentDir}/habits`;
|
|
319
|
+
if (!deps.existsSync(habitsDir)) {
|
|
320
|
+
checks.push({ label: `${agentDir} habits dir`, status: "warn", detail: "no habits directory" });
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
checks.push({ label: `${agentDir} habits dir`, status: "pass", detail: habitsDir });
|
|
324
|
+
// Check for launchd plists on macOS
|
|
325
|
+
const launchAgentsDir = `${deps.homedir}/Library/LaunchAgents`;
|
|
326
|
+
if (deps.existsSync(launchAgentsDir)) {
|
|
327
|
+
const plists = deps.readdirSync(launchAgentsDir).filter((f) => f.startsWith(`bot.ouro.${agentName}.`) && f.endsWith(".plist"));
|
|
328
|
+
if (plists.length > 0) {
|
|
329
|
+
checks.push({ label: `${agentDir} launchd plists`, status: "pass", detail: `${plists.length} plist(s)` });
|
|
330
|
+
}
|
|
331
|
+
else {
|
|
332
|
+
checks.push({ label: `${agentDir} launchd plists`, status: "fail", detail: "no matching plists in LaunchAgents" });
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
if (checks.length === 0) {
|
|
337
|
+
checks.push({ label: "habits", status: "warn", detail: "no agents found" });
|
|
338
|
+
}
|
|
339
|
+
return { name: "Habits", checks };
|
|
340
|
+
}
|
|
341
|
+
function checkSecurity(deps) {
|
|
342
|
+
const checks = [];
|
|
343
|
+
const agents = discoverAgents(deps);
|
|
344
|
+
for (const agentDir of agents) {
|
|
345
|
+
// Check agent.json for leaked credential keys
|
|
346
|
+
const configPath = `${deps.bundlesRoot}/${agentDir}/agent.json`;
|
|
347
|
+
if (deps.existsSync(configPath)) {
|
|
348
|
+
try {
|
|
349
|
+
const raw = deps.readFileSync(configPath);
|
|
350
|
+
const found = credentialKeyLeaks(raw);
|
|
351
|
+
if (found.length > 0) {
|
|
352
|
+
checks.push({ label: `${agentDir} credential leak`, status: "warn", detail: `agent.json contains keys: ${found.join(", ")}` });
|
|
353
|
+
}
|
|
354
|
+
else {
|
|
355
|
+
checks.push({ label: `${agentDir} credential leak`, status: "pass", detail: "no credential keys in agent.json" });
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
catch {
|
|
359
|
+
checks.push({ label: `${agentDir} credential leak`, status: "fail", detail: "could not read agent.json" });
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
if (checks.length === 0) {
|
|
364
|
+
checks.push({ label: "security", status: "warn", detail: "no agents found" });
|
|
365
|
+
}
|
|
366
|
+
return { name: "Security", checks };
|
|
367
|
+
}
|
|
368
|
+
function checkTrips(deps) {
|
|
369
|
+
const checks = [];
|
|
370
|
+
const agents = discoverAgents(deps);
|
|
371
|
+
if (agents.length === 0) {
|
|
372
|
+
checks.push({ label: "trip ledger", status: "warn", detail: "no agent bundles found" });
|
|
373
|
+
return { name: "Trips", checks };
|
|
374
|
+
}
|
|
375
|
+
for (const agentDir of agents) {
|
|
376
|
+
const tripsRootPath = `${deps.bundlesRoot}/${agentDir}/state/trips`;
|
|
377
|
+
if (!deps.existsSync(tripsRootPath)) {
|
|
378
|
+
// Trip ledger is optional; absence is fine. Pass with a hint.
|
|
379
|
+
checks.push({ label: `${agentDir} trip ledger`, status: "pass", detail: "no ledger directory (no trips ensured yet)" });
|
|
380
|
+
continue;
|
|
381
|
+
}
|
|
382
|
+
const ledgerPath = `${tripsRootPath}/ledger.json`;
|
|
383
|
+
if (!deps.existsSync(ledgerPath)) {
|
|
384
|
+
checks.push({ label: `${agentDir} trip ledger`, status: "warn", detail: "state/trips/ exists but ledger.json missing — run trip_ensure_ledger" });
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
387
|
+
let raw;
|
|
388
|
+
/* v8 ignore start -- defensive: readFileSync failure after existsSync passes is a race-condition fallback @preserve */
|
|
389
|
+
try {
|
|
390
|
+
raw = deps.readFileSync(ledgerPath);
|
|
391
|
+
}
|
|
392
|
+
catch {
|
|
393
|
+
checks.push({ label: `${agentDir} trip ledger`, status: "fail", detail: "ledger.json could not be read" });
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
/* v8 ignore stop */
|
|
397
|
+
let parsed;
|
|
398
|
+
try {
|
|
399
|
+
parsed = JSON.parse(raw);
|
|
400
|
+
}
|
|
401
|
+
catch {
|
|
402
|
+
checks.push({ label: `${agentDir} trip ledger`, status: "fail", detail: "ledger.json is not valid JSON" });
|
|
403
|
+
continue;
|
|
404
|
+
}
|
|
405
|
+
const nestedLedger = parsed.ledger && typeof parsed.ledger === "object"
|
|
406
|
+
? parsed.ledger
|
|
407
|
+
: null;
|
|
408
|
+
const ledgerId = typeof parsed.ledgerId === "string"
|
|
409
|
+
? parsed.ledgerId
|
|
410
|
+
: typeof nestedLedger?.ledgerId === "string"
|
|
411
|
+
? nestedLedger.ledgerId
|
|
412
|
+
: null;
|
|
413
|
+
const hasPrivateKey = typeof parsed.privateKeyPem === "string" && parsed.privateKeyPem.includes("BEGIN");
|
|
414
|
+
if (!ledgerId) {
|
|
415
|
+
checks.push({ label: `${agentDir} trip ledger`, status: "warn", detail: "ledger.json missing ledgerId field" });
|
|
416
|
+
continue;
|
|
417
|
+
}
|
|
418
|
+
if (!hasPrivateKey) {
|
|
419
|
+
checks.push({ label: `${agentDir} trip ledger`, status: "fail", detail: `${ledgerId}: privateKeyPem missing — encrypted trip records cannot be read` });
|
|
420
|
+
continue;
|
|
421
|
+
}
|
|
422
|
+
let recordCount = 0;
|
|
423
|
+
const recordsDir = `${tripsRootPath}/records`;
|
|
424
|
+
/* v8 ignore start -- defensive: records dir presence and readdir error are filesystem-state branches not all exercised by tests; pluralization branch likewise depends on record count fixtures @preserve */
|
|
425
|
+
if (deps.existsSync(recordsDir)) {
|
|
426
|
+
try {
|
|
427
|
+
recordCount = deps.readdirSync(recordsDir).filter((name) => name.endsWith(".json")).length;
|
|
428
|
+
}
|
|
429
|
+
catch {
|
|
430
|
+
// ignore — the warn detail will still report 0 records
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
checks.push({
|
|
434
|
+
label: `${agentDir} trip ledger`,
|
|
435
|
+
status: "pass",
|
|
436
|
+
detail: `${ledgerId} (${recordCount} record${recordCount === 1 ? "" : "s"})`,
|
|
437
|
+
});
|
|
438
|
+
/* v8 ignore stop */
|
|
439
|
+
}
|
|
440
|
+
return { name: "Trips", checks };
|
|
441
|
+
}
|
|
442
|
+
function checkMailroom(deps) {
|
|
443
|
+
const checks = [];
|
|
444
|
+
const agents = discoverAgents(deps);
|
|
445
|
+
if (agents.length === 0) {
|
|
446
|
+
checks.push({ label: "mailroom", status: "warn", detail: "no agent bundles found" });
|
|
447
|
+
return { name: "Mailroom", checks };
|
|
448
|
+
}
|
|
449
|
+
for (const agentDir of agents) {
|
|
450
|
+
const mailroomRoot = `${deps.bundlesRoot}/${agentDir}/state/mailroom`;
|
|
451
|
+
if (!deps.existsSync(mailroomRoot)) {
|
|
452
|
+
checks.push({ label: `${agentDir} mailroom`, status: "pass", detail: "no mailroom directory (mail not connected)" });
|
|
453
|
+
continue;
|
|
454
|
+
}
|
|
455
|
+
const registryPath = `${mailroomRoot}/registry.json`;
|
|
456
|
+
if (!deps.existsSync(registryPath)) {
|
|
457
|
+
checks.push({ label: `${agentDir} mailroom`, status: "warn", detail: "state/mailroom/ exists but registry.json missing" });
|
|
458
|
+
continue;
|
|
459
|
+
}
|
|
460
|
+
let raw;
|
|
461
|
+
/* v8 ignore start -- defensive: readFileSync failure after existsSync passes is a race-condition fallback @preserve */
|
|
462
|
+
try {
|
|
463
|
+
raw = deps.readFileSync(registryPath);
|
|
464
|
+
}
|
|
465
|
+
catch {
|
|
466
|
+
checks.push({ label: `${agentDir} mailroom`, status: "fail", detail: "registry.json could not be read" });
|
|
467
|
+
continue;
|
|
468
|
+
}
|
|
469
|
+
/* v8 ignore stop */
|
|
470
|
+
let parsed;
|
|
471
|
+
try {
|
|
472
|
+
parsed = JSON.parse(raw);
|
|
473
|
+
}
|
|
474
|
+
catch {
|
|
475
|
+
checks.push({ label: `${agentDir} mailroom`, status: "fail", detail: "registry.json is not valid JSON" });
|
|
476
|
+
continue;
|
|
477
|
+
}
|
|
478
|
+
/* v8 ignore start -- defensive: registry shape is validated by mailroom code; non-array fallbacks are belt-and-suspenders @preserve */
|
|
479
|
+
const mailboxes = Array.isArray(parsed.mailboxes) ? parsed.mailboxes : null;
|
|
480
|
+
const sourceGrants = Array.isArray(parsed.sourceGrants) ? parsed.sourceGrants : [];
|
|
481
|
+
/* v8 ignore stop */
|
|
482
|
+
if (!mailboxes || mailboxes.length === 0) {
|
|
483
|
+
checks.push({ label: `${agentDir} mailroom`, status: "warn", detail: "registry.json has no mailboxes — provision via `ouro connect mail`" });
|
|
484
|
+
continue;
|
|
485
|
+
}
|
|
486
|
+
let messagesCount = 0;
|
|
487
|
+
const messagesDir = `${mailroomRoot}/messages`;
|
|
488
|
+
/* v8 ignore start -- defensive: messages-dir presence + readdir error + pluralization branches depend on filesystem-state fixtures not exhaustively covered @preserve */
|
|
489
|
+
if (deps.existsSync(messagesDir)) {
|
|
490
|
+
try {
|
|
491
|
+
messagesCount = deps.readdirSync(messagesDir).filter((name) => name.endsWith(".json")).length;
|
|
492
|
+
}
|
|
493
|
+
catch {
|
|
494
|
+
// ignore — pass detail just won't include the message count
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
checks.push({
|
|
498
|
+
label: `${agentDir} mailroom`,
|
|
499
|
+
status: "pass",
|
|
500
|
+
detail: `${mailboxes.length} mailbox${mailboxes.length === 1 ? "" : "es"}, ${sourceGrants.length} source grant${sourceGrants.length === 1 ? "" : "s"}, ${messagesCount} message${messagesCount === 1 ? "" : "s"}`,
|
|
501
|
+
});
|
|
502
|
+
/* v8 ignore stop */
|
|
503
|
+
}
|
|
504
|
+
return { name: "Mailroom", checks };
|
|
505
|
+
}
|
|
506
|
+
function checkFriends(deps) {
|
|
507
|
+
const checks = [];
|
|
508
|
+
const agents = discoverAgents(deps);
|
|
509
|
+
if (agents.length === 0) {
|
|
510
|
+
checks.push({ label: "friends", status: "warn", detail: "no agent bundles found" });
|
|
511
|
+
return { name: "Friends", checks };
|
|
512
|
+
}
|
|
513
|
+
for (const agentDir of agents) {
|
|
514
|
+
const friendsDir = `${deps.bundlesRoot}/${agentDir}/friends`;
|
|
515
|
+
if (!deps.existsSync(friendsDir)) {
|
|
516
|
+
checks.push({ label: `${agentDir} friends`, status: "pass", detail: "no friends directory (no friends recorded yet)" });
|
|
517
|
+
continue;
|
|
518
|
+
}
|
|
519
|
+
let entries;
|
|
520
|
+
/* v8 ignore start -- defensive: readdirSync failure after existsSync passes is a race-condition fallback @preserve */
|
|
521
|
+
try {
|
|
522
|
+
entries = deps.readdirSync(friendsDir).filter((name) => name.endsWith(".json"));
|
|
523
|
+
}
|
|
524
|
+
catch {
|
|
525
|
+
checks.push({ label: `${agentDir} friends`, status: "fail", detail: "friends directory could not be read" });
|
|
526
|
+
continue;
|
|
527
|
+
}
|
|
528
|
+
/* v8 ignore stop */
|
|
529
|
+
if (entries.length === 0) {
|
|
530
|
+
checks.push({ label: `${agentDir} friends`, status: "pass", detail: "0 friends recorded" });
|
|
531
|
+
continue;
|
|
532
|
+
}
|
|
533
|
+
let parseFailures = 0;
|
|
534
|
+
let trustFamily = 0;
|
|
535
|
+
let trustFriend = 0;
|
|
536
|
+
let trustStranger = 0;
|
|
537
|
+
let trustOther = 0;
|
|
538
|
+
/* v8 ignore start -- per-record trust-level tally branches: tests don't exhaustively combine all four trust buckets in one fixture @preserve */
|
|
539
|
+
for (const name of entries) {
|
|
540
|
+
const filePath = `${friendsDir}/${name}`;
|
|
541
|
+
let raw;
|
|
542
|
+
try {
|
|
543
|
+
raw = deps.readFileSync(filePath);
|
|
544
|
+
}
|
|
545
|
+
catch {
|
|
546
|
+
parseFailures += 1;
|
|
547
|
+
continue;
|
|
548
|
+
}
|
|
549
|
+
let parsed;
|
|
550
|
+
try {
|
|
551
|
+
parsed = JSON.parse(raw);
|
|
552
|
+
}
|
|
553
|
+
catch {
|
|
554
|
+
parseFailures += 1;
|
|
555
|
+
continue;
|
|
556
|
+
}
|
|
557
|
+
const trustLevel = typeof parsed.trustLevel === "string" ? parsed.trustLevel : "friend";
|
|
558
|
+
if (trustLevel === "family")
|
|
559
|
+
trustFamily += 1;
|
|
560
|
+
else if (trustLevel === "friend")
|
|
561
|
+
trustFriend += 1;
|
|
562
|
+
else if (trustLevel === "stranger")
|
|
563
|
+
trustStranger += 1;
|
|
564
|
+
else
|
|
565
|
+
trustOther += 1;
|
|
566
|
+
}
|
|
567
|
+
if (parseFailures > 0) {
|
|
568
|
+
checks.push({ label: `${agentDir} friends`, status: "warn", detail: `${entries.length} record${entries.length === 1 ? "" : "s"}, ${parseFailures} unparseable` });
|
|
569
|
+
continue;
|
|
570
|
+
}
|
|
571
|
+
const parts = [
|
|
572
|
+
`${entries.length} friend${entries.length === 1 ? "" : "s"}`,
|
|
573
|
+
`${trustFamily} family`,
|
|
574
|
+
`${trustFriend} friend`,
|
|
575
|
+
`${trustStranger} stranger`,
|
|
576
|
+
];
|
|
577
|
+
if (trustOther > 0)
|
|
578
|
+
parts.push(`${trustOther} other`);
|
|
579
|
+
checks.push({ label: `${agentDir} friends`, status: "pass", detail: parts.join(", ") });
|
|
580
|
+
/* v8 ignore stop */
|
|
581
|
+
}
|
|
582
|
+
return { name: "Friends", checks };
|
|
583
|
+
}
|
|
584
|
+
function checkDisk(deps) {
|
|
585
|
+
const checks = [];
|
|
586
|
+
const addLogSizeCheck = (labelPrefix, logsDir) => {
|
|
587
|
+
let totalSize = 0;
|
|
588
|
+
try {
|
|
589
|
+
const files = deps.readdirSync(logsDir);
|
|
590
|
+
for (const file of files) {
|
|
591
|
+
try {
|
|
592
|
+
const stat = deps.statSync(`${logsDir}/${file}`);
|
|
593
|
+
totalSize += stat.size;
|
|
594
|
+
}
|
|
595
|
+
catch {
|
|
596
|
+
// skip unreadable files
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
catch {
|
|
601
|
+
// readdirSync failure handled below
|
|
602
|
+
}
|
|
603
|
+
const sizeMB = totalSize / (1024 * 1024);
|
|
604
|
+
if (sizeMB > 500) {
|
|
605
|
+
checks.push({ label: `${labelPrefix} daemon log size`, status: "fail", detail: `${sizeMB.toFixed(1)}MB — exceeds 500MB limit` });
|
|
606
|
+
}
|
|
607
|
+
else if (sizeMB > 100) {
|
|
608
|
+
checks.push({ label: `${labelPrefix} daemon log size`, status: "warn", detail: `${sizeMB.toFixed(1)}MB — consider pruning with \`ouro logs prune\`` });
|
|
609
|
+
}
|
|
610
|
+
else {
|
|
611
|
+
checks.push({ label: `${labelPrefix} daemon log size`, status: "pass", detail: `${sizeMB.toFixed(1)}MB` });
|
|
612
|
+
}
|
|
613
|
+
};
|
|
614
|
+
const agents = discoverAgents(deps);
|
|
615
|
+
if (agents.length === 0) {
|
|
616
|
+
checks.push({ label: "daemon logs dir", status: "warn", detail: "no agent bundles found for bundle-local logs" });
|
|
617
|
+
}
|
|
618
|
+
for (const agentDir of agents) {
|
|
619
|
+
const logsDir = `${deps.bundlesRoot}/${agentDir}/state/daemon/logs`;
|
|
620
|
+
if (!deps.existsSync(logsDir)) {
|
|
621
|
+
checks.push({ label: `${agentDir} daemon logs dir`, status: "warn", detail: `${logsDir} not found` });
|
|
622
|
+
}
|
|
623
|
+
else {
|
|
624
|
+
addLogSizeCheck(agentDir, logsDir);
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
// Check AgentBundles root
|
|
628
|
+
if (deps.existsSync(deps.bundlesRoot)) {
|
|
629
|
+
checks.push({ label: "bundles root", status: "pass", detail: deps.bundlesRoot });
|
|
630
|
+
}
|
|
631
|
+
else {
|
|
632
|
+
checks.push({ label: "bundles root", status: "warn", detail: `${deps.bundlesRoot} not found` });
|
|
633
|
+
}
|
|
634
|
+
return { name: "Disk", checks };
|
|
635
|
+
}
|
|
636
|
+
// ── Orchestrator ──
|
|
637
|
+
function computeSummary(categories) {
|
|
638
|
+
let passed = 0;
|
|
639
|
+
let warnings = 0;
|
|
640
|
+
let failed = 0;
|
|
641
|
+
for (const cat of categories) {
|
|
642
|
+
for (const check of cat.checks) {
|
|
643
|
+
/* v8 ignore next 3 -- all three branches tested; v8 misreports compound if/else-if chain @preserve */
|
|
644
|
+
if (check.status === "pass")
|
|
645
|
+
passed++;
|
|
646
|
+
else if (check.status === "warn")
|
|
647
|
+
warnings++;
|
|
648
|
+
else
|
|
649
|
+
failed++;
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
return { passed, warnings, failed };
|
|
653
|
+
}
|
|
654
|
+
/**
|
|
655
|
+
* Recent daemon lifecycle: surfaces last activity timestamp, recent restarts,
|
|
656
|
+
* version-install events, and process errors from the last hour. Designed
|
|
657
|
+
* to answer the operator's question after the daemon has gone silent: "did
|
|
658
|
+
* it crash? when did it last do anything? did it just upgrade?"
|
|
659
|
+
*
|
|
660
|
+
* Reads daemon.ndjson from the first available agent bundle (one daemon
|
|
661
|
+
* serves all agents, so any agent's bundle has the shared log).
|
|
662
|
+
*/
|
|
663
|
+
function checkLifecycle(deps) {
|
|
664
|
+
const checks = [];
|
|
665
|
+
const HOUR_MS = 60 * 60 * 1000;
|
|
666
|
+
const STALE_THRESHOLD_MS = 5 * 60 * 1000;
|
|
667
|
+
const cutoff = Date.now() - HOUR_MS;
|
|
668
|
+
const agents = discoverAgents(deps);
|
|
669
|
+
let logPath = null;
|
|
670
|
+
for (const agentDir of agents) {
|
|
671
|
+
const candidate = `${deps.bundlesRoot}/${agentDir}/state/daemon/logs/daemon.ndjson`;
|
|
672
|
+
if (deps.existsSync(candidate)) {
|
|
673
|
+
logPath = candidate;
|
|
674
|
+
break;
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
if (!logPath) {
|
|
678
|
+
checks.push({ label: "daemon log readable", status: "warn", detail: "no daemon.ndjson found in any agent bundle" });
|
|
679
|
+
return { name: "Lifecycle", checks };
|
|
680
|
+
}
|
|
681
|
+
let lastTs = null;
|
|
682
|
+
let lastEvent = null;
|
|
683
|
+
let startCount = 0;
|
|
684
|
+
let installCount = 0;
|
|
685
|
+
let installVersions = [];
|
|
686
|
+
let processErrors = [];
|
|
687
|
+
let lastEntryAgeMs = Number.POSITIVE_INFINITY;
|
|
688
|
+
try {
|
|
689
|
+
// Read the whole log via deps.readFileSync, then take the tail. For a
|
|
690
|
+
// chatty daemon this can be a few MB; we only inspect the last 5000
|
|
691
|
+
// lines which is enough for the last hour of activity. If the file is
|
|
692
|
+
// small (typical case), reading it all is cheap.
|
|
693
|
+
const raw = deps.readFileSync(logPath);
|
|
694
|
+
const allLines = raw.split("\n").filter((l) => l.trim());
|
|
695
|
+
const usable = allLines.length > 5000 ? allLines.slice(-5000) : allLines;
|
|
696
|
+
for (const line of usable) {
|
|
697
|
+
let parsed;
|
|
698
|
+
try {
|
|
699
|
+
parsed = JSON.parse(line);
|
|
700
|
+
}
|
|
701
|
+
catch {
|
|
702
|
+
continue;
|
|
703
|
+
}
|
|
704
|
+
const ts = typeof parsed.ts === "string" ? parsed.ts : null;
|
|
705
|
+
const event = typeof parsed.event === "string" ? parsed.event : null;
|
|
706
|
+
if (!ts || !event)
|
|
707
|
+
continue;
|
|
708
|
+
const tsMs = Date.parse(ts);
|
|
709
|
+
if (Number.isNaN(tsMs))
|
|
710
|
+
continue;
|
|
711
|
+
lastTs = ts;
|
|
712
|
+
lastEvent = event;
|
|
713
|
+
lastEntryAgeMs = Math.min(lastEntryAgeMs, Date.now() - tsMs);
|
|
714
|
+
if (tsMs < cutoff)
|
|
715
|
+
continue;
|
|
716
|
+
if (event === "daemon.daemon_started")
|
|
717
|
+
startCount++;
|
|
718
|
+
if (event === "daemon.cli_version_install_end") {
|
|
719
|
+
installCount++;
|
|
720
|
+
const meta = parsed.meta;
|
|
721
|
+
const ver = typeof meta?.version === "string" ? meta.version : null;
|
|
722
|
+
if (ver)
|
|
723
|
+
installVersions.push(ver);
|
|
724
|
+
}
|
|
725
|
+
if (event === "daemon.agent_process_error") {
|
|
726
|
+
const meta = parsed.meta;
|
|
727
|
+
const reason = typeof meta?.reason === "string" ? meta.reason : "unknown";
|
|
728
|
+
const agent = typeof meta?.agent === "string" ? meta.agent : "unknown";
|
|
729
|
+
processErrors.push(`${agent}: ${reason}`);
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
catch (error) {
|
|
734
|
+
checks.push({ label: "daemon log readable", status: "fail", detail: `read failed: ${error instanceof Error ? error.message : /* v8 ignore next -- non-Error throw is unreachable from deps.readFileSync (always Error) @preserve */ String(error)}` });
|
|
735
|
+
return { name: "Lifecycle", checks };
|
|
736
|
+
}
|
|
737
|
+
if (lastTs === null) {
|
|
738
|
+
checks.push({ label: "recent daemon activity", status: "warn", detail: "no parseable events in tail of daemon.ndjson" });
|
|
739
|
+
}
|
|
740
|
+
else {
|
|
741
|
+
const ageSec = Math.round(lastEntryAgeMs / 1000);
|
|
742
|
+
const ageDetail = ageSec < 60 ? `${ageSec}s ago` : `${Math.round(ageSec / 60)}m ago`;
|
|
743
|
+
if (lastEntryAgeMs > STALE_THRESHOLD_MS) {
|
|
744
|
+
checks.push({
|
|
745
|
+
label: "recent daemon activity",
|
|
746
|
+
status: "warn",
|
|
747
|
+
detail: `last event ${ageDetail} (${lastEvent}) — daemon may be silent or stopped`,
|
|
748
|
+
});
|
|
749
|
+
}
|
|
750
|
+
else {
|
|
751
|
+
checks.push({
|
|
752
|
+
label: "recent daemon activity",
|
|
753
|
+
status: "pass",
|
|
754
|
+
detail: `last event ${ageDetail} (${lastEvent})`,
|
|
755
|
+
});
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
if (startCount > 0) {
|
|
759
|
+
checks.push({
|
|
760
|
+
label: "daemon restarts (last hour)",
|
|
761
|
+
status: startCount > 3 ? "warn" : "pass",
|
|
762
|
+
detail: `${startCount} restart${startCount === 1 ? "" : "s"}${startCount > 3 ? " — high churn, investigate" : ""}`,
|
|
763
|
+
});
|
|
764
|
+
}
|
|
765
|
+
if (installCount > 0) {
|
|
766
|
+
checks.push({
|
|
767
|
+
label: "version installs (last hour)",
|
|
768
|
+
status: "pass",
|
|
769
|
+
detail: `installed: ${installVersions.join(", ")}`,
|
|
770
|
+
});
|
|
771
|
+
}
|
|
772
|
+
if (processErrors.length > 0) {
|
|
773
|
+
checks.push({
|
|
774
|
+
label: "agent process errors (last hour)",
|
|
775
|
+
status: "warn",
|
|
776
|
+
detail: `${processErrors.length} error${processErrors.length === 1 ? "" : "s"}: ${processErrors.slice(0, 3).join("; ")}${processErrors.length > 3 ? "..." : ""}`,
|
|
777
|
+
});
|
|
778
|
+
}
|
|
779
|
+
return { name: "Lifecycle", checks };
|
|
780
|
+
}
|
|
781
|
+
const CATEGORY_CHECKERS = [
|
|
782
|
+
{ name: "CLI", fn: checkCliPath },
|
|
783
|
+
{ name: "Daemon", fn: checkDaemon },
|
|
784
|
+
{ name: "Lifecycle", fn: checkLifecycle },
|
|
785
|
+
{ name: "Agents", fn: checkAgents },
|
|
786
|
+
{ name: "Senses", fn: checkSenses },
|
|
787
|
+
{ name: "Habits", fn: checkHabits },
|
|
788
|
+
{ name: "Security", fn: checkSecurity },
|
|
789
|
+
{ name: "Trips", fn: checkTrips },
|
|
790
|
+
{ name: "Mailroom", fn: checkMailroom },
|
|
791
|
+
{ name: "Friends", fn: checkFriends },
|
|
792
|
+
{ name: "Disk", fn: checkDisk },
|
|
793
|
+
];
|
|
794
|
+
exports.KNOWN_DOCTOR_CATEGORIES = CATEGORY_CHECKERS.map((c) => c.name);
|
|
795
|
+
async function runDoctorChecks(deps, options = {}) {
|
|
796
|
+
const categories = [];
|
|
797
|
+
const filter = options.category?.toLowerCase();
|
|
798
|
+
/* v8 ignore next -- branch: filter present vs absent — covered separately by --category and plain doctor tests but the filter-array generation isn't double-counted by both code paths in the same suite @preserve */
|
|
799
|
+
const checkers = filter
|
|
800
|
+
? CATEGORY_CHECKERS.filter((c) => c.name.toLowerCase() === filter)
|
|
801
|
+
: CATEGORY_CHECKERS;
|
|
802
|
+
for (const checker of checkers) {
|
|
803
|
+
try {
|
|
804
|
+
const category = await Promise.resolve(checker.fn(deps));
|
|
805
|
+
categories.push(category);
|
|
806
|
+
}
|
|
807
|
+
catch (error) {
|
|
808
|
+
(0, runtime_1.emitNervesEvent)({
|
|
809
|
+
level: "warn",
|
|
810
|
+
component: "daemon",
|
|
811
|
+
event: "daemon.doctor_check_error",
|
|
812
|
+
message: `doctor check ${checker.name} failed`,
|
|
813
|
+
meta: { category: checker.name, error: error instanceof Error ? error.message : String(error) },
|
|
814
|
+
});
|
|
815
|
+
categories.push({
|
|
816
|
+
name: checker.name,
|
|
817
|
+
checks: [{
|
|
818
|
+
label: checker.name.toLowerCase(),
|
|
819
|
+
status: "fail",
|
|
820
|
+
detail: `check crashed: ${error instanceof Error ? error.message : String(error)}`,
|
|
821
|
+
}],
|
|
822
|
+
});
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
return { categories, summary: computeSummary(categories) };
|
|
826
|
+
}
|