@ouro.bot/cli 0.1.0-alpha.82 → 0.1.0-alpha.821

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