@cohortapp/agent-sdk 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (731) hide show
  1. package/.claude/commands/init-agent.md +104 -0
  2. package/.claude/commands/init-maestro.md +1187 -0
  3. package/.claude/settings.json +161 -0
  4. package/.env.example +216 -0
  5. package/README.md +632 -0
  6. package/agents/browser-operator/agent.md +52 -0
  7. package/agents/calendar-ops/agent.md +50 -0
  8. package/agents/communications/agent.md +96 -0
  9. package/agents/decision-log/agent.md +65 -0
  10. package/agents/desktop-operator/agent.md +59 -0
  11. package/agents/gmail-operator/agent.md +62 -0
  12. package/agents/inbound-dispatcher/agent.md +66 -0
  13. package/agents/inbox-processor/agent.md +39 -0
  14. package/agents/pmo-execution/agent.md +60 -0
  15. package/agents/session-spawner/agent.md +64 -0
  16. package/agents/slack-operator/agent.md +60 -0
  17. package/agents/whatsapp-operator/agent.md +60 -0
  18. package/agents/workflow-automation/agent.md +61 -0
  19. package/archetypes/altitudes/c-suite.yaml +58 -0
  20. package/archetypes/altitudes/founder.yaml +68 -0
  21. package/archetypes/altitudes/senior-manager.yaml +63 -0
  22. package/archetypes/altitudes/svp.yaml +60 -0
  23. package/archetypes/altitudes/vp.yaml +50 -0
  24. package/archetypes/archetype.schema.json +77 -0
  25. package/archetypes/base.yaml +47 -0
  26. package/archetypes/capabilities/commercial-leader.yaml +159 -0
  27. package/archetypes/capabilities/compliance-officer.yaml +159 -0
  28. package/archetypes/capabilities/executive-operator.yaml +169 -0
  29. package/archetypes/capabilities/finance-leader.yaml +162 -0
  30. package/archetypes/capabilities/operations-leader.yaml +154 -0
  31. package/archetypes/capabilities/product-leader.yaml +148 -0
  32. package/archetypes/capabilities/technical-leader.yaml +146 -0
  33. package/archetypes/functions/commercial-leader.yaml +62 -0
  34. package/archetypes/functions/compliance-officer.yaml +64 -0
  35. package/archetypes/functions/executive-operator.yaml +70 -0
  36. package/archetypes/functions/finance-leader.yaml +70 -0
  37. package/archetypes/functions/operations-leader.yaml +62 -0
  38. package/archetypes/functions/product-leader.yaml +61 -0
  39. package/archetypes/functions/technical-leader.yaml +57 -0
  40. package/bin/cohort-mcp.mjs +81 -0
  41. package/bin/maestro.mjs +3516 -0
  42. package/bin/maestro.test.mjs +1015 -0
  43. package/desktop-control/README.md +56 -0
  44. package/desktop-control/app-profiles/gmail.yaml +120 -0
  45. package/desktop-control/app-profiles/slack.yaml +315 -0
  46. package/desktop-control/app-profiles/whatsapp.yaml +107 -0
  47. package/docs/architecture/agent-topology.md +2239 -0
  48. package/docs/architecture/archetype-agent-factory.md +110 -0
  49. package/docs/architecture/collective-memory-and-org-mesh.md +115 -0
  50. package/docs/architecture/continuous-monitoring.md +221 -0
  51. package/docs/architecture/mcp-capability-map.md +585 -0
  52. package/docs/architecture/system-architecture.md +1272 -0
  53. package/docs/company-context/README.md +40 -0
  54. package/docs/guides/agent-persona-setup.md +600 -0
  55. package/docs/guides/agents-observe-setup.md +64 -0
  56. package/docs/guides/billing-console-keys.md +88 -0
  57. package/docs/guides/ccxray-diagnostics.md +65 -0
  58. package/docs/guides/channel-bus.md +127 -0
  59. package/docs/guides/claude-mem-setup.md +79 -0
  60. package/docs/guides/claude-pace-setup.md +56 -0
  61. package/docs/guides/claudraband-sessions.md +98 -0
  62. package/docs/guides/clawteam-swarm.md +116 -0
  63. package/docs/guides/code-review-graph-setup.md +86 -0
  64. package/docs/guides/email-setup.md +431 -0
  65. package/docs/guides/mac-mini.md +119 -0
  66. package/docs/guides/media-generation-setup.md +349 -0
  67. package/docs/guides/model-routing.md +162 -0
  68. package/docs/guides/observability-otel.md +265 -0
  69. package/docs/guides/org-onboarding.md +132 -0
  70. package/docs/guides/outbound-governance-setup.md +437 -0
  71. package/docs/guides/pdf-generation-setup.md +315 -0
  72. package/docs/guides/poller-daemon-setup.md +563 -0
  73. package/docs/guides/rag-context-setup.md +459 -0
  74. package/docs/guides/self-optimization-pattern.md +82 -0
  75. package/docs/guides/setup-wizard.md +178 -0
  76. package/docs/guides/slack-setup.md +350 -0
  77. package/docs/guides/telegram-setup.md +227 -0
  78. package/docs/guides/twilio-subaccounts-setup.md +223 -0
  79. package/docs/guides/verification.md +128 -0
  80. package/docs/guides/voice-mode.md +188 -0
  81. package/docs/guides/voice-sms-setup.md +698 -0
  82. package/docs/guides/webhook-relay-setup.md +349 -0
  83. package/docs/guides/whatsapp-setup.md +288 -0
  84. package/docs/prompts/board-pack-cover-template.md +36 -0
  85. package/docs/prompts/decision-recommendation-template.md +88 -0
  86. package/docs/prompts/followup-message-template.md +141 -0
  87. package/docs/prompts/investor-letter-template.md +52 -0
  88. package/docs/prompts/morning-brief-template.md +82 -0
  89. package/docs/prompts/presentation-template.md +58 -0
  90. package/docs/prompts/weekly-strategic-memo-template.md +104 -0
  91. package/docs/research/hallucinated-tool-output-investigation.md +151 -0
  92. package/docs/runbooks/backup-restore.md +205 -0
  93. package/docs/runbooks/cohort-cutover.md +129 -0
  94. package/docs/runbooks/fleet-operations.md +200 -0
  95. package/docs/runbooks/incident-response.md +226 -0
  96. package/docs/runbooks/mac-mini-bootstrap.md +431 -0
  97. package/docs/runbooks/perpetual-operations.md +509 -0
  98. package/docs/runbooks/recovery-and-failover.md +260 -0
  99. package/framework-features.json +267 -0
  100. package/ingest/README.md +87 -0
  101. package/lib/action-executor.js +689 -0
  102. package/lib/action-executor.test.mjs +871 -0
  103. package/lib/agent-root.mjs +37 -0
  104. package/lib/archetype.mjs +236 -0
  105. package/lib/archetype.test.mjs +132 -0
  106. package/lib/autonomy.mjs +114 -0
  107. package/lib/autonomy.test.mjs +66 -0
  108. package/lib/backlog.mjs +358 -0
  109. package/lib/backlog.test.mjs +266 -0
  110. package/lib/budget-guard.mjs +279 -0
  111. package/lib/budget-guard.test.mjs +291 -0
  112. package/lib/cadence-bus-schedule.test.mjs +194 -0
  113. package/lib/cadence-bus.mjs +1120 -0
  114. package/lib/cadence-bus.test.mjs +720 -0
  115. package/lib/cadences.mjs +205 -0
  116. package/lib/cadences.test.mjs +125 -0
  117. package/lib/capability.mjs +154 -0
  118. package/lib/capability.test.mjs +78 -0
  119. package/lib/channels/base-adapter.mjs +719 -0
  120. package/lib/channels/base-adapter.test.mjs +590 -0
  121. package/lib/channels/channel.mjs +128 -0
  122. package/lib/channels/channels.test.mjs +371 -0
  123. package/lib/channels/contract.mjs +215 -0
  124. package/lib/channels/contract.test.mjs +137 -0
  125. package/lib/channels/conversation-resolver.mjs +95 -0
  126. package/lib/channels/gmail/adapter.mjs +87 -0
  127. package/lib/channels/inbox-item.mjs +255 -0
  128. package/lib/channels/inbox-item.test.mjs +335 -0
  129. package/lib/channels/index.mjs +94 -0
  130. package/lib/channels/orgmail/adapter.mjs +353 -0
  131. package/lib/channels/orgmail/adapter.test.mjs +311 -0
  132. package/lib/channels/pairing.mjs +363 -0
  133. package/lib/channels/pairing.test.mjs +270 -0
  134. package/lib/channels/registry.mjs +164 -0
  135. package/lib/channels/slack/adapter.mjs +317 -0
  136. package/lib/channels/slack-adapter.test.mjs +212 -0
  137. package/lib/channels/sms/adapter.mjs +43 -0
  138. package/lib/channels/telegram/adapter.mjs +432 -0
  139. package/lib/channels/telegram-adapter.test.mjs +306 -0
  140. package/lib/channels/voice/adapter.mjs +301 -0
  141. package/lib/channels/voice/adapter.test.mjs +278 -0
  142. package/lib/channels/whatsapp/adapter-baileys.mjs +587 -0
  143. package/lib/channels/whatsapp/adapter-baileys.test.mjs +359 -0
  144. package/lib/channels/whatsapp/adapter-twilio.mjs +65 -0
  145. package/lib/channels/whatsapp/baileys-typing.test.mjs +154 -0
  146. package/lib/charter.mjs +256 -0
  147. package/lib/charter.test.mjs +89 -0
  148. package/lib/claude-bin.mjs +134 -0
  149. package/lib/claude-bin.test.mjs +75 -0
  150. package/lib/collective/capture.mjs +185 -0
  151. package/lib/collective/capture.test.mjs +121 -0
  152. package/lib/collective/cards.mjs +201 -0
  153. package/lib/collective/cards.test.mjs +114 -0
  154. package/lib/collective/config.mjs +186 -0
  155. package/lib/collective/config.test.mjs +123 -0
  156. package/lib/collective/global-config.mjs +113 -0
  157. package/lib/collective/global-config.test.mjs +75 -0
  158. package/lib/collective/presence.mjs +201 -0
  159. package/lib/collective/presence.test.mjs +95 -0
  160. package/lib/collective/recall.mjs +215 -0
  161. package/lib/collective/recall.test.mjs +116 -0
  162. package/lib/comms/send-gate.mjs +554 -0
  163. package/lib/comms/send-gate.test.mjs +577 -0
  164. package/lib/comms.mjs +67 -0
  165. package/lib/comms.test.mjs +41 -0
  166. package/lib/diagnostics/alerts.mjs +424 -0
  167. package/lib/diagnostics/alerts.test.mjs +318 -0
  168. package/lib/diagnostics/backup-freshness.mjs +188 -0
  169. package/lib/diagnostics/backup-freshness.test.mjs +185 -0
  170. package/lib/diagnostics/counters.mjs +269 -0
  171. package/lib/diagnostics/counters.test.mjs +206 -0
  172. package/lib/diagnostics/events.mjs +188 -0
  173. package/lib/diagnostics/events.test.mjs +290 -0
  174. package/lib/diagnostics/otel.mjs +237 -0
  175. package/lib/diagnostics/otel.test.mjs +196 -0
  176. package/lib/diagnostics/trace.mjs +216 -0
  177. package/lib/diagnostics/trace.test.mjs +251 -0
  178. package/lib/env-compat.mjs +74 -0
  179. package/lib/env-compat.test.mjs +104 -0
  180. package/lib/feature-init.mjs +331 -0
  181. package/lib/fs-atomic.mjs +112 -0
  182. package/lib/fs-atomic.test.mjs +72 -0
  183. package/lib/fs-ownership.mjs +111 -0
  184. package/lib/fs-ownership.test.mjs +158 -0
  185. package/lib/hooks/bus.mjs +347 -0
  186. package/lib/hooks/bus.test.mjs +387 -0
  187. package/lib/index.js +16 -0
  188. package/lib/learning/config.mjs +106 -0
  189. package/lib/learning/config.test.mjs +75 -0
  190. package/lib/learning/counters.mjs +156 -0
  191. package/lib/learning/counters.test.mjs +69 -0
  192. package/lib/learning/curator-consolidate.test.mjs +238 -0
  193. package/lib/learning/curator.mjs +453 -0
  194. package/lib/learning/curator.test.mjs +106 -0
  195. package/lib/learning/log.mjs +40 -0
  196. package/lib/learning/reflect.mjs +534 -0
  197. package/lib/learning/reflect.test.mjs +0 -0
  198. package/lib/learning/session-index.mjs +352 -0
  199. package/lib/learning/session-index.test.mjs +125 -0
  200. package/lib/learning/skill-writer.mjs +474 -0
  201. package/lib/learning/skill-writer.test.mjs +210 -0
  202. package/lib/mcp/server.mjs +328 -0
  203. package/lib/mcp/server.test.mjs +400 -0
  204. package/lib/model-router/auth-profiles.mjs +758 -0
  205. package/lib/model-router/auth-profiles.test.mjs +580 -0
  206. package/lib/model-router/catalog/anthropic.yaml +153 -0
  207. package/lib/model-router/catalog/deepseek.yaml +86 -0
  208. package/lib/model-router/catalog/moonshot.yaml +81 -0
  209. package/lib/model-router/catalog/qwen.yaml +114 -0
  210. package/lib/model-router/catalog.mjs +925 -0
  211. package/lib/model-router/catalog.test.mjs +385 -0
  212. package/lib/model-router/economics.mjs +564 -0
  213. package/lib/model-router/economics.test.mjs +344 -0
  214. package/lib/model-router/failover.mjs +298 -0
  215. package/lib/model-router/failover.test.mjs +439 -0
  216. package/lib/model-router/health.mjs +453 -0
  217. package/lib/model-router/health.test.mjs +338 -0
  218. package/lib/model-router/integration-coverage.test.mjs +829 -0
  219. package/lib/model-router/integration.test.mjs +564 -0
  220. package/lib/model-router/ledger.mjs +402 -0
  221. package/lib/model-router/ledger.test.mjs +382 -0
  222. package/lib/model-router/llm-task.mjs +515 -0
  223. package/lib/model-router/llm-task.test.mjs +392 -0
  224. package/lib/model-router/org-credentials.mjs +260 -0
  225. package/lib/model-router/org-credentials.test.mjs +265 -0
  226. package/lib/model-router/pricing-refresh.mjs +463 -0
  227. package/lib/model-router/pricing-refresh.test.mjs +286 -0
  228. package/lib/model-router/reconcile.mjs +429 -0
  229. package/lib/model-router/reconcile.test.mjs +316 -0
  230. package/lib/model-router/repair.mjs +471 -0
  231. package/lib/model-router/repair.test.mjs +180 -0
  232. package/lib/model-router/resolve.mjs +1206 -0
  233. package/lib/model-router/spawn.mjs +497 -0
  234. package/lib/model-router/spawn.test.mjs +425 -0
  235. package/lib/model-router/taxonomy.mjs +893 -0
  236. package/lib/model-router/taxonomy.test.mjs +410 -0
  237. package/lib/model-router.mjs +677 -0
  238. package/lib/model-router.test.mjs +907 -0
  239. package/lib/org/activity.mjs +211 -0
  240. package/lib/org/activity.test.mjs +134 -0
  241. package/lib/org/approvals.mjs +448 -0
  242. package/lib/org/approvals.test.mjs +216 -0
  243. package/lib/org/awareness.mjs +222 -0
  244. package/lib/org/awareness.test.mjs +159 -0
  245. package/lib/org/board.mjs +229 -0
  246. package/lib/org/board.test.mjs +177 -0
  247. package/lib/org/bootstrap-context.mjs +169 -0
  248. package/lib/org/bootstrap-context.test.mjs +153 -0
  249. package/lib/org/client.mjs +1628 -0
  250. package/lib/org/client.test.mjs +1107 -0
  251. package/lib/org/cohort-client.mjs +67 -0
  252. package/lib/org/cohort-client.test.mjs +126 -0
  253. package/lib/org/cost-sync.mjs +227 -0
  254. package/lib/org/cost-sync.test.mjs +153 -0
  255. package/lib/org/doctor.mjs +212 -0
  256. package/lib/org/doctor.test.mjs +212 -0
  257. package/lib/org/handoff.mjs +293 -0
  258. package/lib/org/handoff.test.mjs +269 -0
  259. package/lib/org/integration-tools.mjs +182 -0
  260. package/lib/org/integration-tools.test.mjs +160 -0
  261. package/lib/org/keys.mjs +131 -0
  262. package/lib/org/keys.test.mjs +92 -0
  263. package/lib/org/knowledge.mjs +463 -0
  264. package/lib/org/knowledge.test.mjs +319 -0
  265. package/lib/org/leases.mjs +335 -0
  266. package/lib/org/leases.test.mjs +235 -0
  267. package/lib/org/mesh-integration.test.mjs +127 -0
  268. package/lib/org/mesh.mjs +459 -0
  269. package/lib/org/mesh.test.mjs +345 -0
  270. package/lib/org/messaging.mjs +503 -0
  271. package/lib/org/messaging.test.mjs +238 -0
  272. package/lib/org/policy.mjs +345 -0
  273. package/lib/org/policy.test.mjs +237 -0
  274. package/lib/org/protocol.checksum +1 -0
  275. package/lib/org/protocol.checksum.test.mjs +90 -0
  276. package/lib/org/protocol.mjs +967 -0
  277. package/lib/org/protocol.test.mjs +264 -0
  278. package/lib/org/registry.mjs +194 -0
  279. package/lib/org/registry.test.mjs +100 -0
  280. package/lib/org/tool-surface-integration.test.mjs +120 -0
  281. package/lib/org/tool-surface.mjs +2535 -0
  282. package/lib/org/tool-surface.test.mjs +589 -0
  283. package/lib/org/ui-parity.mjs +3236 -0
  284. package/lib/org/ui-parity.test.mjs +348 -0
  285. package/lib/org/verify.mjs +176 -0
  286. package/lib/org/verify.test.mjs +194 -0
  287. package/lib/rag/embed.mjs +188 -0
  288. package/lib/rag/indexer.mjs +425 -0
  289. package/lib/rag/rag.test.mjs +505 -0
  290. package/lib/rag/search.mjs +475 -0
  291. package/lib/rate-guard.mjs +246 -0
  292. package/lib/rate-guard.test.mjs +201 -0
  293. package/lib/render.mjs +112 -0
  294. package/lib/render.test.mjs +68 -0
  295. package/lib/resource-governor.mjs +297 -0
  296. package/lib/resource-governor.test.mjs +262 -0
  297. package/lib/scheduling/dynamic-jobs.mjs +675 -0
  298. package/lib/scheduling/dynamic-jobs.test.mjs +344 -0
  299. package/lib/scheduling/jitter.mjs +0 -0
  300. package/lib/scheduling/jitter.test.mjs +140 -0
  301. package/lib/secrets/broker.mjs +315 -0
  302. package/lib/secrets/broker.test.mjs +280 -0
  303. package/lib/secrets/providers.mjs +461 -0
  304. package/lib/secrets/providers.test.mjs +274 -0
  305. package/lib/security/audit-engine.mjs +684 -0
  306. package/lib/security/audit-engine.test.mjs +389 -0
  307. package/lib/security/coerce-args.mjs +552 -0
  308. package/lib/security/coerce-args.test.mjs +281 -0
  309. package/lib/security/dangerous-tools.mjs +97 -0
  310. package/lib/security/dangerous-tools.test.mjs +68 -0
  311. package/lib/security/external-content.mjs +145 -0
  312. package/lib/security/external-content.test.mjs +67 -0
  313. package/lib/security/redact.mjs +592 -0
  314. package/lib/security/redact.test.mjs +441 -0
  315. package/lib/security/secret-equal.mjs +73 -0
  316. package/lib/security/secret-equal.test.mjs +55 -0
  317. package/lib/session-permissions.mjs +101 -0
  318. package/lib/session-permissions.test.mjs +100 -0
  319. package/lib/setup/claude-probe.mjs +74 -0
  320. package/lib/setup/completeness.mjs +175 -0
  321. package/lib/setup/completeness.test.mjs +110 -0
  322. package/lib/setup/context-pack.mjs +173 -0
  323. package/lib/setup/context-pack.test.mjs +89 -0
  324. package/lib/setup/enrich.mjs +277 -0
  325. package/lib/setup/enrich.test.mjs +115 -0
  326. package/lib/setup/enroll-from-cohort.mjs +441 -0
  327. package/lib/setup/enroll-from-cohort.test.mjs +233 -0
  328. package/lib/setup/integration.test.mjs +162 -0
  329. package/lib/setup/io.mjs +360 -0
  330. package/lib/setup/io.test.mjs +77 -0
  331. package/lib/setup/run-generator.mjs +81 -0
  332. package/lib/setup/runner.mjs +244 -0
  333. package/lib/setup/runner.test.mjs +132 -0
  334. package/lib/setup/sections/comms.mjs +173 -0
  335. package/lib/setup/sections/company.mjs +120 -0
  336. package/lib/setup/sections/enrich.mjs +138 -0
  337. package/lib/setup/sections/identity.mjs +182 -0
  338. package/lib/setup/sections/identity.test.mjs +140 -0
  339. package/lib/setup/sections/learning.mjs +153 -0
  340. package/lib/setup/sections/learning.test.mjs +81 -0
  341. package/lib/setup/sections/messaging.mjs +219 -0
  342. package/lib/setup/sections/messaging.test.mjs +127 -0
  343. package/lib/setup/sections/model.mjs +102 -0
  344. package/lib/setup/sections/operating-model.mjs +78 -0
  345. package/lib/setup/sections/org.mjs +475 -0
  346. package/lib/setup/sections/org.test.mjs +313 -0
  347. package/lib/setup/sections/orgmail.mjs +173 -0
  348. package/lib/setup/sections/orgmail.test.mjs +118 -0
  349. package/lib/setup/sections/recovery.mjs +159 -0
  350. package/lib/setup/sections/recovery.test.mjs +98 -0
  351. package/lib/setup/sections/tools.mjs +132 -0
  352. package/lib/setup/sections/verify.mjs +97 -0
  353. package/lib/setup/sot.mjs +205 -0
  354. package/lib/setup/sot.test.mjs +81 -0
  355. package/lib/setup/state.mjs +151 -0
  356. package/lib/setup/state.test.mjs +92 -0
  357. package/lib/singleton.js +229 -0
  358. package/lib/singleton.test.mjs +135 -0
  359. package/lib/telemetry/alerts.mjs +216 -0
  360. package/lib/telemetry/alerts.test.mjs +109 -0
  361. package/lib/telemetry/collect.mjs +512 -0
  362. package/lib/telemetry/collect.test.mjs +202 -0
  363. package/lib/tool-definitions-integration.test.mjs +83 -0
  364. package/lib/tool-definitions.js +738 -0
  365. package/lib/tool-definitions.test.mjs +437 -0
  366. package/lib/util/fetch-timeout.mjs +136 -0
  367. package/lib/util/fetch-timeout.test.mjs +202 -0
  368. package/lib/util/reconnect.mjs +343 -0
  369. package/lib/util/reconnect.test.mjs +369 -0
  370. package/lib/util/unhandled.mjs +205 -0
  371. package/lib/util/unhandled.test.mjs +216 -0
  372. package/lib/voice/context-loader.mjs +466 -0
  373. package/lib/voice/index.mjs +100 -0
  374. package/lib/voice/openai-realtime.mjs +510 -0
  375. package/lib/voice/outbound.mjs +542 -0
  376. package/lib/voice/outbound.test.mjs +69 -0
  377. package/lib/voice/post-call-brief.mjs +428 -0
  378. package/lib/voice/provider.mjs +52 -0
  379. package/lib/voice/session-rotation.mjs +257 -0
  380. package/lib/voice/stt.mjs +161 -0
  381. package/lib/voice/stt.test.mjs +226 -0
  382. package/lib/voice/tool-bridge.mjs +370 -0
  383. package/lib/voice/tts.mjs +104 -0
  384. package/lib/voice/twilio-sip-bridge.mjs +288 -0
  385. package/lib/voice/voice.test.mjs +990 -0
  386. package/mcp/README.md +80 -0
  387. package/package.json +151 -0
  388. package/plugins/maestro-skills/plugin.json +139 -0
  389. package/plugins/maestro-skills/skills/agents-observe.md +110 -0
  390. package/plugins/maestro-skills/skills/board-deck.md +68 -0
  391. package/plugins/maestro-skills/skills/books-close.md +77 -0
  392. package/plugins/maestro-skills/skills/brand-steward.md +121 -0
  393. package/plugins/maestro-skills/skills/calendar-plan.md +57 -0
  394. package/plugins/maestro-skills/skills/call-working-sessions.md +124 -0
  395. package/plugins/maestro-skills/skills/ccxray-diagnostics.md +91 -0
  396. package/plugins/maestro-skills/skills/claude-pace.md +61 -0
  397. package/plugins/maestro-skills/skills/code-review-graph.md +99 -0
  398. package/plugins/maestro-skills/skills/crm-pipeline.md +65 -0
  399. package/plugins/maestro-skills/skills/decision-brief.md +89 -0
  400. package/plugins/maestro-skills/skills/directory-hygiene.md +125 -0
  401. package/plugins/maestro-skills/skills/draft-comms.md +84 -0
  402. package/plugins/maestro-skills/skills/evening-wrap.md +53 -0
  403. package/plugins/maestro-skills/skills/files-find.md +65 -0
  404. package/plugins/maestro-skills/skills/generative-ui.md +228 -0
  405. package/plugins/maestro-skills/skills/hiring-triage.md +74 -0
  406. package/plugins/maestro-skills/skills/inbox-triage.md +61 -0
  407. package/plugins/maestro-skills/skills/mail-triage.md +86 -0
  408. package/plugins/maestro-skills/skills/morning-brief.md +54 -0
  409. package/plugins/maestro-skills/skills/native-artifacts.md +157 -0
  410. package/plugins/maestro-skills/skills/org-board.md +133 -0
  411. package/plugins/maestro-skills/skills/org-credential.md +68 -0
  412. package/plugins/maestro-skills/skills/org-recall.md +81 -0
  413. package/plugins/maestro-skills/skills/pipeline-review.md +76 -0
  414. package/plugins/maestro-skills/skills/regulatory-status.md +81 -0
  415. package/plugins/maestro-skills/skills/router-why.md +78 -0
  416. package/plugins/maestro-skills/skills/schedule-meeting.md +91 -0
  417. package/plugins/maestro-skills/skills/session-search.md +71 -0
  418. package/plugins/maestro-skills/skills/set-reminder.md +93 -0
  419. package/plugins/maestro-skills/skills/slack-followup.md +64 -0
  420. package/plugins/maestro-skills/skills/team-activity.md +86 -0
  421. package/plugins/maestro-skills/skills/weekly-memo.md +70 -0
  422. package/policies/action-classification.yaml +114 -0
  423. package/policies/ai-disclosure.yaml +294 -0
  424. package/policies/communication-style.md +139 -0
  425. package/policies/information-barriers.yaml +118 -0
  426. package/policies/prompt-injection-defence.yaml +138 -0
  427. package/public/assets/icon-dark.png +0 -0
  428. package/public/assets/icon-dark.svg +9 -0
  429. package/public/assets/icon-light.svg +9 -0
  430. package/public/assets/logo-dark.svg +15 -0
  431. package/public/assets/logo-light.svg +15 -0
  432. package/scaffold/.mcp.json +7 -0
  433. package/scaffold/CLAUDE.md +368 -0
  434. package/scaffold/config/agent.json +55 -0
  435. package/scaffold/config/agent.ts +76 -0
  436. package/scaffold/config/agent.ts.example +89 -0
  437. package/scaffold/config/alerts.yaml +23 -0
  438. package/scaffold/config/allowlist.yaml.example +25 -0
  439. package/scaffold/config/caller-id-map.yaml +46 -0
  440. package/scaffold/config/collective.yaml +49 -0
  441. package/scaffold/config/company.json +20 -0
  442. package/scaffold/config/known-agents.json +6 -0
  443. package/scaffold/config/learning.yaml +55 -0
  444. package/scaffold/config/model-routing.yaml.example +104 -0
  445. package/scaffold/config/org.yaml +25 -0
  446. package/scaffold/config/orgmail.yaml.example +19 -0
  447. package/scaffold/config/recovery.yaml +72 -0
  448. package/scaffold/config/secrets.yaml +27 -0
  449. package/scaffold/config/slack.yaml.example +35 -0
  450. package/scaffold/config/telegram.yaml.example +38 -0
  451. package/scaffold/config/voice.yaml.example +89 -0
  452. package/scaffold/config/whatsapp.yaml.example +39 -0
  453. package/schedules/README.md +49 -0
  454. package/schedules/triggers/backlog-executor.md +102 -0
  455. package/schedules/triggers/brand-steward.md +72 -0
  456. package/schedules/triggers/daily-evening-wrap.md +159 -0
  457. package/schedules/triggers/daily-midday-sweep.md +58 -0
  458. package/schedules/triggers/daily-morning-brief.md +55 -0
  459. package/schedules/triggers/directory-hygiene.md +81 -0
  460. package/schedules/triggers/dynamic-jobs.md +40 -0
  461. package/schedules/triggers/inbox-processor.md +115 -0
  462. package/schedules/triggers/meeting-action-capture.md +60 -0
  463. package/schedules/triggers/meeting-prep.md +69 -0
  464. package/schedules/triggers/messaging-inbound.md +50 -0
  465. package/schedules/triggers/org-pulse.md +24 -0
  466. package/schedules/triggers/quarterly-self-assessment.md +54 -0
  467. package/schedules/triggers/weekly-engineering-health.md +37 -0
  468. package/schedules/triggers/weekly-execution.md +65 -0
  469. package/schedules/triggers/weekly-hiring.md +53 -0
  470. package/schedules/triggers/weekly-priorities.md +38 -0
  471. package/schedules/triggers/weekly-strategic-memo.md +124 -0
  472. package/scripts/archive-email.sh +55 -0
  473. package/scripts/cadence/cadence-status.mjs +36 -0
  474. package/scripts/cadence/enqueue-cadence-tick.mjs +174 -0
  475. package/scripts/cadence/enqueue-cadence-tick.test.mjs +187 -0
  476. package/scripts/cadence/launchd-cadence-wrapper.sh +85 -0
  477. package/scripts/cadence/launchd-cloud-relay-wrapper.sh +95 -0
  478. package/scripts/cadence/launchd-socket-mode-wrapper.sh +95 -0
  479. package/scripts/ci/check-docs-accuracy.mjs +493 -0
  480. package/scripts/ci/check-docs-accuracy.test.mjs +409 -0
  481. package/scripts/ci/check-exports-exist.mjs +140 -0
  482. package/scripts/ci/check-files-exist.mjs +107 -0
  483. package/scripts/ci/check-no-build-artifacts.mjs +111 -0
  484. package/scripts/ci/check-no-build-artifacts.test.mjs +71 -0
  485. package/scripts/ci/check-no-confidential.mjs +198 -0
  486. package/scripts/ci/check-no-conflict-markers.mjs +169 -0
  487. package/scripts/ci/check-no-residual-identity.mjs +163 -0
  488. package/scripts/ci/check-no-residual-identity.test.mjs +89 -0
  489. package/scripts/ci/check-tarball-fidelity.mjs +205 -0
  490. package/scripts/ci/check-unresolved-tokens.mjs +83 -0
  491. package/scripts/ci/check.mjs +109 -0
  492. package/scripts/ci/check.test.mjs +194 -0
  493. package/scripts/ci/run-coverage.mjs +82 -0
  494. package/scripts/ci/run-tests.mjs +71 -0
  495. package/scripts/cloud-relay/README.md +59 -0
  496. package/scripts/cloud-relay/index.mjs +233 -0
  497. package/scripts/cloud-relay/package.json +15 -0
  498. package/scripts/cloud-relay/railway.json +13 -0
  499. package/scripts/cloud-relay/voice/README.md +94 -0
  500. package/scripts/cloud-relay/voice/package-lock.json +39 -0
  501. package/scripts/cloud-relay/voice/package.json +16 -0
  502. package/scripts/cloud-relay/voice/railway.json +13 -0
  503. package/scripts/cloud-relay/voice/server.mjs +532 -0
  504. package/scripts/collective/hook-runner.mjs +211 -0
  505. package/scripts/collective/hook-runner.test.mjs +90 -0
  506. package/scripts/collective/org-pulse.mjs +72 -0
  507. package/scripts/collective/org-sync.mjs +61 -0
  508. package/scripts/collective/recall.mjs +45 -0
  509. package/scripts/collective/who.mjs +30 -0
  510. package/scripts/comms-monitor.sh +288 -0
  511. package/scripts/configure-whatsapp-sandbox.sh +201 -0
  512. package/scripts/continuous-monitor.sh +91 -0
  513. package/scripts/cost/fleet-digest.mjs +407 -0
  514. package/scripts/cost/fleet-digest.test.mjs +207 -0
  515. package/scripts/cost/track-claude-usage.mjs +169 -0
  516. package/scripts/daemon/agent-daemon.mjs +989 -0
  517. package/scripts/daemon/agent-daemon.test.mjs +525 -0
  518. package/scripts/daemon/cadence-consumer-governance.test.mjs +220 -0
  519. package/scripts/daemon/cadence-consumer.mjs +1080 -0
  520. package/scripts/daemon/cadence-consumer.test.mjs +770 -0
  521. package/scripts/daemon/cadence-handlers.mjs +1121 -0
  522. package/scripts/daemon/cadence-handlers.test.mjs +617 -0
  523. package/scripts/daemon/classifier.mjs +704 -0
  524. package/scripts/daemon/classifier.test.mjs +238 -0
  525. package/scripts/daemon/classify-kind.mjs +54 -0
  526. package/scripts/daemon/classify-kind.test.mjs +40 -0
  527. package/scripts/daemon/context-compiler.mjs +605 -0
  528. package/scripts/daemon/context-compiler.test.mjs +300 -0
  529. package/scripts/daemon/dispatcher-cooldown.test.mjs +122 -0
  530. package/scripts/daemon/dispatcher-governance.test.mjs +886 -0
  531. package/scripts/daemon/dispatcher.mjs +1516 -0
  532. package/scripts/daemon/health.mjs +72 -0
  533. package/scripts/daemon/inbox-deferral.mjs +210 -0
  534. package/scripts/daemon/inbox-deferral.test.mjs +242 -0
  535. package/scripts/daemon/integration.test.mjs +149 -0
  536. package/scripts/daemon/launchd-wrapper-generic.sh +96 -0
  537. package/scripts/daemon/launchd-wrapper-slack-events.sh +37 -0
  538. package/scripts/daemon/launchd-wrapper.sh +91 -0
  539. package/scripts/daemon/lib/session-router.mjs +274 -0
  540. package/scripts/daemon/lib/session-router.test.mjs +295 -0
  541. package/scripts/daemon/maestro-daemon.mjs +275 -0
  542. package/scripts/daemon/prompt-builder.mjs +685 -0
  543. package/scripts/daemon/prompt-builder.test.mjs +213 -0
  544. package/scripts/daemon/responder.mjs +854 -0
  545. package/scripts/daemon/session-lock.mjs +721 -0
  546. package/scripts/daemon/session-lock.test.mjs +252 -0
  547. package/scripts/daemon/session-outcomes.mjs +640 -0
  548. package/scripts/daemon/session-outcomes.test.mjs +533 -0
  549. package/scripts/daemon/typing-registry.mjs +90 -0
  550. package/scripts/daemon/typing-registry.test.mjs +77 -0
  551. package/scripts/daemon/voice-webhook-server.mjs +804 -0
  552. package/scripts/decisions/capture-decision.mjs +116 -0
  553. package/scripts/disclosure_assessment.py +873 -0
  554. package/scripts/disclosure_boundaries.py +562 -0
  555. package/scripts/email-signature-principal.html +52 -0
  556. package/scripts/email-signature.html +60 -0
  557. package/scripts/email_quote_thread.py +167 -0
  558. package/scripts/email_thread_dedup.py +362 -0
  559. package/scripts/emergency-stop.sh +81 -0
  560. package/scripts/healthcheck.sh +116 -0
  561. package/scripts/hooks/block-mcp-cohort-send.sh +15 -0
  562. package/scripts/hooks/block-mcp-slack-send.sh +7 -0
  563. package/scripts/hooks/post-action-log.sh +126 -0
  564. package/scripts/hooks/pre-send-audit.sh +174 -0
  565. package/scripts/hooks/pre-send-audit.test.mjs +215 -0
  566. package/scripts/hooks/session-end-log.sh +27 -0
  567. package/scripts/hooks/session-start-banner.sh +115 -0
  568. package/scripts/huddle/audio-bridge.mjs +664 -0
  569. package/scripts/huddle/boot-slack-cdp.sh +102 -0
  570. package/scripts/huddle/huddle-controller.mjs +942 -0
  571. package/scripts/huddle/huddle-server.mjs +1229 -0
  572. package/scripts/huddle/launch-slack.sh +232 -0
  573. package/scripts/huddle/openai-realtime-bridge.mjs +462 -0
  574. package/scripts/huddle/package-lock.json +62 -0
  575. package/scripts/huddle/package.json +22 -0
  576. package/scripts/huddle/setup-audio.sh +239 -0
  577. package/scripts/huddle/start-call.mjs +318 -0
  578. package/scripts/huddle/test-pipeline.mjs +263 -0
  579. package/scripts/learning/consolidate-skills.mjs +72 -0
  580. package/scripts/learning/session-search.mjs +125 -0
  581. package/scripts/llm_email_dedup.py +442 -0
  582. package/scripts/local-triggers/generate-plists.sh +432 -0
  583. package/scripts/local-triggers/generate-plists.test.mjs +413 -0
  584. package/scripts/local-triggers/install-all.sh +49 -0
  585. package/scripts/local-triggers/plists/.gitkeep +0 -0
  586. package/scripts/local-triggers/run-trigger.sh +63 -0
  587. package/scripts/local-triggers/templates/rag-reindex.plist.template +47 -0
  588. package/scripts/local-triggers/templates/voice-relay-poller.plist.template +54 -0
  589. package/scripts/local-triggers/templates/voice-tunnel.plist.template +55 -0
  590. package/scripts/local-triggers/templates/voice-webhook.plist.template +51 -0
  591. package/scripts/maintenance/backup-to-cloud.sh +124 -0
  592. package/scripts/maintenance/health-check.sh +377 -0
  593. package/scripts/media-generation/README.md +105 -0
  594. package/scripts/media-generation/gemini-image-client.mjs +173 -0
  595. package/scripts/media-generation/generate-assets.mjs +289 -0
  596. package/scripts/media-generation/veo-video-client.mjs +219 -0
  597. package/scripts/org/send-orgmail.mjs +227 -0
  598. package/scripts/outbound-dedup-cleanup.sh +43 -0
  599. package/scripts/outbound-dedup.sh +477 -0
  600. package/scripts/outbound_dedup.py +115 -0
  601. package/scripts/parse-voice-transcript.mjs +481 -0
  602. package/scripts/pdf-generation/README.md +63 -0
  603. package/scripts/pdf-generation/build-document.mjs +247 -0
  604. package/scripts/pdf-generation/templates/board-pack.latex +136 -0
  605. package/scripts/pdf-generation/templates/corporate-letter.latex +126 -0
  606. package/scripts/pdf-generation/templates/memo.latex +114 -0
  607. package/scripts/poll-slack-events.sh +35 -0
  608. package/scripts/poller/calendar-poller.mjs +12 -0
  609. package/scripts/poller/gmail-poller.mjs +192 -0
  610. package/scripts/poller/imap-client.mjs +289 -0
  611. package/scripts/poller/inbox-scan-poller.mjs +156 -0
  612. package/scripts/poller/inbox-scan-poller.test.mjs +231 -0
  613. package/scripts/poller/index.mjs +73 -0
  614. package/scripts/poller/intra-session-check.mjs +285 -0
  615. package/scripts/poller/lib/cloud-relay-dedup.mjs +88 -0
  616. package/scripts/poller/lib/cloud-relay-dedup.test.mjs +133 -0
  617. package/scripts/poller/lib/slash-command-handlers.mjs +177 -0
  618. package/scripts/poller/secondary-gmail-poller.mjs +132 -0
  619. package/scripts/poller/slack-cloud-relay-client.mjs +368 -0
  620. package/scripts/poller/slack-poller.mjs +854 -0
  621. package/scripts/poller/slack-socket-mode.mjs +917 -0
  622. package/scripts/poller/slack-socket-mode.test.mjs +753 -0
  623. package/scripts/poller/trigger.mjs +75 -0
  624. package/scripts/poller/utils.mjs +371 -0
  625. package/scripts/poller/voice-cloud-relay-client.mjs +179 -0
  626. package/scripts/poller/voice-poller.mjs +236 -0
  627. package/scripts/poller-launchd/install.sh +66 -0
  628. package/scripts/poller-launchd/poller.plist.template +40 -0
  629. package/scripts/poller-launchd/whatsapp-handler.plist.template +39 -0
  630. package/scripts/post-interaction-indexer.py +1598 -0
  631. package/scripts/pre-draft-context.py +994 -0
  632. package/scripts/pre_draft_lookup.py +258 -0
  633. package/scripts/rag/build-index.mjs +47 -0
  634. package/scripts/rag/ingest.mjs +111 -0
  635. package/scripts/rag/search.mjs +119 -0
  636. package/scripts/rag-indexer.py +629 -0
  637. package/scripts/restore-from-backup.sh +248 -0
  638. package/scripts/restore-from-backup.test.mjs +178 -0
  639. package/scripts/resume-operations.sh +80 -0
  640. package/scripts/search-secondary-inbox.py +181 -0
  641. package/scripts/secondary-inbox-poller.py +437 -0
  642. package/scripts/self-optimization/compute-metrics.py +398 -0
  643. package/scripts/send-email-as-principal.py +369 -0
  644. package/scripts/send-email-threaded.py +392 -0
  645. package/scripts/send-email-with-attachment.py +377 -0
  646. package/scripts/send-email.sh +131 -0
  647. package/scripts/send-sms.sh +175 -0
  648. package/scripts/send-whatsapp.sh +292 -0
  649. package/scripts/session-start.sh +106 -0
  650. package/scripts/setup/boot-claude-session.sh +94 -0
  651. package/scripts/setup/configure-macos.sh +674 -0
  652. package/scripts/setup/configure-twilio-sip-trunk.mjs +207 -0
  653. package/scripts/setup/configure-voice-tunnel.mjs +182 -0
  654. package/scripts/setup/generate-agent-env.mjs +92 -0
  655. package/scripts/setup/generate-agent-package-json.mjs +222 -0
  656. package/scripts/setup/generate-agent-package-json.test.mjs +143 -0
  657. package/scripts/setup/generate-autonomy.mjs +60 -0
  658. package/scripts/setup/generate-backlog.mjs +101 -0
  659. package/scripts/setup/generate-cadences.mjs +92 -0
  660. package/scripts/setup/generate-capability.mjs +162 -0
  661. package/scripts/setup/generate-charter.mjs +76 -0
  662. package/scripts/setup/generate-comms.mjs +59 -0
  663. package/scripts/setup/generate-company.mjs +92 -0
  664. package/scripts/setup/init-agent.sh +547 -0
  665. package/scripts/setup/init-agent.test.mjs +151 -0
  666. package/scripts/setup/init-archetype.mjs +74 -0
  667. package/scripts/setup/init-backup.mjs +54 -0
  668. package/scripts/setup/init-cadence-bus.mjs +60 -0
  669. package/scripts/setup/init-channel-bus.mjs +46 -0
  670. package/scripts/setup/init-cost-tracking.mjs +45 -0
  671. package/scripts/setup/init-decision-capture.mjs +66 -0
  672. package/scripts/setup/init-known-agents.mjs +57 -0
  673. package/scripts/setup/init-learning.mjs +70 -0
  674. package/scripts/setup/init-memory-executive.mjs +45 -0
  675. package/scripts/setup/init-model-router.mjs +124 -0
  676. package/scripts/setup/init-rag.mjs +174 -0
  677. package/scripts/setup/init-session-router.mjs +38 -0
  678. package/scripts/setup/init-slack-socket-mode.mjs +260 -0
  679. package/scripts/setup/init-telegram.mjs +165 -0
  680. package/scripts/setup/init-voice-realtime.mjs +204 -0
  681. package/scripts/setup/init-whatsapp-baileys.mjs +77 -0
  682. package/scripts/setup/install-dev-tools.sh +150 -0
  683. package/scripts/setup/lib/install-plist.mjs +95 -0
  684. package/scripts/setup/migrate-agent-to-sot.mjs +192 -0
  685. package/scripts/setup/render-environment-yaml.mjs +133 -0
  686. package/scripts/slack-events-ctl.sh +177 -0
  687. package/scripts/slack-events-server.mjs +1045 -0
  688. package/scripts/slack-react.mjs +89 -0
  689. package/scripts/slack-responded.sh +232 -0
  690. package/scripts/slack-send.sh +287 -0
  691. package/scripts/slack-typing.mjs +196 -0
  692. package/scripts/slack-upload-v2.py +95 -0
  693. package/scripts/sms-handler.mjs +450 -0
  694. package/scripts/spawn-session.sh +120 -0
  695. package/scripts/sync-protocol.mjs +217 -0
  696. package/scripts/system-verify.sh +184 -0
  697. package/scripts/test-email-thread-dedup.py +239 -0
  698. package/scripts/test-information-barriers.py +484 -0
  699. package/scripts/test-llm-email-dedup.py +251 -0
  700. package/scripts/test-pre-draft-integration.py +203 -0
  701. package/scripts/test-rag-phase2.sh +442 -0
  702. package/scripts/test-rag-search.sh +251 -0
  703. package/scripts/test-voice-parser.mjs +316 -0
  704. package/scripts/user-context-search.py +659 -0
  705. package/scripts/validate_outbound.py +1504 -0
  706. package/scripts/watchdog/ai.maestro.memory-watchdog.plist +41 -0
  707. package/scripts/watchdog/force-reboot.sh +157 -0
  708. package/scripts/watchdog/memory-watchdog.sh +473 -0
  709. package/scripts/whatsapp-handler.mjs +538 -0
  710. package/teams/desktop-operations.yaml +34 -0
  711. package/teams/executive-office.yaml +27 -0
  712. package/teams/legal-and-regulatory.yaml +24 -0
  713. package/teams/platform-and-engineering.yaml +23 -0
  714. package/teams/strategy-and-growth.yaml +29 -0
  715. package/workflows/continuous/backlog-executor.yaml +141 -0
  716. package/workflows/continuous/inbound-monitor.yaml +168 -0
  717. package/workflows/daily/applicant-triage.yaml +197 -0
  718. package/workflows/daily/comms-triage.yaml +80 -0
  719. package/workflows/daily/evening-wrap.yaml +105 -0
  720. package/workflows/daily/morning-brief.yaml +164 -0
  721. package/workflows/daily/slack-followup-sweep.yaml +87 -0
  722. package/workflows/event-driven/README.md +50 -0
  723. package/workflows/event-driven/agent-failure-investigation.yaml +137 -0
  724. package/workflows/event-driven/pr-review.yaml +107 -0
  725. package/workflows/monthly/board-readiness.yaml +76 -0
  726. package/workflows/quarterly/strategic-scenario-analysis.yaml +85 -0
  727. package/workflows/session-protocol.md +171 -0
  728. package/workflows/weekly/engineering-health.yaml +154 -0
  729. package/workflows/weekly/hiring-review.yaml +169 -0
  730. package/workflows/weekly/rollup-pipeline-review.yaml +76 -0
  731. package/workflows/weekly/strategic-memo.yaml +79 -0
@@ -0,0 +1,542 @@
1
+ /**
2
+ * Maestro — Outbound voice message dispatcher.
3
+ *
4
+ * Delivers a synthesized voice message to a peer over the right transport:
5
+ *
6
+ * WhatsApp (E.164 phone via Baileys) ── inline voice note (PTT, opus)
7
+ * Slack (channel / DM ID) ── audio file upload to channel
8
+ * PSTN (E.164 phone via Twilio) ── one-shot Twilio <Play> call
9
+ * Live conversational PSTN ── Twilio outbound + OpenAI SIP bridge
10
+ *
11
+ * The agent doesn't need to know which transport will be picked: the
12
+ * `to` field is parsed and we route accordingly. Operators can force a
13
+ * specific transport via the `transport` option.
14
+ *
15
+ * Public API:
16
+ * sendVoiceMessage(opts) → { transport, message_id, duration_ms, cost_usd }
17
+ * placeOutboundCall(opts) → { call_id, twilio_call_sid, status, context_delivered } (Stage 2)
18
+ *
19
+ * Cost notes:
20
+ * - TTS: ~$0.015/1k chars (gpt-4o-mini-tts)
21
+ * - WhatsApp Baileys send: free (your own WA Web session)
22
+ * - Slack upload: free
23
+ * - Twilio outbound: ~$0.013-0.014/min for US numbers, varies internationally
24
+ * - OpenAI Realtime via SIP: $0.40-0.80/min
25
+ *
26
+ * Operator-level safety: this module honours policies/action-classification.yaml
27
+ * indirectly — it's expected to be called via lib/voice/tool-bridge.mjs or
28
+ * via the daemon's action-executor, both of which already gate by risk.
29
+ */
30
+
31
+ import { writeFileSync, mkdirSync, existsSync, readFileSync } from "node:fs";
32
+ import { join, resolve } from "node:path";
33
+ import { tmpdir } from "node:os";
34
+ import { synthesizeSpeech, pickDefaultVoice, ttsCostEstimate } from "./tts.mjs";
35
+
36
+ const TWILIO_API_BASE = "https://api.twilio.com/2010-04-01";
37
+
38
+ // ---------------------------------------------------------------------------
39
+ // Transport detection
40
+ // ---------------------------------------------------------------------------
41
+
42
+ const E164_REGEX = /^\+[1-9]\d{6,14}$/;
43
+ const SLACK_CHANNEL_REGEX = /^[CDG][A-Z0-9]{8,}$/; // C/D/G prefix: channel/DM/group
44
+
45
+ /**
46
+ * Decide which transport to use for an outbound voice message.
47
+ *
48
+ * Order of preference when only `to` is given:
49
+ * 1. Slack channel/DM id (C/D/G prefix) → slack
50
+ * 2. WhatsApp JID ("...@s.whatsapp.net") or
51
+ * E.164 phone when WhatsApp is configured → whatsapp
52
+ * 3. E.164 phone otherwise → pstn (Twilio)
53
+ */
54
+ export function pickTransport(opts) {
55
+ if (opts.transport) return opts.transport;
56
+ const to = String(opts.to || "").trim();
57
+ if (!to) throw new Error("pickTransport: to required");
58
+
59
+ if (to.includes("@s.whatsapp.net") || to.includes("@g.us")) return "whatsapp";
60
+ if (SLACK_CHANNEL_REGEX.test(to)) return "slack";
61
+ if (E164_REGEX.test(to)) {
62
+ // Prefer WhatsApp if the agent has the Baileys auth state populated;
63
+ // operators can override per-call by passing transport: "pstn".
64
+ if (opts.agentRoot && existsSync(join(opts.agentRoot, "state/whatsapp/auth/creds.json"))) {
65
+ return "whatsapp";
66
+ }
67
+ return "pstn";
68
+ }
69
+ throw new Error(`pickTransport: cannot infer transport for "${to}" (use transport: option to force)`);
70
+ }
71
+
72
+ // ---------------------------------------------------------------------------
73
+ // sendVoiceMessage — top-level entry point
74
+ // ---------------------------------------------------------------------------
75
+
76
+ /**
77
+ * @typedef {object} VoiceMessageOpts
78
+ * @property {string} to Destination (E.164, Slack id, or WA jid)
79
+ * @property {string} text Spoken content (max 4096 chars)
80
+ * @property {string} [voice] TTS voice (default: pickDefaultVoice from agent config)
81
+ * @property {string} [transport] Force "whatsapp" | "slack" | "pstn" (defaults: auto)
82
+ * @property {string} [agentRoot] Agent repo root (default: $AGENT_ROOT)
83
+ * @property {string} [model] TTS model (default: gpt-4o-mini-tts)
84
+ * @property {string} [format] Audio format (default: per-transport)
85
+ * @property {object} [meta] Transport-specific metadata (slack channel id, etc.)
86
+ */
87
+
88
+ /**
89
+ * @param {VoiceMessageOpts} opts
90
+ * @returns {Promise<{transport, message_id, audio_bytes, cost_usd}>}
91
+ */
92
+ export async function sendVoiceMessage(opts = {}) {
93
+ const agentRoot = resolve(opts.agentRoot || process.env.AGENT_ROOT || process.env.AGENT_DIR || process.cwd());
94
+ const voiceConfig = loadAgentVoiceConfig(agentRoot);
95
+ const transport = pickTransport({ ...opts, agentRoot });
96
+
97
+ // Per-transport defaults: WhatsApp wants opus PTT, Slack/PSTN are fine with mp3.
98
+ const fmt = opts.format || (transport === "whatsapp" ? "opus" : "mp3");
99
+ const voice = opts.voice || pickDefaultVoice(voiceConfig);
100
+
101
+ const audio = await synthesizeSpeech({
102
+ text: opts.text,
103
+ voice,
104
+ format: fmt,
105
+ model: opts.model,
106
+ });
107
+ const costUsd = ttsCostEstimate(opts.text, opts.model);
108
+
109
+ switch (transport) {
110
+ case "whatsapp": {
111
+ const result = await deliverViaWhatsApp({ to: opts.to, audio, fmt, agentRoot, text: opts.text });
112
+ return { transport, ...result, cost_usd: costUsd };
113
+ }
114
+ case "slack": {
115
+ const result = await deliverViaSlack({ to: opts.to, audio, fmt, agentRoot, text: opts.text, meta: opts.meta });
116
+ return { transport, ...result, cost_usd: costUsd };
117
+ }
118
+ case "pstn": {
119
+ const result = await deliverViaPSTN({ to: opts.to, audio, fmt, agentRoot, text: opts.text });
120
+ return { transport, ...result, cost_usd: costUsd };
121
+ }
122
+ default:
123
+ throw new Error(`sendVoiceMessage: unknown transport "${transport}"`);
124
+ }
125
+ }
126
+
127
+ // ---------------------------------------------------------------------------
128
+ // WhatsApp (Baileys) — sends an inline voice note (PTT)
129
+ // ---------------------------------------------------------------------------
130
+
131
+ async function deliverViaWhatsApp({ to, audio, fmt, agentRoot, text }) {
132
+ // Reuse the channel-bus registry — the daemon already keeps a Baileys
133
+ // session alive there. We import lazily so this module doesn't pull
134
+ // baileys for non-WA use cases.
135
+ const channelsMod = await import("../channels/index.mjs");
136
+ let channel = channelsMod.getChannel?.("whatsapp");
137
+ if (!channel) {
138
+ throw new Error(
139
+ "WhatsApp channel not registered. Either rename config/whatsapp.yaml.disabled → .yaml " +
140
+ "and restart the daemon, or pass transport: 'pstn' for Twilio."
141
+ );
142
+ }
143
+ if (typeof channel.send !== "function") {
144
+ throw new Error("WhatsApp channel has no send() method");
145
+ }
146
+ // Cache the audio to disk for replay/audit.
147
+ const outboundDir = join(agentRoot, "state/whatsapp/outbound");
148
+ mkdirSync(outboundDir, { recursive: true });
149
+ const filename = `${Date.now()}-voice-${slugify(text).slice(0, 24)}.${fmt}`;
150
+ writeFileSync(join(outboundDir, filename), audio);
151
+
152
+ const result = await channel.send({
153
+ to,
154
+ text: text, // human-readable fallback if the recipient's client can't play audio
155
+ attachments: [
156
+ {
157
+ type: "voice_note",
158
+ mime: fmt === "opus" ? "audio/ogg; codecs=opus" : "audio/mpeg",
159
+ buffer: audio,
160
+ },
161
+ ],
162
+ });
163
+ return {
164
+ message_id: result?.message_id || filename,
165
+ audio_bytes: audio.length,
166
+ cached_at: join(outboundDir, filename),
167
+ };
168
+ }
169
+
170
+ // ---------------------------------------------------------------------------
171
+ // Slack — uploads the audio as a file attachment
172
+ // ---------------------------------------------------------------------------
173
+
174
+ async function deliverViaSlack({ to, audio, fmt, agentRoot, text: textArg, meta }) {
175
+ // Coerce text defensively: callers may pass null/undefined/non-string and we
176
+ // immediately .slice() it for the filename / title / initial_comment below.
177
+ const text = String(textArg || "");
178
+ const token = process.env.SLACK_USER_TOKEN || process.env.SLACK_BOT_TOKEN || process.env.SLACK_TOKEN;
179
+ if (!token) throw new Error("Slack delivery requires SLACK_USER_TOKEN or SLACK_BOT_TOKEN");
180
+
181
+ const ext = fmt === "opus" ? "ogg" : fmt;
182
+ const filename = `voice-${slugify(text).slice(0, 32)}.${ext}`;
183
+
184
+ // Slack's modern flow: files.getUploadURLExternal → PUT audio → files.completeUploadExternal
185
+ // We use it because files.upload was deprecated in 2024.
186
+ const getUrl = `https://slack.com/api/files.getUploadURLExternal?length=${audio.length}&filename=${encodeURIComponent(filename)}`;
187
+ const getResp = await (await fetch(getUrl, {
188
+ method: "POST",
189
+ headers: { Authorization: `Bearer ${token}` },
190
+ })).json();
191
+ if (!getResp.ok) throw new Error(`slack getUploadURL failed: ${JSON.stringify(getResp)}`);
192
+
193
+ const putResp = await fetch(getResp.upload_url, {
194
+ method: "POST",
195
+ headers: { "Content-Type": fmt === "opus" ? "audio/ogg" : "audio/mpeg" },
196
+ body: audio,
197
+ });
198
+ if (!putResp.ok) throw new Error(`slack file upload failed: HTTP ${putResp.status}`);
199
+
200
+ const completeBody = {
201
+ files: [{ id: getResp.file_id, title: text.slice(0, 80) }],
202
+ channel_id: to,
203
+ initial_comment: text,
204
+ };
205
+ if (meta?.thread_ref) completeBody.thread_ts = meta.thread_ref;
206
+
207
+ const completeResp = await (await fetch("https://slack.com/api/files.completeUploadExternal", {
208
+ method: "POST",
209
+ headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
210
+ body: JSON.stringify(completeBody),
211
+ })).json();
212
+ if (!completeResp.ok) throw new Error(`slack files.completeUploadExternal: ${JSON.stringify(completeResp)}`);
213
+
214
+ return {
215
+ message_id: getResp.file_id,
216
+ audio_bytes: audio.length,
217
+ file_url: completeResp.files?.[0]?.permalink || null,
218
+ };
219
+ }
220
+
221
+ // ---------------------------------------------------------------------------
222
+ // PSTN (Twilio) — one-shot outbound call that plays the synthesized audio
223
+ // ---------------------------------------------------------------------------
224
+
225
+ async function deliverViaPSTN({ to, audio, fmt, agentRoot, text: textArg }) {
226
+ // Coerce text defensively: it is fed to slugify()/escapeXml() below, both of
227
+ // which expect a string (escapeXml in particular does text.replace()).
228
+ const text = String(textArg || "");
229
+ const sid = process.env.TWILIO_ACCOUNT_SID;
230
+ const token = process.env.TWILIO_AUTH_TOKEN;
231
+ const fromNumber = process.env.TWILIO_FROM_NUMBER || process.env.TWILIO_VOICE_FROM;
232
+ if (!sid || !token) throw new Error("PSTN delivery requires TWILIO_ACCOUNT_SID + TWILIO_AUTH_TOKEN");
233
+ if (!fromNumber) throw new Error("PSTN delivery requires TWILIO_FROM_NUMBER or TWILIO_VOICE_FROM");
234
+
235
+ // For Twilio <Play>, the audio must be reachable via a public HTTPS URL.
236
+ // We host it ourselves at the voice-webhook server's /voice/audio/<id>
237
+ // endpoint, which the voice-webhook server should serve. For now we
238
+ // fall back to Twilio's <Say> TwiML so the call works without a tunnel
239
+ // — the user gets the message spoken by Twilio's TTS instead of our
240
+ // higher-quality OpenAI TTS. Operators with a public webhook URL can
241
+ // configure VOICE_PUBLIC_BASE_URL to use <Play> instead.
242
+ const publicBase = process.env.VOICE_PUBLIC_BASE_URL;
243
+ let twiml;
244
+ if (publicBase) {
245
+ // Cache the audio and reference it via <Play>.
246
+ const cacheDir = join(agentRoot, "state/voice/outbound-audio");
247
+ mkdirSync(cacheDir, { recursive: true });
248
+ const audioId = `${Date.now()}-${slugify(text).slice(0, 16)}`;
249
+ const filename = `${audioId}.${fmt === "opus" ? "ogg" : fmt}`;
250
+ writeFileSync(join(cacheDir, filename), audio);
251
+ const audioUrl = `${publicBase.replace(/\/$/, "")}/voice/audio/${filename}`;
252
+ twiml = `<?xml version="1.0" encoding="UTF-8"?><Response><Play>${audioUrl}</Play></Response>`;
253
+ } else {
254
+ // Fall back to Twilio Say (Twilio synthesizes from text directly).
255
+ const safe = escapeXml(text);
256
+ twiml = `<?xml version="1.0" encoding="UTF-8"?><Response><Say voice="Polly.Joanna">${safe}</Say></Response>`;
257
+ }
258
+
259
+ const auth = Buffer.from(`${sid}:${token}`).toString("base64");
260
+ const params = new URLSearchParams({ To: to, From: fromNumber, Twiml: twiml });
261
+ const r = await fetch(`${TWILIO_API_BASE}/Accounts/${sid}/Calls.json`, {
262
+ method: "POST",
263
+ headers: {
264
+ Authorization: `Basic ${auth}`,
265
+ "Content-Type": "application/x-www-form-urlencoded",
266
+ },
267
+ body: params.toString(),
268
+ });
269
+ const data = await r.json();
270
+ if (!r.ok) throw new Error(`twilio call failed: ${JSON.stringify(data)}`);
271
+ return {
272
+ message_id: data.sid,
273
+ audio_bytes: audio.length,
274
+ twilio_status: data.status,
275
+ };
276
+ }
277
+
278
+ // ---------------------------------------------------------------------------
279
+ // Stage 2 — placeOutboundCall: live conversational PSTN via SIP bridge
280
+ // ---------------------------------------------------------------------------
281
+
282
+ /**
283
+ * Place an outbound PSTN call and bridge it to OpenAI Realtime for a
284
+ * live conversation. The agent's "brief" is loaded as the session
285
+ * instructions so the model knows what to say.
286
+ *
287
+ * Two transports (auto-selected based on env config):
288
+ *
289
+ * 1. **media_streams** (DEFAULT, recommended) — The Twilio call uses
290
+ * TwiML <Connect><Stream> to open a WebSocket to our Railway voice
291
+ * cloud-relay. The relay opens a parallel WebSocket to OpenAI's
292
+ * Realtime API and bridges audio bidirectionally (g711_ulaw on
293
+ * both sides — no transcoding). Works without OpenAI dashboard
294
+ * SIP config and without OpenAI accepting inbound INVITES from
295
+ * Twilio's outbound infrastructure.
296
+ * Requires: VOICE_RELAY_URL, VOICE_TWIML_TOKEN, OPENAI_API_KEY (on Railway).
297
+ *
298
+ * 2. **sip** (legacy / experimental) — TwiML <Dial><Sip>OpenAI</Sip>.
299
+ * Requires OpenAI's project to accept inbound SIP INVITES from
300
+ * arbitrary Twilio outbound — which empirically does not work
301
+ * today (Twilio's outbound SIP gets rejected by OpenAI for missing
302
+ * auth/registration). Kept for future when OpenAI documents the
303
+ * auth scheme.
304
+ * Requires: OPENAI_SIP_URI or TWILIO_SIP_TRUNK_DOMAIN.
305
+ *
306
+ * @param {{ to: string, brief: string, agentRoot?: string, voice?: string,
307
+ * transport?: "media_streams"|"sip", cancelOnContextFail?: boolean }} opts
308
+ * cancelOnContextFail: when true, hard-cancel the dialed call if the brief
309
+ * could not be pre-registered with the relay (default false — call proceeds
310
+ * with the relay's fallback prompt, and context_delivered:false is returned).
311
+ * @returns {Promise<{ call_id, twilio_call_sid, status, transport,
312
+ * context_delivered: boolean }>}
313
+ * context_delivered is false when a media_streams brief failed to register
314
+ * with the relay; true otherwise (including sip / no-relay paths that do not
315
+ * pre-register context).
316
+ */
317
+ export async function placeOutboundCall(opts = {}) {
318
+ const sid = process.env.TWILIO_ACCOUNT_SID;
319
+ const token = process.env.TWILIO_AUTH_TOKEN;
320
+ const fromNumber = process.env.TWILIO_FROM_NUMBER || process.env.TWILIO_VOICE_FROM;
321
+ if (!sid || !token || !fromNumber) {
322
+ throw new Error("placeOutboundCall requires TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN, TWILIO_FROM_NUMBER");
323
+ }
324
+ const agentRoot = resolve(opts.agentRoot || process.env.AGENT_ROOT || process.env.AGENT_DIR || process.cwd());
325
+
326
+ // Pick transport. Default to media_streams when relay is configured
327
+ // (it's the only one that empirically works today). Fall back to SIP
328
+ // only when explicitly requested.
329
+ const relayUrl = (process.env.VOICE_RELAY_URL || "").replace(/\/$/, "");
330
+ const twimlToken = process.env.VOICE_TWIML_TOKEN || "";
331
+ const transport = opts.transport || (relayUrl ? "media_streams" : "sip");
332
+
333
+ const callId = `out-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
334
+
335
+ // Persist brief locally (always — for audit + later post-call processing)
336
+ const briefsDir = join(agentRoot, "state/voice/outbound-briefs");
337
+ mkdirSync(briefsDir, { recursive: true });
338
+ writeFileSync(join(briefsDir, `${callId}.json`), JSON.stringify({
339
+ call_id: callId,
340
+ to: opts.to,
341
+ brief: opts.brief,
342
+ voice: opts.voice || "cedar",
343
+ transport,
344
+ created_at: new Date().toISOString(),
345
+ }, null, 2));
346
+
347
+ // For media_streams, we use URL-based TwiML — Twilio fetches the TwiML
348
+ // from /twilio/twiml when the call connects, instead of us passing it
349
+ // inline. This gives us a server-side log of every TwiML request
350
+ // (proving Twilio is actually reaching Railway) and avoids any
351
+ // inline-TwiML parsing quirks.
352
+ let twiml = null;
353
+ let twimlUrl = null;
354
+ if (transport === "media_streams") {
355
+ if (!relayUrl) {
356
+ throw new Error("placeOutboundCall(media_streams) requires VOICE_RELAY_URL");
357
+ }
358
+ const tokenQp = twimlToken ? `?token=${encodeURIComponent(twimlToken)}` : "";
359
+ twimlUrl = `${relayUrl}/twilio/twiml${tokenQp}`;
360
+ } else if (transport === "sip") {
361
+ const sipTarget = process.env.OPENAI_SIP_URI || process.env.TWILIO_SIP_TRUNK_DOMAIN;
362
+ if (!sipTarget) {
363
+ throw new Error("placeOutboundCall(sip) requires OPENAI_SIP_URI or TWILIO_SIP_TRUNK_DOMAIN");
364
+ }
365
+ const baseUri = sipTarget.startsWith("sip:") ? sipTarget : `sip:${sipTarget}`;
366
+ const sipUri = `${baseUri}?X-Maestro-Call-Id=${encodeURIComponent(callId)}`;
367
+ twiml = `<?xml version="1.0" encoding="UTF-8"?><Response><Dial><Sip>${escapeXml(sipUri)}</Sip></Dial></Response>`;
368
+ } else {
369
+ throw new Error(`unknown transport: ${transport}`);
370
+ }
371
+
372
+ // Initiate the call via Twilio. Use Url= (Twilio fetches TwiML from
373
+ // our endpoint) for media_streams so we get a server-side log of every
374
+ // TwiML request; use Twiml= (inline) for SIP since there's no benefit
375
+ // to a round-trip there.
376
+ const auth = Buffer.from(`${sid}:${token}`).toString("base64");
377
+ const params = new URLSearchParams({ To: opts.to, From: fromNumber });
378
+ if (twimlUrl) {
379
+ params.set("Url", twimlUrl);
380
+ params.set("Method", "GET");
381
+ } else {
382
+ params.set("Twiml", twiml);
383
+ }
384
+ const r = await fetch(`${TWILIO_API_BASE}/Accounts/${sid}/Calls.json`, {
385
+ method: "POST",
386
+ headers: { Authorization: `Basic ${auth}`, "Content-Type": "application/x-www-form-urlencoded" },
387
+ body: params.toString(),
388
+ });
389
+ const data = await r.json();
390
+ if (!r.ok) throw new Error(`twilio outbound failed: ${JSON.stringify(data)}`);
391
+
392
+ // For media_streams transport: pre-register this call's session config
393
+ // with the relay so it's ready when Twilio connects.
394
+ //
395
+ // Critical: we build the FULL contextual system prompt locally (agent
396
+ // identity from config/agent.json, top queue items, recent decisions,
397
+ // RAG hits, peer history, voice-mode rules) and ship it to Railway.
398
+ // The relay only injects strings into session.update — it has no
399
+ // access to ravi-ai's filesystem, so all context must come from here.
400
+ // Whether the call's context (system prompt / identity) successfully
401
+ // pre-registered with the relay. For transports that don't pre-register
402
+ // (sip, or media_streams with no relay) there is nothing to deliver, so the
403
+ // call is not context-dependent and this stays true. For media_streams with
404
+ // a relay it reflects the real outcome of the queue-outbound POST so callers
405
+ // can decide whether to keep the dialed call or hang up.
406
+ let contextDelivered = true;
407
+ if (transport === "media_streams" && relayUrl) {
408
+ contextDelivered = false;
409
+ let instructions = null;
410
+ try {
411
+ const ctxLoader = await import("./context-loader.mjs");
412
+ instructions = await ctxLoader.buildContextualInstructions({
413
+ agentRoot,
414
+ peer: opts.to,
415
+ callKind: "outbound",
416
+ brief: opts.brief,
417
+ });
418
+ } catch (err) {
419
+ // Fallback: hand-crafted minimal prompt that at least disambiguates
420
+ // who's the agent vs the caller. Better than nothing.
421
+ const id = opts.identity || {};
422
+ instructions = [
423
+ `# Identity`,
424
+ `You are ${id.fullName || id.name || "the agent"}, ${id.role || "an executive operator"} at ${id.company || "the company"}.`,
425
+ ``,
426
+ `# Your task on this call`,
427
+ `${opts.brief}`,
428
+ ``,
429
+ `# Caller`,
430
+ `You are calling ${opts.peer_name || opts.to}. They picked up. Greet them by name (NOT by your own name) and proceed with the task above.`,
431
+ ``,
432
+ `# Voice mode rules`,
433
+ `- Speak briefly: 1-2 sentences before pausing.`,
434
+ `- Confirm the listener can hear you on first turn.`,
435
+ `- If asked to invoke a high-risk action, read back the parameters and get verbal confirmation in the same turn before doing it.`,
436
+ ].join("\n");
437
+ console.warn(`[outbound] context-loader failed (${err.message}) — using fallback prompt`);
438
+ }
439
+
440
+ try {
441
+ const queueResp = await fetch(`${relayUrl}/twilio/queue-outbound`, {
442
+ method: "POST",
443
+ headers: {
444
+ "Content-Type": "application/json",
445
+ ...(twimlToken ? { Authorization: `Bearer ${twimlToken}` } : {}),
446
+ },
447
+ body: JSON.stringify({
448
+ call_sid: data.sid,
449
+ call_id: callId,
450
+ peer: opts.to,
451
+ peer_name: opts.peer_name || opts.to,
452
+ brief: opts.brief,
453
+ // Full pre-loaded system prompt (identity + memory + RAG + rules)
454
+ instructions,
455
+ voice: opts.voice || "cedar",
456
+ model: opts.model || "gpt-realtime-2",
457
+ identity: opts.identity || null,
458
+ tools: opts.tools || [],
459
+ }),
460
+ });
461
+ if (queueResp.ok) {
462
+ contextDelivered = true;
463
+ } else {
464
+ const errText = await queueResp.text();
465
+ console.warn(`[outbound] failed to queue brief at relay: HTTP ${queueResp.status} ${errText.slice(0, 200)}`);
466
+ }
467
+ } catch (err) {
468
+ console.warn(`[outbound] queue-outbound failed: ${err.message}`);
469
+ }
470
+
471
+ // The call is already dialing at this point. If the brief could not be
472
+ // pre-registered the relay will fall back to its own default prompt, so the
473
+ // model would speak without this agent's identity/brief. Operators can opt
474
+ // in to hard-cancelling the dialed call in that case via cancelOnContextFail
475
+ // (default off — preserves the existing "let the call proceed" behaviour).
476
+ if (!contextDelivered && opts.cancelOnContextFail) {
477
+ await cancelTwilioCall({ sid, token, callSid: data.sid }).catch((err) => {
478
+ console.warn(`[outbound] failed to cancel call after context-delivery failure: ${err.message}`);
479
+ });
480
+ }
481
+ }
482
+
483
+ return {
484
+ call_id: callId,
485
+ twilio_call_sid: data.sid,
486
+ status: data.status,
487
+ transport,
488
+ context_delivered: contextDelivered,
489
+ };
490
+ }
491
+
492
+ /**
493
+ * Best-effort cancel of an in-flight Twilio call (sets Status=completed).
494
+ * Used when a media_streams brief could not be pre-registered and the operator
495
+ * asked us not to let a context-less call proceed.
496
+ *
497
+ * @param {{ sid: string, token: string, callSid: string }} args
498
+ */
499
+ async function cancelTwilioCall({ sid, token, callSid }) {
500
+ const auth = Buffer.from(`${sid}:${token}`).toString("base64");
501
+ const r = await fetch(`${TWILIO_API_BASE}/Accounts/${sid}/Calls/${encodeURIComponent(callSid)}.json`, {
502
+ method: "POST",
503
+ headers: { Authorization: `Basic ${auth}`, "Content-Type": "application/x-www-form-urlencoded" },
504
+ body: new URLSearchParams({ Status: "completed" }).toString(),
505
+ });
506
+ if (!r.ok) {
507
+ const t = await r.text();
508
+ throw new Error(`twilio cancel failed: HTTP ${r.status} ${t.slice(0, 200)}`);
509
+ }
510
+ }
511
+
512
+ // ---------------------------------------------------------------------------
513
+ // Helpers
514
+ // ---------------------------------------------------------------------------
515
+
516
+ function loadAgentVoiceConfig(agentRoot) {
517
+ const path = join(agentRoot, "config/voice.yaml");
518
+ if (!existsSync(path)) return {};
519
+ try {
520
+ // Tolerant minimal parse — we only need the top-level `voice:` key.
521
+ const body = readFileSync(path, "utf-8");
522
+ const m = body.match(/^voice:\s*([^\s#]+)/m);
523
+ return { voice: m ? m[1].trim() : null };
524
+ } catch {
525
+ return {};
526
+ }
527
+ }
528
+
529
+ function slugify(s) {
530
+ return String(s || "")
531
+ .toLowerCase()
532
+ .replace(/[^a-z0-9]+/g, "-")
533
+ .replace(/^-|-$/g, "");
534
+ }
535
+
536
+ function escapeXml(s) {
537
+ return String(s).replace(/[<>&"']/g, (c) => ({
538
+ "<": "&lt;", ">": "&gt;", "&": "&amp;", '"': "&quot;", "'": "&apos;",
539
+ }[c]));
540
+ }
541
+
542
+ export default { sendVoiceMessage, placeOutboundCall, pickTransport };
@@ -0,0 +1,69 @@
1
+ /**
2
+ * outbound.test.mjs — node:test coverage for outbound voice.
3
+ *
4
+ * No real network: synthesizeSpeech, Baileys, Slack, Twilio all stubbed
5
+ * via dependency injection / process.env manipulation.
6
+ */
7
+
8
+ import { test } from "node:test";
9
+ import assert from "node:assert/strict";
10
+
11
+ import { pickTransport } from "./outbound.mjs";
12
+ import { pickDefaultVoice, ttsCostEstimate } from "./tts.mjs";
13
+
14
+ // ---------------------------------------------------------------------------
15
+ // pickTransport
16
+ // ---------------------------------------------------------------------------
17
+
18
+ test("pickTransport: explicit override wins", () => {
19
+ assert.equal(pickTransport({ to: "+14155551234", transport: "pstn" }), "pstn");
20
+ assert.equal(pickTransport({ to: "C12345678", transport: "whatsapp" }), "whatsapp");
21
+ });
22
+
23
+ test("pickTransport: Slack channel id (C/D/G prefix)", () => {
24
+ assert.equal(pickTransport({ to: "C012345678" }), "slack");
25
+ assert.equal(pickTransport({ to: "D099N1JE7NE" }), "slack");
26
+ assert.equal(pickTransport({ to: "GABC123456" }), "slack");
27
+ });
28
+
29
+ test("pickTransport: WhatsApp JID", () => {
30
+ assert.equal(pickTransport({ to: "447825004124@s.whatsapp.net" }), "whatsapp");
31
+ assert.equal(pickTransport({ to: "120363041234567890@g.us" }), "whatsapp");
32
+ });
33
+
34
+ test("pickTransport: E.164 without WA → pstn", () => {
35
+ // agentRoot pointing at a tmp dir without state/whatsapp/auth/creds.json
36
+ assert.equal(pickTransport({ to: "+14155551234", agentRoot: "/tmp/no-wa-here-xyz" }), "pstn");
37
+ });
38
+
39
+ test("pickTransport: refuses ambiguous targets", () => {
40
+ assert.throws(() => pickTransport({ to: "" }), /to required/);
41
+ assert.throws(() => pickTransport({ to: "Alex" }), /cannot infer/);
42
+ assert.throws(() => pickTransport({ to: "alex@example.com" }), /cannot infer/);
43
+ });
44
+
45
+ // ---------------------------------------------------------------------------
46
+ // TTS helpers
47
+ // ---------------------------------------------------------------------------
48
+
49
+ test("pickDefaultVoice: maps Realtime-only voices to TTS equivalents", () => {
50
+ assert.equal(pickDefaultVoice({ voice: "cedar" }), "ash");
51
+ assert.equal(pickDefaultVoice({ voice: "marin" }), "shimmer");
52
+ });
53
+
54
+ test("pickDefaultVoice: passes through valid TTS voices", () => {
55
+ for (const v of ["alloy", "ash", "ballad", "coral", "echo", "sage", "shimmer", "verse"]) {
56
+ assert.equal(pickDefaultVoice({ voice: v }), v);
57
+ }
58
+ });
59
+
60
+ test("pickDefaultVoice: fallback to alloy on unknown/empty", () => {
61
+ assert.equal(pickDefaultVoice({}), "alloy");
62
+ assert.equal(pickDefaultVoice({ voice: "nonexistent" }), "alloy");
63
+ });
64
+
65
+ test("ttsCostEstimate: matches OpenAI's $0.015/1k chars", () => {
66
+ assert.equal(ttsCostEstimate(""), 0);
67
+ assert.equal(ttsCostEstimate("x".repeat(1000)), 0.015);
68
+ assert.equal(ttsCostEstimate("x".repeat(10000)), 0.15);
69
+ });