@ouro.bot/cli 0.1.0-alpha.83 → 0.1.0-alpha.831
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/LICENSE +21 -0
- package/README.md +154 -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/assets/bluebubbles-host +264 -0
- package/assets/sanctuary-host-launcher.sh +16 -0
- package/changelog.json +5308 -16
- package/deploy/unraid/Dockerfile +54 -0
- package/deploy/unraid/README.txt +2983 -0
- package/deploy/unraid/audit-container-spec.sh +4 -0
- package/deploy/unraid/container-runtime.json +1 -0
- package/deploy/unraid/docker-man-template-transaction.mjs +711 -0
- package/deploy/unraid/docker-man-template-xml.cjs +105 -0
- package/deploy/unraid/migrate-sanctuary-bundle.mjs +60 -0
- package/deploy/unraid/ouro-events/bootstrap-spool.sh +92 -0
- package/deploy/unraid/ouro-events/emit-event.mjs +310 -0
- package/deploy/unraid/ouro-events/emit-usenet-event.sh +54 -0
- package/deploy/unraid/ouro-events/install-usenet-guard.sh +435 -0
- package/deploy/unraid/ouro-events/usenet-health.sh +176 -0
- package/deploy/unraid/sanctuary-acceptance-adapter.sh +43 -0
- package/deploy/unraid/sanctuary-acceptance-contract.json +198 -0
- package/deploy/unraid/sanctuary-acceptance-harness.sh +23 -0
- package/deploy/unraid/sanctuary-authority-installation.json +36 -0
- package/deploy/unraid/sanctuary-authority-service.sh +30 -0
- package/deploy/unraid/sanctuary-deployment-target.mjs +710 -0
- package/deploy/unraid/sanctuary-safe-rename.py +24 -0
- package/deploy/unraid/sanctuary-unit16-host-broker.mjs +2084 -0
- package/deploy/unraid/sanctuary-unit16-run.sh +560 -0
- package/deploy/unraid/sanctuary-unit18-target-audit.sh +27 -0
- package/deploy/unraid/sanctuary.ouro/agent.json +66 -0
- package/deploy/unraid/sanctuary.ouro/arc/README.md +3 -0
- package/deploy/unraid/sanctuary.ouro/bundle-meta.json +5 -0
- package/deploy/unraid/sanctuary.ouro/habits/sanctuary-health.md +8 -0
- package/deploy/unraid/sanctuary.ouro/mcp/media-mcp.mjs +795 -0
- package/deploy/unraid/sanctuary.ouro/provider-readiness.json +11 -0
- package/deploy/unraid/sanctuary.ouro/psyche/ASPIRATIONS.md +5 -0
- package/deploy/unraid/sanctuary.ouro/psyche/IDENTITY.md +5 -0
- package/deploy/unraid/sanctuary.ouro/psyche/LORE.md +15 -0
- package/deploy/unraid/sanctuary.ouro/psyche/SOUL.md +25 -0
- package/deploy/unraid/sanctuary.ouro/psyche/TACIT.md +15 -0
- package/deploy/unraid/sanctuary.ouro/state/policy/steward.json +7 -0
- package/deploy/unraid/sanctuary.ouro/tool-profiles.json +135 -0
- package/deploy/unraid/sanctuary.xml +28 -0
- package/dist/a2a/card.js +57 -0
- package/dist/a2a/client.js +180 -0
- package/dist/a2a/config.js +50 -0
- package/dist/a2a/delegation-stores.js +56 -0
- package/dist/a2a/did-resolution.js +93 -0
- package/dist/a2a/identity.js +170 -0
- package/dist/a2a/inbound-share.js +107 -0
- package/dist/a2a/mission-result-wire.js +196 -0
- package/dist/a2a/onboarding.js +115 -0
- package/dist/a2a/pin-store.js +132 -0
- package/dist/a2a/seen-ledger.js +120 -0
- package/dist/a2a/server.js +604 -0
- package/dist/a2a/task-store.js +69 -0
- package/dist/a2a/transport.js +45 -0
- package/dist/a2a/types.js +3 -0
- package/dist/arc/attention-types.js +8 -0
- package/dist/arc/cares.js +360 -0
- package/dist/arc/episodes.js +118 -0
- package/dist/arc/evolution.js +490 -0
- package/dist/arc/flight-recorder.js +1034 -0
- package/dist/arc/intentions.js +134 -0
- package/dist/arc/json-store.js +117 -0
- package/dist/arc/obligations.js +386 -0
- package/dist/arc/packets.js +336 -0
- package/dist/arc/presence.js +185 -0
- package/dist/arc/task-lifecycle.js +57 -0
- package/dist/commerce/store.js +755 -0
- package/dist/commerce/types.js +3 -0
- package/dist/heart/active-work.js +798 -40
- package/dist/heart/agent-entry.js +252 -3
- package/dist/heart/approval-files.js +113 -0
- package/dist/heart/approval-store.js +960 -0
- 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/bluebubbles.js +156 -0
- package/dist/heart/attachments/sources/cli-local-file.js +78 -0
- package/dist/heart/attachments/sources/index.js +18 -0
- package/dist/heart/attachments/sources/telegram.js +71 -0
- package/dist/heart/attachments/store.js +132 -0
- package/dist/heart/attachments/types.js +93 -0
- package/dist/heart/auth/auth-flow.js +495 -0
- package/dist/heart/autonomy-budget.js +478 -0
- package/dist/heart/awaiting/await-alert.js +148 -0
- package/dist/heart/awaiting/await-expiry.js +143 -0
- package/dist/heart/awaiting/await-loader.js +91 -0
- package/dist/heart/awaiting/await-parser.js +149 -0
- package/dist/heart/awaiting/await-runtime-state.js +111 -0
- package/dist/heart/awaiting/await-scheduler.js +382 -0
- package/dist/heart/background-operations.js +281 -0
- package/dist/heart/bridges/manager.js +137 -17
- package/dist/heart/bridges/store.js +14 -2
- package/dist/heart/bundle-state.js +168 -0
- package/dist/heart/commitments.js +44 -18
- package/dist/heart/config-registry.js +331 -0
- package/dist/heart/config.js +174 -131
- package/dist/heart/context-loss-gauntlet.js +387 -0
- package/dist/heart/context-loss-sentinel.js +1157 -0
- package/dist/heart/core.js +2169 -581
- package/dist/heart/cross-chat-delivery.js +9 -22
- package/dist/heart/daemon/agent-config-check.js +451 -0
- package/dist/heart/daemon/agent-discovery.js +185 -3
- package/dist/heart/daemon/agent-service.js +542 -0
- package/dist/heart/daemon/agentic-repair.js +547 -0
- package/dist/heart/daemon/await-private-wake.js +59 -0
- package/dist/heart/daemon/bluebubbles-cli-integration.js +71 -0
- package/dist/heart/daemon/bluebubbles-health-diagnostics.js +122 -0
- package/dist/heart/daemon/bluebubbles-host-protocol.js +433 -0
- package/dist/heart/daemon/bluebubbles-host.js +273 -0
- package/dist/heart/daemon/boot-sync-probe.js +197 -0
- package/dist/heart/daemon/cadence.js +180 -0
- package/dist/heart/daemon/cli-defaults.js +854 -0
- package/dist/heart/daemon/cli-desk.js +322 -0
- package/dist/heart/daemon/cli-exec.js +8822 -0
- package/dist/heart/daemon/cli-help.js +716 -0
- package/dist/heart/daemon/cli-parse.js +2771 -0
- package/dist/heart/daemon/cli-render-doctor.js +57 -0
- package/dist/heart/daemon/cli-render.js +777 -0
- package/dist/heart/daemon/cli-types.js +8 -0
- package/dist/heart/daemon/connect-bay.js +323 -0
- package/dist/heart/daemon/container-credential-bootstrap.js +273 -0
- package/dist/heart/daemon/container-healthcheck.js +112 -0
- package/dist/heart/daemon/container-runtime.js +97 -0
- package/dist/heart/daemon/container-spec-auditor-main.js +171 -0
- package/dist/heart/daemon/container-spec-auditor.js +229 -0
- package/dist/heart/daemon/daemon-bootstrap-startup.js +115 -0
- package/dist/heart/daemon/daemon-cli.js +29 -2349
- package/dist/heart/daemon/daemon-entry.js +930 -11
- package/dist/heart/daemon/daemon-health.js +186 -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 +2084 -105
- package/dist/heart/daemon/dns-workflow.js +394 -0
- package/dist/heart/daemon/doctor-types.js +8 -0
- package/dist/heart/daemon/doctor.js +1755 -0
- package/dist/heart/daemon/freshness.js +345 -0
- package/dist/heart/daemon/habit-private-wake.js +61 -0
- package/dist/heart/daemon/health-monitor.js +142 -11
- package/dist/heart/daemon/hooks/agent-config-v2.js +33 -0
- package/dist/heart/daemon/hooks/bundle-meta.js +135 -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 +78 -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 +79 -10
- package/dist/heart/daemon/logs-prune.js +132 -0
- package/dist/heart/daemon/mcp-canary.js +524 -0
- package/dist/heart/daemon/message-router.js +65 -5
- package/dist/heart/daemon/migrate-to-desk.js +848 -0
- package/dist/heart/daemon/os-cron-deps.js +135 -0
- package/dist/heart/daemon/os-cron.js +22 -14
- package/dist/heart/daemon/ouro-bot-entry.js +4 -2
- package/dist/heart/daemon/ouro-entry.js +3 -1
- package/dist/heart/daemon/plugin-cli.js +432 -0
- package/dist/heart/daemon/process-manager.js +733 -61
- package/dist/heart/daemon/provider-discovery.js +137 -0
- package/dist/heart/daemon/provider-ping-progress.js +83 -0
- package/dist/heart/daemon/prunable-bundle.js +144 -0
- package/dist/heart/daemon/pulse.js +511 -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 +47 -14
- package/dist/heart/daemon/runtime-metadata.js +2 -30
- package/dist/heart/daemon/runtime-mode.js +13 -2
- package/dist/heart/daemon/safe-mode.js +161 -0
- package/dist/heart/daemon/sanctuary-acceptance-adapter.js +2654 -0
- package/dist/heart/daemon/sanctuary-acceptance-harness.js +1830 -0
- package/dist/heart/daemon/sanctuary-acceptance-marker.js +267 -0
- package/dist/heart/daemon/sanctuary-acceptance-scenarios.js +955 -0
- package/dist/heart/daemon/sanctuary-authority-codec.js +86 -0
- package/dist/heart/daemon/sanctuary-authority-epoch.js +256 -0
- package/dist/heart/daemon/sanctuary-authority-installation.js +140 -0
- package/dist/heart/daemon/sanctuary-authority-ledger.js +241 -0
- package/dist/heart/daemon/sanctuary-authority-root-lifecycle.js +819 -0
- package/dist/heart/daemon/sanctuary-authority-vault-migration.js +79 -0
- package/dist/heart/daemon/sanctuary-bundle-migration.js +729 -0
- package/dist/heart/daemon/sanctuary-host-authority.js +1008 -0
- package/dist/heart/daemon/sanctuary-host-detached-supervisor.js +494 -0
- package/dist/heart/daemon/sanctuary-host-executor.js +670 -0
- package/dist/heart/daemon/sanctuary-host-linux-kernel.js +334 -0
- package/dist/heart/daemon/sanctuary-host-supervisor-entry.js +207 -0
- package/dist/heart/daemon/sanctuary-host-supervisor.js +95 -0
- package/dist/heart/daemon/sanctuary-package-management.js +103 -0
- package/dist/heart/daemon/sanctuary-scheduler-liveness.js +227 -0
- package/dist/heart/daemon/sanctuary-scheduler-origin.js +132 -0
- package/dist/heart/daemon/sanctuary-telegram-authority-entry.js +445 -0
- package/dist/heart/daemon/sanctuary-telegram-authority-gateway.js +585 -0
- package/dist/heart/daemon/sanctuary-telegram-authority-service.js +630 -0
- package/dist/heart/daemon/sense-manager.js +699 -48
- package/dist/heart/daemon/session-id-resolver.js +131 -0
- package/dist/heart/daemon/skill-management-installer.js +1 -1
- package/dist/heart/daemon/socket-client.js +180 -12
- package/dist/heart/daemon/stale-bundle-prune.js +113 -0
- package/dist/heart/daemon/startup-tui.js +339 -0
- package/dist/heart/daemon/supercronic-supervisor.js +282 -0
- package/dist/heart/daemon/task-scheduler.js +117 -39
- package/dist/heart/daemon/terminal-ui.js +499 -0
- package/dist/heart/daemon/thoughts.js +138 -54
- package/dist/heart/daemon/up-progress.js +366 -0
- package/dist/heart/daemon/vault-items.js +56 -0
- package/dist/heart/delegation.js +1 -4
- package/dist/heart/external-events/router.js +1430 -0
- package/dist/heart/frontend-approval-runtime.js +506 -0
- package/dist/heart/frontend-journal.js +215 -0
- package/dist/heart/frontend-session-service.js +237 -0
- package/dist/heart/frontend-socket-client.js +164 -0
- package/dist/heart/frontend-socket.js +400 -0
- package/dist/heart/habits/habit-cancel.js +810 -0
- package/dist/heart/habits/habit-lifecycle.js +1327 -0
- package/dist/heart/habits/habit-migration.js +194 -0
- package/dist/heart/habits/habit-parser.js +278 -0
- package/dist/heart/habits/habit-runtime-state.js +93 -0
- package/dist/heart/habits/habit-scheduler.js +653 -0
- package/dist/heart/habits/habit-session-summary.js +318 -0
- package/dist/heart/habits/habit-session.js +636 -0
- package/dist/heart/{daemon → hatch}/hatch-flow.js +55 -62
- 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 +53 -18
- package/dist/heart/identity.js +196 -59
- package/dist/heart/kept-notes.js +289 -0
- package/dist/heart/kicks.js +1 -1
- package/dist/heart/machine-identity.js +161 -0
- package/dist/heart/mail-import-discovery.js +356 -0
- package/dist/heart/mailbox/mailbox-http-hooks.js +98 -0
- package/dist/heart/mailbox/mailbox-http-response.js +7 -0
- package/dist/heart/mailbox/mailbox-http-routes.js +398 -0
- package/dist/heart/mailbox/mailbox-http-static.js +103 -0
- package/dist/heart/mailbox/mailbox-http-transport.js +129 -0
- package/dist/heart/mailbox/mailbox-http.js +99 -0
- package/dist/heart/mailbox/mailbox-read.js +39 -0
- package/dist/heart/mailbox/mailbox-types.js +27 -0
- package/dist/heart/mailbox/mailbox-view.js +197 -0
- package/dist/heart/mailbox/readers/agent-machine.js +428 -0
- package/dist/heart/mailbox/readers/continuity-readers.js +329 -0
- package/dist/heart/mailbox/readers/mail.js +375 -0
- package/dist/heart/mailbox/readers/runtime-readers.js +823 -0
- package/dist/heart/mailbox/readers/sessions.js +231 -0
- package/dist/heart/mailbox/readers/shared.js +111 -0
- package/dist/heart/mcp/mcp-server.js +696 -0
- package/dist/heart/migrate-config.js +100 -0
- package/dist/heart/model-capabilities.js +11 -0
- package/dist/heart/orientation-frame.js +345 -0
- package/dist/heart/platform.js +81 -0
- package/dist/heart/primitives.js +1 -1
- package/dist/heart/private-runtime/decision-renderer.js +158 -0
- package/dist/heart/private-runtime/index.js +18 -0
- package/dist/heart/private-runtime/ledger.js +221 -0
- package/dist/heart/private-runtime/policy.js +252 -0
- package/dist/heart/private-runtime/types.js +2 -0
- package/dist/heart/progress-story.js +1 -1
- package/dist/heart/provider-attempt.js +137 -0
- package/dist/heart/provider-binding-resolver.js +273 -0
- package/dist/heart/provider-credentials.js +449 -0
- package/dist/heart/provider-failover.js +311 -0
- package/dist/heart/provider-models.js +96 -0
- package/dist/heart/provider-ping.js +265 -0
- package/dist/heart/provider-readiness-cache.js +119 -0
- package/dist/heart/provider-visibility.js +188 -0
- package/dist/heart/providers/anthropic-token.js +131 -0
- package/dist/heart/providers/anthropic.js +173 -60
- package/dist/heart/providers/azure.js +23 -11
- package/dist/heart/providers/error-classification.js +127 -0
- package/dist/heart/providers/github-copilot.js +34 -38
- package/dist/heart/providers/minimax-vlm.js +189 -0
- package/dist/heart/providers/minimax.js +26 -8
- package/dist/heart/providers/openai-codex-token.js +349 -0
- package/dist/heart/providers/openai-codex.js +55 -40
- package/dist/heart/providers/openai-compatible.js +177 -0
- package/dist/heart/run-ledger.js +247 -0
- package/dist/heart/runtime-capability-check.js +170 -0
- package/dist/heart/runtime-credentials.js +607 -0
- package/dist/heart/runtime-cwd.js +87 -0
- package/dist/heart/sense-truth.js +22 -4
- package/dist/heart/session-activity.js +92 -24
- package/dist/heart/session-events.js +1504 -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-redaction-repair-cli-main.js +31 -0
- package/dist/heart/session-redaction-repair.js +552 -0
- package/dist/heart/session-stats-cli-main.js +5 -0
- package/dist/heart/session-stats.js +183 -0
- package/dist/heart/session-transcript.js +133 -0
- package/dist/heart/start-of-turn-packet.js +372 -0
- package/dist/heart/steward-policy.js +618 -0
- package/dist/heart/streaming.js +604 -207
- package/dist/heart/structured-output.js +196 -0
- package/dist/heart/sync-classification.js +176 -0
- package/dist/heart/sync.js +449 -0
- package/dist/heart/target-resolution.js +11 -7
- 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-approval.js +382 -0
- package/dist/heart/tool-description.js +155 -0
- package/dist/heart/tool-friction.js +55 -0
- package/dist/heart/tool-loop.js +200 -0
- package/dist/heart/turn-context.js +482 -0
- package/dist/heart/turn-execution-lease.js +30 -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 +432 -0
- package/dist/heart/versioning/ouro-recovery-launcher.js +76 -0
- package/dist/heart/versioning/ouro-version-manager.js +409 -0
- package/dist/heart/{daemon → versioning}/staged-restart.js +40 -8
- package/dist/heart/{daemon → versioning}/update-checker.js +6 -1
- package/dist/heart/versioning/update-hooks.js +154 -0
- package/dist/heart/versioning/version-intent.js +114 -0
- package/dist/heart/work-card.js +380 -0
- package/dist/mailbox-ui/assets/index-Cyv4mw3Y.js +1 -0
- package/dist/mailbox-ui/assets/index-Du_9G9WO.css +1 -0
- package/dist/mailbox-ui/assets/vendor-CcN1XpQ9.js +61 -0
- package/dist/mailbox-ui/index.html +16 -0
- package/dist/mailroom/attention.js +167 -0
- package/dist/mailroom/autonomy.js +209 -0
- package/dist/mailroom/blob-store.js +889 -0
- package/dist/mailroom/body-cache.js +61 -0
- package/dist/mailroom/cache-sync-cli.js +58 -0
- package/dist/mailroom/core.js +803 -0
- package/dist/mailroom/entry.js +160 -0
- package/dist/mailroom/file-store.js +568 -0
- package/dist/mailroom/hosted-cache-sync.js +384 -0
- package/dist/mailroom/mbox-import.js +393 -0
- package/dist/mailroom/migration.js +164 -0
- package/dist/mailroom/outbound.js +380 -0
- package/dist/mailroom/policy.js +263 -0
- package/dist/mailroom/reader.js +292 -0
- package/dist/mailroom/search-cache.js +513 -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 +24 -2
- package/dist/mind/context.js +309 -111
- package/dist/mind/desk-section.js +362 -0
- package/dist/mind/diary-integrity.js +60 -0
- package/dist/mind/{memory.js → diary.js} +68 -77
- package/dist/mind/embedding-provider.js +60 -0
- package/dist/mind/file-state.js +179 -0
- package/dist/mind/{associative-recall.js → note-search.js} +47 -58
- package/dist/mind/obligation-steering.js +199 -3
- package/dist/mind/pending.js +36 -10
- package/dist/mind/prompt-budget.js +479 -0
- package/dist/mind/prompt-refresh.js +3 -2
- package/dist/mind/prompt.js +1050 -189
- package/dist/mind/provenance-trust.js +26 -0
- package/dist/mind/record-paths.js +312 -0
- package/dist/mind/scrutiny.js +173 -0
- package/dist/mind/session-transaction.js +564 -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 +139 -5
- package/dist/nerves/coverage/run-artifacts.js +55 -35
- package/dist/nerves/coverage/source-scanner.js +4 -4
- package/dist/nerves/event-buffer.js +111 -0
- package/dist/nerves/index.js +293 -9
- 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 +159 -0
- package/dist/nerves/runtime.js +17 -1
- package/dist/repertoire/ado-client.js +15 -56
- package/dist/repertoire/ado-semantic.js +16 -10
- package/dist/repertoire/api-client.js +97 -0
- package/dist/repertoire/bitwarden-store.js +1045 -0
- package/dist/repertoire/bundle-templates.js +71 -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 +331 -0
- package/dist/repertoire/coding/feedback.js +174 -32
- package/dist/repertoire/coding/index.js +15 -3
- package/dist/repertoire/coding/manager.js +159 -10
- package/dist/repertoire/coding/spawner.js +58 -11
- package/dist/repertoire/coding/tools.js +221 -15
- package/dist/repertoire/coding/workbench-client.js +272 -0
- package/dist/repertoire/coding/workbench-manager.js +346 -0
- 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/desk/classifier.js +362 -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 +811 -18
- package/dist/repertoire/mcp-client.js +44 -3
- package/dist/repertoire/mcp-manager.js +427 -139
- package/dist/repertoire/mcp-tools.js +124 -0
- package/dist/repertoire/plugin-mcp.js +175 -0
- package/dist/repertoire/plugins.js +253 -0
- package/dist/repertoire/relationship-authorization.js +322 -0
- package/dist/repertoire/shell-sessions.js +134 -0
- package/dist/repertoire/skills.js +48 -4
- package/dist/repertoire/stripe-client.js +131 -0
- package/dist/repertoire/tool-arguments.js +170 -0
- package/dist/repertoire/tool-results.js +29 -0
- package/dist/repertoire/tools-a2a.js +699 -0
- package/dist/repertoire/tools-attachments.js +322 -0
- package/dist/repertoire/tools-awaiting.js +575 -0
- package/dist/repertoire/tools-base.js +77 -1138
- package/dist/repertoire/tools-bluebubbles.js +2 -0
- package/dist/repertoire/tools-bridge.js +144 -0
- package/dist/repertoire/tools-bundle.js +993 -0
- package/dist/repertoire/tools-commerce.js +253 -0
- package/dist/repertoire/tools-config.js +186 -0
- package/dist/repertoire/tools-continuity.js +575 -0
- package/dist/repertoire/tools-credential.js +383 -0
- package/dist/repertoire/tools-evolution.js +527 -0
- package/dist/repertoire/tools-files.js +344 -0
- package/dist/repertoire/tools-flight.js +290 -0
- package/dist/repertoire/tools-flow.js +119 -0
- package/dist/repertoire/tools-github.js +3 -8
- package/dist/repertoire/tools-habits.js +103 -0
- package/dist/repertoire/tools-mail.js +1828 -0
- package/dist/repertoire/tools-notes.js +485 -0
- package/dist/repertoire/tools-obligations.js +182 -0
- package/dist/repertoire/tools-orientation.js +31 -0
- package/dist/repertoire/tools-record.js +482 -0
- package/dist/repertoire/tools-rsvp.js +139 -0
- package/dist/repertoire/tools-runtime.js +150 -0
- package/dist/repertoire/tools-sanctuary-host.js +202 -0
- package/dist/repertoire/tools-session.js +1133 -0
- package/dist/repertoire/tools-shell.js +149 -0
- package/dist/repertoire/tools-steward-policy.js +119 -0
- package/dist/repertoire/tools-stripe.js +224 -0
- package/dist/repertoire/tools-surface.js +369 -0
- package/dist/repertoire/tools-teams.js +12 -39
- package/dist/repertoire/tools-telegram-contacts.js +37 -0
- package/dist/repertoire/tools-travel.js +125 -0
- package/dist/repertoire/tools-trip.js +982 -0
- package/dist/repertoire/tools-unraid.js +531 -0
- package/dist/repertoire/tools-user-profile.js +146 -0
- package/dist/repertoire/tools-vault.js +40 -0
- package/dist/repertoire/tools-voice.js +145 -0
- package/dist/repertoire/tools.js +494 -148
- package/dist/repertoire/travel-api-client.js +360 -0
- package/dist/repertoire/unraid-client.js +155 -0
- package/dist/repertoire/unraid-restart.js +339 -0
- package/dist/repertoire/user-profile.js +131 -0
- package/dist/repertoire/vault-setup.js +246 -0
- package/dist/repertoire/vault-unlock.js +628 -0
- package/dist/rsvp/aisleplanner-client.js +354 -0
- package/dist/rsvp/cli.js +926 -0
- package/dist/rsvp/config.js +327 -0
- package/dist/rsvp/cutover.js +433 -0
- package/dist/rsvp/diagnostics.js +232 -0
- package/dist/rsvp/diff-renderer.js +176 -0
- package/dist/rsvp/habit-policy.js +110 -0
- package/dist/rsvp/habit-stage.js +123 -0
- package/dist/rsvp/incident-bundle.js +97 -0
- package/dist/rsvp/migration.js +204 -0
- package/dist/rsvp/native-habit-runner.js +630 -0
- package/dist/rsvp/outbound-state.js +628 -0
- package/dist/rsvp/query.js +118 -0
- package/dist/rsvp/replay.js +233 -0
- package/dist/rsvp/snapshot.js +232 -0
- package/dist/rsvp/spend-ledger.js +175 -0
- package/dist/scripts/claude-code-hook.js +41 -0
- package/dist/scripts/claude-code-stop-hook.js +47 -0
- package/dist/senses/a2a-entry.js +115 -0
- package/dist/senses/acp-server.js +386 -0
- package/dist/senses/attention-queue.js +186 -0
- package/dist/senses/await-turn-message.js +58 -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} +355 -40
- package/dist/senses/bluebubbles/context-packet.js +207 -0
- package/dist/senses/bluebubbles/context-smoke.js +144 -0
- package/dist/senses/bluebubbles/entry.js +90 -0
- package/dist/senses/{bluebubbles-inbound-log.js → bluebubbles/inbound-log.js} +21 -3
- package/dist/senses/bluebubbles/index.js +3841 -0
- package/dist/senses/bluebubbles/latest-turn.js +311 -0
- package/dist/senses/{bluebubbles-media.js → bluebubbles/media.js} +121 -71
- package/dist/senses/{bluebubbles-model.js → bluebubbles/model.js} +159 -28
- package/dist/senses/{bluebubbles-mutation-log.js → bluebubbles/mutation-log.js} +24 -13
- package/dist/senses/bluebubbles/outbound-state.js +308 -0
- package/dist/senses/bluebubbles/processed-log.js +133 -0
- package/dist/senses/bluebubbles/reaction-policy.js +47 -0
- package/dist/senses/bluebubbles/replay.js +284 -0
- package/dist/senses/{bluebubbles-runtime-state.js → bluebubbles/runtime-state.js} +30 -2
- package/dist/senses/bluebubbles/semantic-receipts.js +1986 -0
- package/dist/senses/{bluebubbles-session-cleanup.js → bluebubbles/session-cleanup.js} +1 -1
- package/dist/senses/bluebubbles/webhook-registration.js +228 -0
- package/dist/senses/bluebubbles-meta-guard.js +39 -0
- 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 +241 -0
- package/dist/senses/cli/virtual-list.js +35 -0
- package/dist/senses/cli-entry.js +60 -8
- package/dist/senses/cli.js +604 -320
- package/dist/senses/commands.js +66 -3
- package/dist/senses/context-packet-ledger.js +371 -0
- package/dist/senses/context-packets.js +154 -0
- package/dist/senses/habit-lifecycle-guard.js +24 -0
- package/dist/senses/habit-turn-message.js +160 -0
- package/dist/senses/ingress-evidence.js +2 -0
- package/dist/senses/mail-entry.js +66 -0
- package/dist/senses/mail.js +524 -0
- package/dist/senses/pipeline.js +1115 -204
- package/dist/senses/private-runtime-worker.js +859 -0
- package/dist/senses/private-runtime.js +1517 -0
- package/dist/senses/proactive-content-guard.js +51 -0
- package/dist/senses/root-host-approval-port.js +345 -0
- package/dist/senses/root-host-approval-runtime.js +393 -0
- package/dist/senses/sanctuary-authority-resident.js +102 -0
- package/dist/senses/sanctuary-download-credit-presentation.js +31 -0
- package/dist/senses/sanctuary-full-visibility-contract.js +302 -0
- package/dist/senses/sanctuary-grounding.js +118 -0
- package/dist/senses/sanctuary-health-acceptance-probe-entry.js +12 -0
- package/dist/senses/sanctuary-health-acceptance-probe.js +774 -0
- package/dist/senses/sanctuary-health-runner.js +100 -0
- package/dist/senses/sanctuary-health.js +408 -0
- package/dist/senses/sanctuary-install-state-contract.js +126 -0
- package/dist/senses/sanctuary-interactive-control.js +410 -0
- package/dist/senses/sanctuary-media-catalog-contract.js +297 -0
- package/dist/senses/sanctuary-media-optimization.js +514 -0
- package/dist/senses/sanctuary-runtime.js +340 -0
- package/dist/senses/sanctuary-sab.js +85 -0
- package/dist/senses/sanctuary-storage-optimization-contract.js +32 -0
- package/dist/senses/shared-turn.js +772 -0
- package/dist/senses/surface-tool.js +109 -0
- package/dist/senses/teams-entry.js +60 -8
- package/dist/senses/teams.js +519 -218
- package/dist/senses/telegram-admission.js +718 -0
- package/dist/senses/telegram-approval-runtime.js +793 -0
- package/dist/senses/telegram-attachments.js +186 -0
- package/dist/senses/telegram-audit-ledger.js +220 -0
- package/dist/senses/telegram-authority-transport.js +231 -0
- package/dist/senses/telegram-client.js +1749 -0
- package/dist/senses/telegram-effect-adapter.js +896 -0
- package/dist/senses/telegram-entry.js +83 -0
- package/dist/senses/telegram.js +2324 -0
- package/dist/senses/trust-gate.js +104 -9
- package/dist/senses/voice/audio-playback.js +237 -0
- package/dist/senses/voice/audio-routing.js +119 -0
- package/dist/senses/voice/elevenlabs.js +202 -0
- package/dist/senses/voice/floor-control.js +431 -0
- package/dist/senses/voice/floor-controller.js +115 -0
- package/dist/senses/voice/golden-path.js +116 -0
- package/dist/senses/voice/index.js +29 -0
- package/dist/senses/voice/meeting.js +113 -0
- package/dist/senses/voice/outbound.js +190 -0
- package/dist/senses/voice/phone.js +33 -0
- package/dist/senses/voice/playback.js +139 -0
- package/dist/senses/voice/realtime-eval.js +496 -0
- package/dist/senses/voice/realtime-trace.js +531 -0
- package/dist/senses/voice/transcript.js +70 -0
- package/dist/senses/voice/turn.js +191 -0
- package/dist/senses/voice/twilio-phone-runtime.js +807 -0
- package/dist/senses/voice/twilio-phone.js +5031 -0
- package/dist/senses/voice/types.js +2 -0
- package/dist/senses/voice/whisper.js +161 -0
- package/dist/senses/voice-entry.js +81 -0
- package/dist/senses/voice-realtime-eval-command.js +99 -0
- package/dist/senses/voice-realtime-eval-entry.js +21 -0
- package/dist/senses/voice-twilio-entry.js +87 -0
- package/dist/trips/core.js +138 -0
- package/dist/trips/store.js +265 -0
- package/dist/util/frontmatter.js +69 -0
- package/npm-shrinkwrap.json +8138 -0
- package/package.json +72 -13
- package/skills/agent-commerce.md +113 -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 +99 -0
- package/skills/travel-planning.md +138 -0
- package/dist/heart/daemon/auth-flow.js +0 -430
- package/dist/heart/daemon/ouro-path-installer.js +0 -260
- package/dist/heart/daemon/ouro-version-manager.js +0 -171
- package/dist/heart/daemon/update-hooks.js +0 -138
- package/dist/heart/obligations.js +0 -191
- package/dist/heart/safe-workspace.js +0 -228
- package/dist/heart/session-recall.js +0 -116
- package/dist/mind/friends/channel.js +0 -84
- package/dist/mind/friends/group-context.js +0 -144
- package/dist/mind/friends/resolver.js +0 -84
- package/dist/mind/friends/store-file.js +0 -190
- package/dist/mind/friends/store.js +0 -4
- package/dist/mind/friends/tokens.js +0 -26
- package/dist/mind/friends/trust-explanation.js +0 -74
- package/dist/mind/friends/types.js +0 -29
- package/dist/repertoire/tasks/board.js +0 -134
- package/dist/repertoire/tasks/index.js +0 -224
- package/dist/repertoire/tasks/lifecycle.js +0 -80
- package/dist/repertoire/tasks/middleware.js +0 -65
- package/dist/repertoire/tasks/parser.js +0 -173
- package/dist/repertoire/tasks/scanner.js +0 -132
- package/dist/repertoire/tasks/transitions.js +0 -144
- package/dist/senses/bluebubbles-entry.js +0 -13
- package/dist/senses/bluebubbles.js +0 -1181
- package/dist/senses/debug-activity.js +0 -154
- package/dist/senses/inner-dialog-worker.js +0 -90
- package/dist/senses/inner-dialog.js +0 -525
- package/subagents/README.md +0 -7
- /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/{repertoire/tasks/types.js → heart/attachments/sources/adapter.js} +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,795 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Sanctuary media MCP server — the Mendelow Cloud Butler's media surface.
|
|
3
|
+
//
|
|
4
|
+
// Design authority: the Butler's own tool-surface spec (Telegram msgs 1700-1713, 2026-09-19).
|
|
5
|
+
// Two hard constraints it set, enforced here:
|
|
6
|
+
// 1. Anything deterministic lives in the tool, not in the agent's reasoning.
|
|
7
|
+
// 2. No "call A, parse it, build B" — every return payload carries the cross-reference
|
|
8
|
+
// fields needed to answer the obvious follow-up without a second call.
|
|
9
|
+
//
|
|
10
|
+
// Protocol: JSON-RPC 2.0 over stdio, newline framed, MCP 2024-11-05.
|
|
11
|
+
|
|
12
|
+
import { readFileSync } from "node:fs"
|
|
13
|
+
import { pathToFileURL } from "node:url"
|
|
14
|
+
|
|
15
|
+
const CRED_PATH = process.env.SANCTUARY_MEDIA_CREDENTIALS ?? "/home/ouro/AgentBundles/sanctuary.ouro/mcp/media-credentials.json"
|
|
16
|
+
|
|
17
|
+
// Credentials are host-specific and deliberately NOT packaged. A fresh install
|
|
18
|
+
// has none, and the server must still start and list its tools — otherwise the
|
|
19
|
+
// whole MCP server fails to connect and the agent silently loses the surface.
|
|
20
|
+
// Missing credentials become a named, reportable per-call failure instead.
|
|
21
|
+
let cred = null
|
|
22
|
+
let credError = null
|
|
23
|
+
try {
|
|
24
|
+
cred = JSON.parse(readFileSync(CRED_PATH, "utf8"))
|
|
25
|
+
} catch (e) {
|
|
26
|
+
credError = `Media credentials are unavailable at ${CRED_PATH} (${e.code ?? e.message}). The media tools cannot reach Jellyseerr/Sonarr/Radarr/Prowlarr until that file is created.`
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const SEERR = cred?.jellyseerr ?? { url: "", apiKey: "" }
|
|
30
|
+
const SONARR = cred?.sonarr ?? { url: "", apiKey: "" }
|
|
31
|
+
const RADARR = cred?.radarr ?? { url: "", apiKey: "" }
|
|
32
|
+
const PROWLARR = cred?.prowlarr ?? { url: "", apiKey: "" }
|
|
33
|
+
const JELLYFIN_WEB = cred?.jellyfinWebUrl ?? "https://media.mendelow.cloud"
|
|
34
|
+
|
|
35
|
+
const TIMEOUT_MS = 45_000
|
|
36
|
+
|
|
37
|
+
// ---------------------------------------------------------------- http helpers
|
|
38
|
+
|
|
39
|
+
async function req(base, path, { key, header = "X-Api-Key", method = "GET", body, query } = {}) {
|
|
40
|
+
let url = `${base}${path}`
|
|
41
|
+
if (query) {
|
|
42
|
+
const parts = Object.entries(query)
|
|
43
|
+
.filter(([, v]) => v !== undefined && v !== null && v !== "")
|
|
44
|
+
// encodeURIComponent, not URLSearchParams: Jellyseerr rejects '+' for spaces.
|
|
45
|
+
.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`)
|
|
46
|
+
if (parts.length) url += `?${parts.join("&")}`
|
|
47
|
+
}
|
|
48
|
+
const headers = { [header]: key, Accept: "application/json" }
|
|
49
|
+
if (body !== undefined) headers["Content-Type"] = "application/json"
|
|
50
|
+
let res
|
|
51
|
+
try {
|
|
52
|
+
res = await fetch(url, { method, headers, body: body === undefined ? undefined : JSON.stringify(body), signal: AbortSignal.timeout(TIMEOUT_MS) })
|
|
53
|
+
} catch (e) {
|
|
54
|
+
throw new ServiceError(`${serviceOf(base)}_unreachable`, `${serviceOf(base)} did not respond: ${e.name === "TimeoutError" ? "timeout" : e.message}`)
|
|
55
|
+
}
|
|
56
|
+
if (!res.ok) {
|
|
57
|
+
const text = await res.text().catch(() => "")
|
|
58
|
+
throw new ServiceError(`${serviceOf(base)}_http_${res.status}`, `${serviceOf(base)} returned ${res.status}${text ? `: ${text.slice(0, 200)}` : ""}`)
|
|
59
|
+
}
|
|
60
|
+
if (res.status === 204) return null
|
|
61
|
+
return res.json()
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function serviceOf(base) {
|
|
65
|
+
if (base === SEERR.url) return "jellyseerr"
|
|
66
|
+
if (base === SONARR.url) return "sonarr"
|
|
67
|
+
if (base === RADARR.url) return "radarr"
|
|
68
|
+
if (base === PROWLARR.url) return "prowlarr"
|
|
69
|
+
return "service"
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
class ServiceError extends Error {
|
|
73
|
+
constructor(code, message) { super(message); this.code = code }
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const seerr = (path, opts = {}) => req(SEERR.url, `/api/v1${path}`, { key: SEERR.apiKey, ...opts })
|
|
77
|
+
// Long enough that an ordinary slow start is not called dead, short enough that
|
|
78
|
+
// nobody waits a day for a release that was never going to arrive.
|
|
79
|
+
const STALL_AFTER_HOURS = 6
|
|
80
|
+
|
|
81
|
+
const sonarr = (path, opts = {}) => req(SONARR.url, `/api/v3${path}`, { key: SONARR.apiKey, ...opts })
|
|
82
|
+
const radarr = (path, opts = {}) => req(RADARR.url, `/api/v3${path}`, { key: RADARR.apiKey, ...opts })
|
|
83
|
+
const prowlarr = (path, opts = {}) => req(PROWLARR.url, `/api/v1${path}`, { key: PROWLARR.apiKey, ...opts })
|
|
84
|
+
|
|
85
|
+
// ------------------------------------------------------------------ mapping
|
|
86
|
+
|
|
87
|
+
// Jellyseerr media status codes.
|
|
88
|
+
const MEDIA_STATUS = { 1: "unknown", 2: "pending", 3: "processing", 4: "partially_available", 5: "available" }
|
|
89
|
+
const REQUEST_STATUS = { 1: "pending_approval", 2: "approved", 3: "declined", 4: "failed" }
|
|
90
|
+
|
|
91
|
+
const TMDB_GENRES = {
|
|
92
|
+
28: "Action", 12: "Adventure", 16: "Animation", 35: "Comedy", 80: "Crime", 99: "Documentary",
|
|
93
|
+
18: "Drama", 10751: "Family", 14: "Fantasy", 36: "History", 27: "Horror", 10402: "Music",
|
|
94
|
+
9648: "Mystery", 10749: "Romance", 878: "Science Fiction", 10770: "TV Movie", 53: "Thriller",
|
|
95
|
+
10752: "War", 37: "Western", 10759: "Action & Adventure", 10762: "Kids", 10763: "News",
|
|
96
|
+
10764: "Reality", 10765: "Sci-Fi & Fantasy", 10766: "Soap", 10767: "Talk", 10768: "War & Politics",
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function genreNames(item) {
|
|
100
|
+
const ids = item.genreIds ?? (item.genres ?? []).map((g) => g.id)
|
|
101
|
+
return ids.map((id) => TMDB_GENRES[id]).filter(Boolean)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function shelfItem(item) {
|
|
105
|
+
const kind = item.mediaType === "tv" ? "series" : item.mediaType === "movie" ? "movie" : (item.firstAirDate ? "series" : "movie")
|
|
106
|
+
const date = item.releaseDate ?? item.firstAirDate ?? ""
|
|
107
|
+
const info = item.mediaInfo ?? {}
|
|
108
|
+
const status = MEDIA_STATUS[info.status] ?? "not_on_shelf"
|
|
109
|
+
return {
|
|
110
|
+
title: item.title ?? item.name ?? null,
|
|
111
|
+
year: date ? Number(date.slice(0, 4)) : null,
|
|
112
|
+
kind,
|
|
113
|
+
tmdb_id: item.id ?? null,
|
|
114
|
+
tvdb_id: info.tvdbId ?? null,
|
|
115
|
+
genres: genreNames(item),
|
|
116
|
+
overview: typeof item.overview === "string" ? item.overview.slice(0, 400) : null,
|
|
117
|
+
// The two fields that stop a second call:
|
|
118
|
+
on_shelf: status === "available" || status === "partially_available",
|
|
119
|
+
shelf_state: status,
|
|
120
|
+
request_state: info.status ? (status === "available" ? "available" : status) : "never_requested",
|
|
121
|
+
sonarr_id: info.externalServiceId ?? null,
|
|
122
|
+
radarr_id: kind === "movie" ? (info.externalServiceId ?? null) : null,
|
|
123
|
+
poster_path: item.posterPath ? `https://image.tmdb.org/t/p/w500${item.posterPath}` : null,
|
|
124
|
+
vote_average: item.voteAverage ?? null,
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ------------------------------------------------------------- tool: search
|
|
129
|
+
|
|
130
|
+
async function mediaSearch(a) {
|
|
131
|
+
const kind = a.kind ?? "any"
|
|
132
|
+
const limit = Math.min(Math.max(a.limit ?? 12, 1), 20)
|
|
133
|
+
const onShelf = a.on_shelf // true | false | undefined(any)
|
|
134
|
+
let raw = []
|
|
135
|
+
let source
|
|
136
|
+
|
|
137
|
+
if (a.query) {
|
|
138
|
+
source = "title_search"
|
|
139
|
+
const body = await seerr("/search", { query: { query: a.query, page: 1 } })
|
|
140
|
+
raw = body.results ?? []
|
|
141
|
+
} else {
|
|
142
|
+
source = "discover"
|
|
143
|
+
const keywordIds = []
|
|
144
|
+
for (const kw of a.keywords ?? []) {
|
|
145
|
+
const hit = await seerr("/search/keyword", { query: { query: kw, page: 1 } })
|
|
146
|
+
const exact = (hit.results ?? []).find((k) => k.name.toLowerCase() === kw.toLowerCase()) ?? (hit.results ?? [])[0]
|
|
147
|
+
if (exact) keywordIds.push(exact.id)
|
|
148
|
+
}
|
|
149
|
+
const genreIds = (a.genres ?? [])
|
|
150
|
+
.map((name) => Number(Object.entries(TMDB_GENRES).find(([, v]) => v.toLowerCase() === String(name).toLowerCase())?.[0]))
|
|
151
|
+
.filter((n) => Number.isFinite(n))
|
|
152
|
+
const [yMin, yMax] = a.year_range ?? []
|
|
153
|
+
const paths = kind === "series" ? ["/discover/tv"] : kind === "movie" ? ["/discover/movies"] : ["/discover/movies", "/discover/tv"]
|
|
154
|
+
// One page of discover is ~20 popular titles, of which only a couple are
|
|
155
|
+
// usually on this shelf — answering "what cozy autumn films do we own?" from
|
|
156
|
+
// that reads as though the shelf is nearly empty. When the caller filters by
|
|
157
|
+
// shelf state, walk further pages until enough matches accumulate.
|
|
158
|
+
const maxPages = onShelf === undefined ? 1 : 5
|
|
159
|
+
for (const p of paths) {
|
|
160
|
+
const isTv = p.endsWith("/tv")
|
|
161
|
+
for (let page = 1; page <= maxPages; page += 1) {
|
|
162
|
+
const q = { page, sortBy: a.sort === "release_year" ? "primary_release_date.desc" : "popularity.desc" }
|
|
163
|
+
if (genreIds.length) q.genre = genreIds.join(",")
|
|
164
|
+
if (keywordIds.length) q.keywords = keywordIds.join(",")
|
|
165
|
+
if (yMin) q[isTv ? "firstAirDateGte" : "primaryReleaseDateGte"] = `${yMin}-01-01`
|
|
166
|
+
if (yMax) q[isTv ? "firstAirDateLte" : "primaryReleaseDateLte"] = `${yMax}-12-31`
|
|
167
|
+
const body = await seerr(p, { query: q })
|
|
168
|
+
const results = body.results ?? []
|
|
169
|
+
raw.push(...results.map((r) => ({ ...r, mediaType: isTv ? "tv" : "movie" })))
|
|
170
|
+
const matchesSoFar = raw
|
|
171
|
+
.map((r) => (r.mediaInfo?.status === 5 || r.mediaInfo?.status === 4))
|
|
172
|
+
.filter((hit) => hit === (onShelf === true)).length
|
|
173
|
+
if (results.length === 0 || page >= (body.totalPages ?? 1) || matchesSoFar >= limit) break
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
let items = raw.filter((r) => r.mediaType !== "person").map(shelfItem)
|
|
179
|
+
if (kind !== "any") items = items.filter((i) => i.kind === kind)
|
|
180
|
+
if (a.year_range) {
|
|
181
|
+
const [lo, hi] = a.year_range
|
|
182
|
+
items = items.filter((i) => i.year && (!lo || i.year >= lo) && (!hi || i.year <= hi))
|
|
183
|
+
}
|
|
184
|
+
if (onShelf === true) items = items.filter((i) => i.on_shelf)
|
|
185
|
+
if (onShelf === false) items = items.filter((i) => !i.on_shelf)
|
|
186
|
+
if (a.sort === "release_year") items.sort((x, y) => (y.year ?? 0) - (x.year ?? 0))
|
|
187
|
+
|
|
188
|
+
return {
|
|
189
|
+
source,
|
|
190
|
+
matched: items.length,
|
|
191
|
+
items: items.slice(0, limit),
|
|
192
|
+
applied_filters: { kind, year_range: a.year_range ?? null, genres: a.genres ?? [], keywords: a.keywords ?? [], on_shelf: onShelf ?? "any" },
|
|
193
|
+
// So the agent never claims something is missing when the filter caused it:
|
|
194
|
+
note: items.length === 0 ? "No matches for these filters. Loosen year_range/genres/keywords before concluding the shelf lacks it." : null,
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// ------------------------------------------------------------ tool: request
|
|
199
|
+
|
|
200
|
+
// Resolve by TMDB id against the title itself rather than scanning the request
|
|
201
|
+
// list: this household has over a thousand requests, so a page-limited scan
|
|
202
|
+
// silently misses older ones and would file a duplicate.
|
|
203
|
+
async function findExistingRequest(tmdbId, kind) {
|
|
204
|
+
const kinds = kind ? [kind] : ["movie", "series"]
|
|
205
|
+
for (const k of kinds) {
|
|
206
|
+
let detail
|
|
207
|
+
try {
|
|
208
|
+
detail = await seerr(`/${k === "series" ? "tv" : "movie"}/${tmdbId}`)
|
|
209
|
+
} catch {
|
|
210
|
+
continue
|
|
211
|
+
}
|
|
212
|
+
const info = detail?.mediaInfo
|
|
213
|
+
if (!info) continue
|
|
214
|
+
const requests = info.requests ?? []
|
|
215
|
+
const latest = requests.length ? requests[requests.length - 1] : null
|
|
216
|
+
return {
|
|
217
|
+
id: latest?.id ?? null,
|
|
218
|
+
status: latest?.status ?? null,
|
|
219
|
+
createdAt: latest?.createdAt ?? null,
|
|
220
|
+
type: k === "series" ? "tv" : "movie",
|
|
221
|
+
media: { ...info, tmdbId, title: detail.title ?? detail.name ?? null },
|
|
222
|
+
hasRequest: requests.length > 0,
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return null
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
async function mediaRequest(a) {
|
|
229
|
+
let tmdbId = a.tmdb_id
|
|
230
|
+
let kind = a.kind ?? "movie"
|
|
231
|
+
let resolvedTitle = a.title ?? null
|
|
232
|
+
let resolvedYear = a.year ?? null
|
|
233
|
+
|
|
234
|
+
if (!tmdbId) {
|
|
235
|
+
if (!a.title) throw new ServiceError("bad_arguments", "media_request needs tmdb_id, or title (year optional).")
|
|
236
|
+
const body = await seerr("/search", { query: { query: a.title, page: 1 } })
|
|
237
|
+
let cands = (body.results ?? []).filter((r) => r.mediaType === "movie" || r.mediaType === "tv")
|
|
238
|
+
if (a.kind) cands = cands.filter((r) => (a.kind === "series" ? r.mediaType === "tv" : r.mediaType === "movie"))
|
|
239
|
+
if (a.year) cands = cands.filter((r) => ((r.releaseDate ?? r.firstAirDate ?? "").slice(0, 4)) === String(a.year))
|
|
240
|
+
const pick = cands[0]
|
|
241
|
+
if (!pick) {
|
|
242
|
+
return { resolved: false, reason: "no_tmdb_match", searched: { title: a.title, year: a.year ?? null, kind: a.kind ?? "any" },
|
|
243
|
+
human_action_required: true, human_action_reason: `No TMDB match for "${a.title}"${a.year ? ` (${a.year})` : ""}. Check the title or give a year.` }
|
|
244
|
+
}
|
|
245
|
+
tmdbId = pick.id
|
|
246
|
+
kind = pick.mediaType === "tv" ? "series" : "movie"
|
|
247
|
+
resolvedTitle = pick.title ?? pick.name
|
|
248
|
+
resolvedYear = Number((pick.releaseDate ?? pick.firstAirDate ?? "").slice(0, 4)) || null
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const existing = await findExistingRequest(tmdbId, kind)
|
|
252
|
+
if (existing?.hasRequest) {
|
|
253
|
+
const shelf = MEDIA_STATUS[existing.media?.status] ?? "unknown"
|
|
254
|
+
return {
|
|
255
|
+
resolved: true, title: resolvedTitle ?? existing.media?.title ?? null, year: resolvedYear, kind, tmdb_id: tmdbId,
|
|
256
|
+
request_id: `jellyseerr:${existing.id}`,
|
|
257
|
+
approval_state: "already_requested",
|
|
258
|
+
duplicate_of: `jellyseerr:${existing.id}`,
|
|
259
|
+
jellyseerr_status: REQUEST_STATUS[existing.status] ?? "unknown",
|
|
260
|
+
shelf_state: shelf,
|
|
261
|
+
submitted_at: existing.createdAt,
|
|
262
|
+
tvdb_id: existing.media?.tvdbId ?? null,
|
|
263
|
+
sonarr_id: kind === "series" ? (existing.media?.externalServiceId ?? null) : null,
|
|
264
|
+
radarr_id: kind === "movie" ? (existing.media?.externalServiceId ?? null) : null,
|
|
265
|
+
note: shelf === "available" ? "Already on the shelf." : "Already requested; use media_request_status to see why it has not landed.",
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const payload = { mediaType: kind === "series" ? "tv" : "movie", mediaId: tmdbId }
|
|
270
|
+
if (kind === "series") {
|
|
271
|
+
payload.seasons = a.season_number ? [a.season_number] : "all"
|
|
272
|
+
}
|
|
273
|
+
const created = await seerr("/request", { method: "POST", body: payload })
|
|
274
|
+
return {
|
|
275
|
+
resolved: true, title: resolvedTitle, year: resolvedYear, kind, tmdb_id: tmdbId,
|
|
276
|
+
request_id: `jellyseerr:${created.id}`,
|
|
277
|
+
approval_state: REQUEST_STATUS[created.status] ?? "submitted",
|
|
278
|
+
duplicate_of: null,
|
|
279
|
+
jellyseerr_status: REQUEST_STATUS[created.status] ?? "submitted",
|
|
280
|
+
shelf_state: MEDIA_STATUS[created.media?.status] ?? "processing",
|
|
281
|
+
submitted_at: created.createdAt,
|
|
282
|
+
tvdb_id: created.media?.tvdbId ?? null,
|
|
283
|
+
sonarr_id: kind === "series" ? (created.media?.externalServiceId ?? null) : null,
|
|
284
|
+
radarr_id: kind === "movie" ? (created.media?.externalServiceId ?? null) : null,
|
|
285
|
+
seasons_requested: kind === "series" ? (a.season_number ? [a.season_number] : "all") : null,
|
|
286
|
+
note: "Submitted. The indexer hunt runs next; media_request_status will show whether a release was grabbed.",
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// -------------------------------------------------- acquisition chain health
|
|
291
|
+
|
|
292
|
+
// The failure that stalled this house for three weeks: Prowlarr had zero indexers,
|
|
293
|
+
// so Sonarr reported "0 active indexers" and every request sat at "requested"
|
|
294
|
+
// forever. That must be a first-class, named answer — never something the agent
|
|
295
|
+
// has to infer from an empty release list.
|
|
296
|
+
async function chainHealth() {
|
|
297
|
+
const out = { indexers: { total: 0, enabled: 0, failing: [], ok: [] }, apps: [], problems: [], healthy: true }
|
|
298
|
+
try {
|
|
299
|
+
const list = await prowlarr("/indexer")
|
|
300
|
+
out.indexers.total = list.length
|
|
301
|
+
out.indexers.enabled = list.filter((i) => i.enable).length
|
|
302
|
+
const status = await prowlarr("/indexerstatus").catch(() => [])
|
|
303
|
+
const disabled = new Set((status ?? []).map((s) => s.indexerId))
|
|
304
|
+
for (const i of list) {
|
|
305
|
+
if (!i.enable) continue
|
|
306
|
+
;(disabled.has(i.id) ? out.indexers.failing : out.indexers.ok).push(i.name)
|
|
307
|
+
}
|
|
308
|
+
const health = await prowlarr("/health").catch(() => [])
|
|
309
|
+
for (const h of health ?? []) {
|
|
310
|
+
if (h.type === "error" && /indexer/i.test(h.source ?? "")) out.problems.push({ service: "prowlarr", message: h.message })
|
|
311
|
+
}
|
|
312
|
+
out.apps = (await prowlarr("/applications").catch(() => [])).map((x) => ({ name: x.name, sync: x.syncLevel }))
|
|
313
|
+
} catch (e) {
|
|
314
|
+
out.problems.push({ service: "prowlarr", message: e.message })
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (out.indexers.total === 0) {
|
|
318
|
+
out.healthy = false
|
|
319
|
+
out.problems.push({ service: "prowlarr", message: "Prowlarr has zero indexers configured. Nothing can ever be found or downloaded until indexers are added." })
|
|
320
|
+
} else if (out.indexers.ok.length === 0) {
|
|
321
|
+
out.healthy = false
|
|
322
|
+
out.problems.push({ service: "prowlarr", message: "Every enabled indexer is currently failing. Searches will return nothing." })
|
|
323
|
+
}
|
|
324
|
+
if (out.apps.length === 0) {
|
|
325
|
+
out.healthy = false
|
|
326
|
+
out.problems.push({ service: "prowlarr", message: "Prowlarr is not syncing to Sonarr/Radarr, so they have no working indexers." })
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
for (const [name, call] of [["sonarr", sonarr], ["radarr", radarr]]) {
|
|
330
|
+
try {
|
|
331
|
+
const h = await call("/health")
|
|
332
|
+
for (const item of h ?? []) {
|
|
333
|
+
if (/indexer/i.test(item.source ?? "") && (item.type === "error" || /unavailable/i.test(item.message ?? ""))) {
|
|
334
|
+
out.problems.push({ service: name, message: item.message })
|
|
335
|
+
if (/all/i.test(item.message ?? "")) out.healthy = false
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
} catch (e) { out.problems.push({ service: name, message: e.message }) }
|
|
339
|
+
}
|
|
340
|
+
return out
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// --------------------------------------------------- tool: request status
|
|
344
|
+
|
|
345
|
+
async function resolveTarget(a) {
|
|
346
|
+
if (a.tmdb_id) {
|
|
347
|
+
const r = await findExistingRequest(a.tmdb_id, a.kind)
|
|
348
|
+
if (r?.hasRequest) return r
|
|
349
|
+
}
|
|
350
|
+
if (a.request_id) {
|
|
351
|
+
const id = Number(String(a.request_id).replace(/^jellyseerr:/, ""))
|
|
352
|
+
if (Number.isFinite(id)) {
|
|
353
|
+
try { return await seerr(`/request/${id}`) } catch { /* fall through to title */ }
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
if (a.title) {
|
|
357
|
+
const body = await seerr("/search", { query: { query: a.title, page: 1 } })
|
|
358
|
+
// Only titles Jellyseerr already knows about can have a request behind them.
|
|
359
|
+
for (const pick of (body.results ?? []).filter((r) => r.mediaInfo)) {
|
|
360
|
+
const found = await findExistingRequest(pick.id, pick.mediaType === "tv" ? "series" : "movie")
|
|
361
|
+
if (found?.hasRequest) return found
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
return null
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
async function mediaRequestStatus(a) {
|
|
368
|
+
const request = await resolveTarget(a)
|
|
369
|
+
if (!request) {
|
|
370
|
+
return { found: false, reason: "no_matching_request",
|
|
371
|
+
searched: { request_id: a.request_id ?? null, tmdb_id: a.tmdb_id ?? null, title: a.title ?? null },
|
|
372
|
+
diagnosis: { stuck_stage: "request", stuck_reason: "never_requested",
|
|
373
|
+
likely_fix: "Submit it with media_request.", human_action_required: false } }
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const kind = request.type === "tv" ? "series" : "movie"
|
|
377
|
+
const media = request.media ?? {}
|
|
378
|
+
const shelf = MEDIA_STATUS[media.status] ?? "unknown"
|
|
379
|
+
const svcId = media.externalServiceId ?? null
|
|
380
|
+
|
|
381
|
+
const result = {
|
|
382
|
+
found: true,
|
|
383
|
+
request: {
|
|
384
|
+
id: `jellyseerr:${request.id}`,
|
|
385
|
+
kind,
|
|
386
|
+
tmdb_id: media.tmdbId ?? null,
|
|
387
|
+
tvdb_id: media.tvdbId ?? null,
|
|
388
|
+
jellyseerr_status: REQUEST_STATUS[request.status] ?? "unknown",
|
|
389
|
+
shelf_state: shelf,
|
|
390
|
+
submitted_at: request.createdAt,
|
|
391
|
+
sonarr_id: kind === "series" ? svcId : null,
|
|
392
|
+
radarr_id: kind === "movie" ? svcId : null,
|
|
393
|
+
},
|
|
394
|
+
search: null, download: null, file: null, chain: null, diagnosis: null,
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
let entity = null, queueRecs = [], history = []
|
|
398
|
+
try {
|
|
399
|
+
if (kind === "series" && svcId) {
|
|
400
|
+
entity = await sonarr(`/series/${svcId}`)
|
|
401
|
+
const q = await sonarr("/queue", { query: { pageSize: 200, includeSeries: true } })
|
|
402
|
+
queueRecs = (q.records ?? []).filter((r) => r.seriesId === svcId)
|
|
403
|
+
history = await sonarr("/history/series", { query: { seriesId: svcId } }).catch(() => [])
|
|
404
|
+
result.request.title = entity.title
|
|
405
|
+
result.file = { on_shelf: (entity.statistics?.episodeFileCount ?? 0) > 0,
|
|
406
|
+
episodes_on_disk: entity.statistics?.episodeFileCount ?? 0,
|
|
407
|
+
episodes_total: entity.statistics?.episodeCount ?? 0,
|
|
408
|
+
size_bytes: entity.statistics?.sizeOnDisk ?? 0,
|
|
409
|
+
path: entity.path ?? null }
|
|
410
|
+
} else if (kind === "movie" && svcId) {
|
|
411
|
+
entity = await radarr(`/movie/${svcId}`)
|
|
412
|
+
const q = await radarr("/queue", { query: { pageSize: 200 } })
|
|
413
|
+
queueRecs = (q.records ?? []).filter((r) => r.movieId === svcId)
|
|
414
|
+
history = await radarr("/history/movie", { query: { movieId: svcId } }).catch(() => [])
|
|
415
|
+
result.request.title = entity.title
|
|
416
|
+
result.file = { on_shelf: Boolean(entity.hasFile), size_bytes: entity.sizeOnDisk ?? 0, path: entity.path ?? null }
|
|
417
|
+
}
|
|
418
|
+
} catch (e) {
|
|
419
|
+
result.chain_error = e.message
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const grabs = (history ?? []).filter((h) => h.eventType === "grabbed")
|
|
423
|
+
result.search = {
|
|
424
|
+
grab_attempts: grabs.length,
|
|
425
|
+
last_grab_at: grabs[0]?.date ?? null,
|
|
426
|
+
last_grab_title: grabs[0]?.sourceTitle ?? null,
|
|
427
|
+
monitored: entity ? Boolean(entity.monitored) : null,
|
|
428
|
+
}
|
|
429
|
+
result.download = computeDownloadState(queueRecs, Date.now())
|
|
430
|
+
|
|
431
|
+
const chain = await chainHealth()
|
|
432
|
+
result.chain = chain
|
|
433
|
+
result.diagnosis = diagnose({ shelf, result, chain, entity, kind })
|
|
434
|
+
return result
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// The deterministic core, split out so it can be tested without a live
|
|
438
|
+
// Sonarr/Radarr. `computeDownloadState` and `diagnose` are the whole reason the
|
|
439
|
+
// media surface can answer "why hasn't this downloaded" without the agent
|
|
440
|
+
// guessing, and D-013 (a stalled download read as progress) shipped verified
|
|
441
|
+
// only in production because there was no seam to test them through.
|
|
442
|
+
//
|
|
443
|
+
// A season pack shows up as one queue row per episode, all sharing one
|
|
444
|
+
// downloadId. Summing rows would report a 45 GB pack as 450 GB, so size and
|
|
445
|
+
// "how many downloads" are counted per distinct download, not per row. A
|
|
446
|
+
// torrent that has not moved a byte since it was grabbed is dead, not slow:
|
|
447
|
+
// Radarr reports trackedDownloadStatus "ok" for these indefinitely, so
|
|
448
|
+
// progress is measured against age rather than trusted from the row.
|
|
449
|
+
export function computeDownloadState(queueRecs, nowMs) {
|
|
450
|
+
const byDownload = new Map()
|
|
451
|
+
for (const r of queueRecs) byDownload.set(r.downloadId ?? `row:${r.id}`, r)
|
|
452
|
+
const downloads = [...byDownload.values()]
|
|
453
|
+
const sizeLeft = downloads.reduce((s, r) => s + (r.sizeleft ?? 0), 0)
|
|
454
|
+
const sizeTotal = downloads.reduce((s, r) => s + (r.size ?? 0), 0)
|
|
455
|
+
const stalledItems = downloads
|
|
456
|
+
.filter((r) => (r.size ?? 0) > 0 && (r.sizeleft ?? 0) >= (r.size ?? 0) && r.added
|
|
457
|
+
&& (nowMs - Date.parse(r.added)) / 3_600_000 >= STALL_AFTER_HOURS)
|
|
458
|
+
.map((r) => ({
|
|
459
|
+
title: r.title ?? null,
|
|
460
|
+
added_at: r.added ?? null,
|
|
461
|
+
age_hours: Math.round((nowMs - Date.parse(r.added)) / 3_600_000),
|
|
462
|
+
size_gb: Number(((r.size ?? 0) / 1073741824).toFixed(2)),
|
|
463
|
+
queue_id: r.id ?? null,
|
|
464
|
+
}))
|
|
465
|
+
return {
|
|
466
|
+
active_downloads: downloads.length,
|
|
467
|
+
active_items: queueRecs.length,
|
|
468
|
+
states: [...new Set(downloads.map((r) => r.status))],
|
|
469
|
+
tracked_states: [...new Set(downloads.map((r) => r.trackedDownloadState).filter(Boolean))],
|
|
470
|
+
errors: [...new Set(downloads.map((r) => r.errorMessage).filter(Boolean))],
|
|
471
|
+
size_left_bytes: sizeLeft,
|
|
472
|
+
size_total_bytes: sizeTotal,
|
|
473
|
+
percent_complete: sizeTotal > 0 ? Math.round(((sizeTotal - sizeLeft) / sizeTotal) * 100) : null,
|
|
474
|
+
stalled: stalledItems.length > 0,
|
|
475
|
+
stalled_items: stalledItems,
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// The deterministic core. The agent must never have to work this out itself.
|
|
480
|
+
export function diagnose({ shelf, result, chain, entity, kind }) {
|
|
481
|
+
const file = result.file ?? {}
|
|
482
|
+
const onShelf = kind === "series" ? (file.episodes_on_disk ?? 0) > 0 : Boolean(file.on_shelf)
|
|
483
|
+
const complete = kind === "series" ? (file.episodes_on_disk ?? 0) >= (file.episodes_total ?? Infinity) : onShelf
|
|
484
|
+
|
|
485
|
+
if (complete) {
|
|
486
|
+
return { stuck_stage: null, stuck_reason: null, likely_fix: null, human_action_required: false,
|
|
487
|
+
summary: "On the shelf and complete. Nothing is stuck." }
|
|
488
|
+
}
|
|
489
|
+
if (result.download.stalled) {
|
|
490
|
+
const items = result.download.stalled_items
|
|
491
|
+
const oldest = Math.max(...items.map((i) => i.age_hours))
|
|
492
|
+
return { stuck_stage: "download", stuck_reason: "stalled_no_progress",
|
|
493
|
+
likely_fix: "blocklist_and_research", human_action_required: false,
|
|
494
|
+
percent_complete: result.download.percent_complete,
|
|
495
|
+
detail: items,
|
|
496
|
+
summary: `Stalled, not slow. ${items.length === 1 ? "The release" : `${items.length} releases`} ${items.length === 1 ? "has" : "have"} not downloaded a single byte in ${oldest} hours, which means no seeders rather than a quiet queue. Waiting will not fix it; blocklist the release and search again for a different one.` }
|
|
497
|
+
}
|
|
498
|
+
if (result.download.active_downloads > 0) {
|
|
499
|
+
const left = result.download.size_left_bytes
|
|
500
|
+
const total = result.download.size_total_bytes
|
|
501
|
+
const gb = (left / 1073741824).toFixed(1)
|
|
502
|
+
const pct = total > 0 ? Math.round(((total - left) / total) * 100) : null
|
|
503
|
+
const what = result.download.active_downloads === 1 && result.download.active_items > 1
|
|
504
|
+
? `a ${result.download.active_items}-episode pack`
|
|
505
|
+
: `${result.download.active_downloads} download(s)`
|
|
506
|
+
return { stuck_stage: null, stuck_reason: null, likely_fix: null, human_action_required: false,
|
|
507
|
+
percent_complete: pct,
|
|
508
|
+
summary: `Downloading now — ${what}${pct === null ? "" : `, ${pct}% done`}, about ${gb} GB to go.` }
|
|
509
|
+
}
|
|
510
|
+
if (result.download.errors.length) {
|
|
511
|
+
return { stuck_stage: "download", stuck_reason: "download_client_error", likely_fix: "retry_or_replace_release",
|
|
512
|
+
human_action_required: false, detail: result.download.errors,
|
|
513
|
+
summary: `The download client reported: ${result.download.errors.join("; ")}` }
|
|
514
|
+
}
|
|
515
|
+
if (!chain.healthy) {
|
|
516
|
+
return { stuck_stage: "indexers", stuck_reason: "acquisition_chain_down",
|
|
517
|
+
likely_fix: "restore_indexers", human_action_required: true,
|
|
518
|
+
detail: chain.problems,
|
|
519
|
+
summary: `Nothing can download: ${chain.problems.map((p) => p.message).join(" ")} This blocks every request, not just this one.` }
|
|
520
|
+
}
|
|
521
|
+
if (entity && entity.monitored === false) {
|
|
522
|
+
return { stuck_stage: "request", stuck_reason: "not_monitored", likely_fix: "enable_monitoring",
|
|
523
|
+
human_action_required: false, summary: "It is not monitored, so no search will ever run for it." }
|
|
524
|
+
}
|
|
525
|
+
if (result.search.grab_attempts === 0) {
|
|
526
|
+
return { stuck_stage: "search", stuck_reason: "no_search_run_or_no_acceptable_release",
|
|
527
|
+
likely_fix: "rescan", human_action_required: false,
|
|
528
|
+
summary: "Indexers are healthy but nothing has ever been grabbed. A fresh search (media_diagnose_and_fix action=rescan) is the next step." }
|
|
529
|
+
}
|
|
530
|
+
if (onShelf && !complete) {
|
|
531
|
+
return { stuck_stage: "import", stuck_reason: "partially_imported", likely_fix: "rescan",
|
|
532
|
+
human_action_required: false,
|
|
533
|
+
summary: `Partly here: ${file.episodes_on_disk} of ${file.episodes_total} episodes. The rest still need grabbing.` }
|
|
534
|
+
}
|
|
535
|
+
return { stuck_stage: "import", stuck_reason: "grabbed_but_not_imported", likely_fix: "force_import_scan",
|
|
536
|
+
human_action_required: false,
|
|
537
|
+
summary: `It was grabbed (${result.search.last_grab_title ?? "unknown release"}) but never landed on the shelf.` }
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// -------------------------------------------------- tool: diagnose_and_fix
|
|
541
|
+
|
|
542
|
+
async function mediaDiagnoseAndFix(a) {
|
|
543
|
+
const before = await mediaRequestStatus({ request_id: a.request_id, tmdb_id: a.tmdb_id, title: a.title })
|
|
544
|
+
if (!before.found) return { result: "no_such_request", before, after: null, human_action_required: true,
|
|
545
|
+
human_action_reason: "Nothing to fix — it was never requested." }
|
|
546
|
+
const action = a.action ?? (before.diagnosis?.likely_fix ?? "rescan")
|
|
547
|
+
|
|
548
|
+
if (before.diagnosis?.stuck_reason === "acquisition_chain_down" && action !== "report_only") {
|
|
549
|
+
return { action, dry_run: Boolean(a.dry_run), result: "rejected_fix_unsafe", before, after: null,
|
|
550
|
+
human_action_required: true,
|
|
551
|
+
human_action_reason: "The acquisition chain itself is down (see before.chain.problems). Re-searching cannot help until indexers are restored." }
|
|
552
|
+
}
|
|
553
|
+
if (a.dry_run) {
|
|
554
|
+
return { action, dry_run: true, result: "would_apply", before,
|
|
555
|
+
would_do: describeAction(action), human_action_required: false }
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
const kind = before.request.kind
|
|
559
|
+
const svcId = kind === "series" ? before.request.sonarr_id : before.request.radarr_id
|
|
560
|
+
if (!svcId) return { action, result: "manual_required", before, after: null, human_action_required: true,
|
|
561
|
+
human_action_reason: "This request is not yet mapped into Sonarr/Radarr, so no search can be triggered." }
|
|
562
|
+
|
|
563
|
+
let commanded = null
|
|
564
|
+
if (action === "rescan" || action === "enable_monitoring" || action === "force_import_scan") {
|
|
565
|
+
if (action === "enable_monitoring") {
|
|
566
|
+
if (kind === "series") await sonarr(`/series/${svcId}`, { method: "PUT", body: { ...(await sonarr(`/series/${svcId}`)), monitored: true } })
|
|
567
|
+
else await radarr(`/movie/${svcId}`, { method: "PUT", body: { ...(await radarr(`/movie/${svcId}`)), monitored: true } })
|
|
568
|
+
}
|
|
569
|
+
const cmd = kind === "series"
|
|
570
|
+
? (action === "force_import_scan" ? { name: "RescanSeries", seriesId: svcId } : { name: "SeriesSearch", seriesId: svcId })
|
|
571
|
+
: (action === "force_import_scan" ? { name: "RescanMovie", movieIds: [svcId] } : { name: "MoviesSearch", movieIds: [svcId] })
|
|
572
|
+
commanded = kind === "series" ? await sonarr("/command", { method: "POST", body: cmd }) : await radarr("/command", { method: "POST", body: cmd })
|
|
573
|
+
} else if (action === "blocklist_and_research") {
|
|
574
|
+
// Removing with blocklist=true is what stops the same dead release being
|
|
575
|
+
// grabbed straight back. The search that follows is then free to pick a
|
|
576
|
+
// different one.
|
|
577
|
+
const stalled = before.download.stalled_items ?? []
|
|
578
|
+
if (!stalled.length) return { action, result: "nothing_to_blocklist", before, after: null, human_action_required: false,
|
|
579
|
+
human_action_reason: "No download has been sitting at zero long enough to call it stalled." }
|
|
580
|
+
for (const item of stalled) {
|
|
581
|
+
if (item.queue_id === null) continue
|
|
582
|
+
const client = kind === "series" ? sonarr : radarr
|
|
583
|
+
await client(`/queue/${item.queue_id}`, { method: "DELETE", query: { removeFromClient: true, blocklist: true, skipRedownload: true } })
|
|
584
|
+
}
|
|
585
|
+
const cmd = kind === "series" ? { name: "SeriesSearch", seriesId: svcId } : { name: "MoviesSearch", movieIds: [svcId] }
|
|
586
|
+
commanded = kind === "series" ? await sonarr("/command", { method: "POST", body: cmd }) : await radarr("/command", { method: "POST", body: cmd })
|
|
587
|
+
} else {
|
|
588
|
+
return { action, result: "unsupported_action", before, after: null, human_action_required: true,
|
|
589
|
+
human_action_reason: `Action "${action}" is not implemented. Supported: rescan, enable_monitoring, force_import_scan, blocklist_and_research, report_only.` }
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
await new Promise((r) => setTimeout(r, 12_000))
|
|
593
|
+
const after = await mediaRequestStatus({ request_id: a.request_id, tmdb_id: a.tmdb_id, title: a.title })
|
|
594
|
+
return {
|
|
595
|
+
action, dry_run: false, applied_at: new Date().toISOString(),
|
|
596
|
+
command: { id: commanded?.id ?? null, name: commanded?.name ?? null },
|
|
597
|
+
before: { diagnosis: before.diagnosis, grab_attempts: before.search.grab_attempts, active_downloads: before.download.active_downloads },
|
|
598
|
+
after: { diagnosis: after.diagnosis, grab_attempts: after.search.grab_attempts, active_downloads: after.download.active_downloads },
|
|
599
|
+
result: after.download.active_downloads > before.download.active_downloads ? "grabbed_and_downloading"
|
|
600
|
+
: after.search.grab_attempts > before.search.grab_attempts ? "grabbed"
|
|
601
|
+
: "queued_for_search",
|
|
602
|
+
human_action_required: Boolean(after.diagnosis?.human_action_required),
|
|
603
|
+
human_action_reason: after.diagnosis?.human_action_required ? after.diagnosis.summary : null,
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
function describeAction(action) {
|
|
608
|
+
return {
|
|
609
|
+
rescan: "Trigger a fresh indexer search and grab the best acceptable release.",
|
|
610
|
+
enable_monitoring: "Mark it monitored, then search.",
|
|
611
|
+
force_import_scan: "Re-scan the disk so an already-downloaded file gets imported.",
|
|
612
|
+
blocklist_and_research: "Blocklist the stalled release so it cannot be grabbed again, then search for a different one.",
|
|
613
|
+
report_only: "Report only; change nothing.",
|
|
614
|
+
}[action] ?? "Unknown action."
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// ------------------------------------------------------- tool: play/resolve
|
|
618
|
+
|
|
619
|
+
async function mediaPlayOrResolve(a) {
|
|
620
|
+
const status = await mediaRequestStatus({ request_id: a.request_id, tmdb_id: a.tmdb_id, title: a.title })
|
|
621
|
+
if (!status.found || !status.file) {
|
|
622
|
+
return { playable: false, reason: "not_on_shelf", diagnosis: status.diagnosis ?? null,
|
|
623
|
+
human_action_required: true, human_action_reason: "It is not on the shelf yet." }
|
|
624
|
+
}
|
|
625
|
+
const onShelf = status.request.kind === "series" ? (status.file.episodes_on_disk ?? 0) > 0 : status.file.on_shelf
|
|
626
|
+
return {
|
|
627
|
+
playable: Boolean(onShelf),
|
|
628
|
+
title: status.request.title ?? null,
|
|
629
|
+
kind: status.request.kind,
|
|
630
|
+
tmdb_id: status.request.tmdb_id,
|
|
631
|
+
episodes_on_disk: status.file.episodes_on_disk ?? null,
|
|
632
|
+
episodes_total: status.file.episodes_total ?? null,
|
|
633
|
+
path: status.file.path,
|
|
634
|
+
size_bytes: status.file.size_bytes,
|
|
635
|
+
play_url: onShelf ? `${JELLYFIN_WEB}/web/index.html#!/search.html?query=${encodeURIComponent(status.request.title ?? "")}` : null,
|
|
636
|
+
diagnosis: status.diagnosis,
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// --------------------------------------------------------- tool: chain health
|
|
641
|
+
|
|
642
|
+
async function mediaChainHealth() {
|
|
643
|
+
const chain = await chainHealth()
|
|
644
|
+
return {
|
|
645
|
+
healthy: chain.healthy,
|
|
646
|
+
indexers: chain.indexers,
|
|
647
|
+
app_sync: chain.apps,
|
|
648
|
+
problems: chain.problems,
|
|
649
|
+
summary: chain.healthy
|
|
650
|
+
? `Acquisition chain healthy: ${chain.indexers.ok.length} of ${chain.indexers.enabled} enabled indexers responding${chain.indexers.failing.length ? `, failing: ${chain.indexers.failing.join(", ")}` : ""}.`
|
|
651
|
+
: `Acquisition chain DOWN. ${chain.problems.map((p) => p.message).join(" ")}`,
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
// ------------------------------------------------------------------- schema
|
|
656
|
+
|
|
657
|
+
const TOOLS = [
|
|
658
|
+
{
|
|
659
|
+
name: "media_search",
|
|
660
|
+
description: "Search or browse the household media catalogue. Use `query` for a title lookup; use `genres`/`keywords`/`year_range` to browse by vibe (e.g. keywords ['autumn','thanksgiving'] for cozy fall films). `on_shelf: true` restricts to what is already downloaded, `false` to what is not, omit for both. Every item says whether it is on the shelf and carries its tmdb_id so a follow-up media_request needs no second lookup.",
|
|
661
|
+
inputSchema: { type: "object", properties: {
|
|
662
|
+
query: { type: "string", description: "Title or free text. Omit to browse by filters." },
|
|
663
|
+
kind: { type: "string", enum: ["movie", "series", "any"], description: "Default any." },
|
|
664
|
+
year_range: { type: "array", items: { type: "number" }, minItems: 2, maxItems: 2, description: "[minYear, maxYear]" },
|
|
665
|
+
genres: { type: "array", items: { type: "string" }, description: "TMDB genre names, e.g. ['Romance','Drama']." },
|
|
666
|
+
keywords: { type: "array", items: { type: "string" }, description: "TMDB keywords, e.g. ['autumn','small town']. Best lever for mood/vibe requests." },
|
|
667
|
+
on_shelf: { type: "boolean", description: "true = only what we already have; false = only what we do not; omit = both." },
|
|
668
|
+
limit: { type: "number", description: "1-20, default 12." },
|
|
669
|
+
sort: { type: "string", enum: ["relevance", "release_year"] },
|
|
670
|
+
} },
|
|
671
|
+
},
|
|
672
|
+
{
|
|
673
|
+
name: "media_request",
|
|
674
|
+
description: "Request a film or series so it gets downloaded. Give tmdb_id when known (from media_search), otherwise title plus optional year. Idempotent: if it was already requested you get approval_state 'already_requested' and duplicate_of, never a duplicate. Returns the downstream sonarr_id/radarr_id so media_request_status needs no re-resolution.",
|
|
675
|
+
inputSchema: { type: "object", properties: {
|
|
676
|
+
tmdb_id: { type: "number" },
|
|
677
|
+
title: { type: "string" },
|
|
678
|
+
year: { type: "number" },
|
|
679
|
+
kind: { type: "string", enum: ["movie", "series"] },
|
|
680
|
+
season_number: { type: "number", description: "Series only. Omit to request all seasons." },
|
|
681
|
+
} },
|
|
682
|
+
},
|
|
683
|
+
{
|
|
684
|
+
name: "media_request_status",
|
|
685
|
+
description: "One call, every stage: request, indexer search, download, file on disk, plus acquisition-chain health and a deterministic `diagnosis` block naming what is stuck, why, and the fix. Use this for any 'is it here yet?' or 'why hasn't X downloaded?' question. A queue row is not proof of progress: a release sitting at zero bytes comes back as stalled, not as downloading. Never infer the cause yourself — read diagnosis.summary.",
|
|
686
|
+
inputSchema: { type: "object", properties: {
|
|
687
|
+
request_id: { type: "string", description: "e.g. jellyseerr:42" },
|
|
688
|
+
tmdb_id: { type: "number" },
|
|
689
|
+
title: { type: "string" },
|
|
690
|
+
} },
|
|
691
|
+
},
|
|
692
|
+
{
|
|
693
|
+
name: "media_diagnose_and_fix",
|
|
694
|
+
description: "Act on a stuck request. Omit `action` to apply the fix that media_request_status already identified, including 'blocklist_and_research' for a release that has stalled at zero bytes. Refuses to act (result 'rejected_fix_unsafe') when the acquisition chain itself is down, because re-searching cannot help then. Use dry_run to preview.",
|
|
695
|
+
inputSchema: { type: "object", properties: {
|
|
696
|
+
request_id: { type: "string" }, tmdb_id: { type: "number" }, title: { type: "string" },
|
|
697
|
+
action: { type: "string", enum: ["rescan", "enable_monitoring", "force_import_scan", "blocklist_and_research", "report_only"] },
|
|
698
|
+
dry_run: { type: "boolean" },
|
|
699
|
+
} },
|
|
700
|
+
},
|
|
701
|
+
{
|
|
702
|
+
name: "media_chain_health",
|
|
703
|
+
description: "Health of the whole acquisition chain: Prowlarr indexers, their failure state, and whether Prowlarr is syncing to Sonarr/Radarr. Call this when several things are stuck at once, or before telling anyone a specific title is the problem — a dead chain blocks everything and is the single most likely cause.",
|
|
704
|
+
inputSchema: { type: "object", properties: {} },
|
|
705
|
+
},
|
|
706
|
+
{
|
|
707
|
+
name: "media_play_or_resolve",
|
|
708
|
+
description: "Resolve something on the shelf to a playable link plus what is actually on disk (episode counts, size, path). If it is not playable you get the same diagnosis block explaining why.",
|
|
709
|
+
inputSchema: { type: "object", properties: {
|
|
710
|
+
request_id: { type: "string" }, tmdb_id: { type: "number" }, title: { type: "string" },
|
|
711
|
+
} },
|
|
712
|
+
},
|
|
713
|
+
]
|
|
714
|
+
|
|
715
|
+
const HANDLERS = {
|
|
716
|
+
media_search: mediaSearch,
|
|
717
|
+
media_request: mediaRequest,
|
|
718
|
+
media_request_status: mediaRequestStatus,
|
|
719
|
+
media_diagnose_and_fix: mediaDiagnoseAndFix,
|
|
720
|
+
media_chain_health: mediaChainHealth,
|
|
721
|
+
media_play_or_resolve: mediaPlayOrResolve,
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
// ------------------------------------------------------------ jsonrpc stdio
|
|
725
|
+
|
|
726
|
+
function send(msg) { process.stdout.write(JSON.stringify(msg) + "\n") }
|
|
727
|
+
|
|
728
|
+
async function handle(msg) {
|
|
729
|
+
const { id, method, params } = msg
|
|
730
|
+
if (method === "initialize") {
|
|
731
|
+
return send({ jsonrpc: "2.0", id, result: {
|
|
732
|
+
protocolVersion: "2024-11-05",
|
|
733
|
+
capabilities: { tools: {} },
|
|
734
|
+
serverInfo: { name: "sanctuary-media", version: "1.0.0" },
|
|
735
|
+
} })
|
|
736
|
+
}
|
|
737
|
+
if (method === "initialized" || method === "notifications/initialized") return
|
|
738
|
+
if (method === "tools/list") return send({ jsonrpc: "2.0", id, result: { tools: TOOLS } })
|
|
739
|
+
if (method === "tools/call") {
|
|
740
|
+
const fn = HANDLERS[params?.name]
|
|
741
|
+
if (!fn) return send({ jsonrpc: "2.0", id, result: { isError: true, content: [{ type: "text", text: `Unknown tool: ${params?.name}` }] } })
|
|
742
|
+
if (credError) {
|
|
743
|
+
const payload = { error: "credentials_unavailable", message: credError, human_action_required: true,
|
|
744
|
+
human_action_reason: credError }
|
|
745
|
+
return send({ jsonrpc: "2.0", id, result: { isError: true, content: [{ type: "text", text: JSON.stringify(payload, null, 1) }] } })
|
|
746
|
+
}
|
|
747
|
+
try {
|
|
748
|
+
const out = await fn(params.arguments ?? {})
|
|
749
|
+
return send({ jsonrpc: "2.0", id, result: { content: [{ type: "text", text: JSON.stringify(out, null, 1) }] } })
|
|
750
|
+
} catch (e) {
|
|
751
|
+
const payload = { error: e.code ?? "tool_failed", message: e.message,
|
|
752
|
+
human_action_required: true,
|
|
753
|
+
human_action_reason: `The media tool could not complete: ${e.message}. Report this failure rather than guessing the answer.` }
|
|
754
|
+
return send({ jsonrpc: "2.0", id, result: { isError: true, content: [{ type: "text", text: JSON.stringify(payload, null, 1) }] } })
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
if (id !== undefined) send({ jsonrpc: "2.0", id, error: { code: -32601, message: `Method not found: ${method}` } })
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
let buf = ""
|
|
761
|
+
let inFlight = 0
|
|
762
|
+
let stdinClosed = false
|
|
763
|
+
|
|
764
|
+
// Exit only when stdin is closed AND nothing is still running. Exiting on 'end'
|
|
765
|
+
// alone drops in-flight tool calls, which silently truncates a slow answer.
|
|
766
|
+
function exitWhenIdle() {
|
|
767
|
+
if (stdinClosed && inFlight === 0) process.exit(0)
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
// Only run the stdio server when invoked directly (node media-mcp.mjs, exactly
|
|
771
|
+
// how agent.json launches it). Guarding this lets the test suite import the
|
|
772
|
+
// module for computeDownloadState/diagnose without attaching to stdin or exiting
|
|
773
|
+
// the test process. The launch is a plain node <abs-path>, so argv[1] is the
|
|
774
|
+
// file's own path and this comparison holds.
|
|
775
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
776
|
+
process.stdin.setEncoding("utf8")
|
|
777
|
+
process.stdin.on("data", (chunk) => {
|
|
778
|
+
buf += chunk
|
|
779
|
+
let nl
|
|
780
|
+
while ((nl = buf.indexOf("\n")) >= 0) {
|
|
781
|
+
const line = buf.slice(0, nl).trim()
|
|
782
|
+
buf = buf.slice(nl + 1)
|
|
783
|
+
if (!line) continue
|
|
784
|
+
let msg
|
|
785
|
+
try { msg = JSON.parse(line) } catch { continue }
|
|
786
|
+
inFlight += 1
|
|
787
|
+
handle(msg)
|
|
788
|
+
.catch((e) => {
|
|
789
|
+
if (msg.id !== undefined) send({ jsonrpc: "2.0", id: msg.id, error: { code: -32603, message: e.message } })
|
|
790
|
+
})
|
|
791
|
+
.finally(() => { inFlight -= 1; exitWhenIdle() })
|
|
792
|
+
}
|
|
793
|
+
})
|
|
794
|
+
process.stdin.on("end", () => { stdinClosed = true; exitWhenIdle() })
|
|
795
|
+
}
|