@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,804 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * voice-webhook-server.mjs — HTTP/WebSocket server for the voice channel.
4
+ *
5
+ * Sits between external audio sources (Twilio SIP, browser widgets,
6
+ * Media Streams) and OpenAI Realtime. Started as its own launchd job
7
+ * (ai.maestro.<agent>-voice-webhook.plist) so it stays up independently
8
+ * of the reactive daemon — voice calls shouldn't drop when the agent
9
+ * daemon restarts.
10
+ *
11
+ * Mounted endpoints (all configurable in `config/voice.yaml`):
12
+ * POST /voice/sip/incoming — OpenAI's `realtime.call.incoming` webhook
13
+ * (native SIP path). Hands the call to
14
+ * lib/voice/twilio-sip-bridge.mjs.
15
+ * POST /voice/twiml — Twilio Voice webhook returns TwiML
16
+ * `<Connect><Stream>` for Media Streams.
17
+ * WS /voice/stream — Twilio Media Streams audio frames.
18
+ * WS /voice/browser — Direct browser WebSocket (HTML widget).
19
+ * GET /voice/token — Mints an ephemeral OpenAI client_secret
20
+ * for browser-side WebRTC.
21
+ * GET /voice/test — Minimal HTML widget for manual testing.
22
+ * GET /health — Health check.
23
+ *
24
+ * Auth model:
25
+ * - Twilio webhooks: validated via X-Twilio-Signature when
26
+ * TWILIO_AUTH_TOKEN is set.
27
+ * - Browser endpoints: protected by a static bearer token from
28
+ * VOICE_BROWSER_TOKEN (set in .env). The /voice/test page injects
29
+ * it; external callers pass it in `Authorization: Bearer …`.
30
+ *
31
+ * Failure mode: every handler is wrapped so a fault in one endpoint
32
+ * doesn't take the whole server down. Failures emit structured logs
33
+ * via console.log/console.error captured by launchd's redirect.
34
+ */
35
+
36
+ import http from "node:http";
37
+ import { existsSync, readFileSync } from "node:fs";
38
+ import { join, resolve, dirname } from "node:path";
39
+ import { fileURLToPath } from "node:url";
40
+ import { createHmac, timingSafeEqual } from "node:crypto";
41
+
42
+ const __dirname = dirname(fileURLToPath(import.meta.url));
43
+ const AGENT_DIR = process.env.AGENT_ROOT || process.env.AGENT_DIR || resolve(__dirname, "..", "..");
44
+ // Framework primitives are colocated with this script via `npx @cohortapp/agent-sdk upgrade`
45
+ // (which ships lib/ into the agent repo). Imports are done as bare relative paths
46
+ // (../../lib/...) so the agent never depends on a separate /Users/<u>/maestro checkout.
47
+
48
+ // Source .env from the agent repo. launchd's bare environment doesn't
49
+ // include OPENAI_API_KEY / TWILIO_AUTH_TOKEN / VOICE_BROWSER_TOKEN — and
50
+ // we don't want to put secrets in the plist. Minimal parser; honours
51
+ // `KEY=value` lines and ignores comments / blanks.
52
+ function loadDotEnv(path) {
53
+ if (!existsSync(path)) return;
54
+ for (const raw of readFileSync(path, "utf-8").split("\n")) {
55
+ const line = raw.trim();
56
+ if (!line || line.startsWith("#")) continue;
57
+ const eq = line.indexOf("=");
58
+ if (eq <= 0) continue;
59
+ const key = line.slice(0, eq).trim();
60
+ let val = line.slice(eq + 1).trim();
61
+ // Strip surrounding quotes if present.
62
+ if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
63
+ val = val.slice(1, -1);
64
+ }
65
+ if (key && !(key in process.env)) process.env[key] = val;
66
+ }
67
+ }
68
+ loadDotEnv(join(AGENT_DIR, ".env"));
69
+
70
+ const PORT = parseInt(process.env.VOICE_WEBHOOK_PORT || "3200", 10);
71
+
72
+ // ---------------------------------------------------------------------------
73
+ // Load voice config
74
+ // ---------------------------------------------------------------------------
75
+
76
+ let voiceConfig = {};
77
+ const voiceCfgPath = join(AGENT_DIR, "config/voice.yaml");
78
+ if (!existsSync(voiceCfgPath)) {
79
+ console.error(`[voice-webhook] config/voice.yaml not present — exiting.`);
80
+ process.exit(0);
81
+ }
82
+ try {
83
+ const yaml = await import("js-yaml");
84
+ voiceConfig = yaml.load(readFileSync(voiceCfgPath, "utf-8"));
85
+ } catch (err) {
86
+ console.error(`[voice-webhook] failed to parse voice.yaml: ${err.message}`);
87
+ process.exit(78);
88
+ }
89
+
90
+ const inbound = voiceConfig.inbound || {};
91
+ const sipEnabled = inbound.twilio_sip?.enabled === true;
92
+ const streamsEnabled = inbound.twilio_media_streams?.enabled === true;
93
+ const browserEnabled = inbound.browser_ws?.enabled === true;
94
+
95
+ // ---------------------------------------------------------------------------
96
+ // Singleton + identity
97
+ // ---------------------------------------------------------------------------
98
+
99
+ try {
100
+ const { acquireLock } = await import("../../lib/singleton.js");
101
+ if (!acquireLock("voice-webhook")) {
102
+ console.log("[voice-webhook] already running — exiting");
103
+ process.exit(0);
104
+ }
105
+ } catch { /* singleton unavailable — proceed without */ }
106
+
107
+ let identity = { name: "Agent", role: "agent", company: "Northwind" };
108
+ try {
109
+ const aj = JSON.parse(readFileSync(join(AGENT_DIR, "config/agent.json"), "utf-8"));
110
+ identity = {
111
+ name: aj.firstName || "Agent",
112
+ role: aj.title || "agent",
113
+ company: aj.company || "Northwind",
114
+ };
115
+ } catch { /* fall through */ }
116
+
117
+ // ---------------------------------------------------------------------------
118
+ // Load framework modules
119
+ // ---------------------------------------------------------------------------
120
+
121
+ const { buildSessionConfig } = await import("../../lib/voice/index.mjs");
122
+ const { actionsToRealtimeTools, registerToolBridge } = await import("../../lib/voice/tool-bridge.mjs");
123
+ const sipBridge = await import("../../lib/voice/twilio-sip-bridge.mjs");
124
+ const { createOpenAIRealtimeProvider } = await import("../../lib/voice/openai-realtime.mjs");
125
+ const { buildContextualInstructions } = await import("../../lib/voice/context-loader.mjs");
126
+
127
+ // ---------------------------------------------------------------------------
128
+ // Helpers
129
+ // ---------------------------------------------------------------------------
130
+
131
+ function nowIso() { return new Date().toISOString(); }
132
+ function log(level, msg, extra = {}) {
133
+ console.log(JSON.stringify({ ts: nowIso(), level, msg, ...extra }));
134
+ }
135
+
136
+ function respond(res, status, body, contentType = "application/json") {
137
+ res.writeHead(status, { "Content-Type": contentType });
138
+ res.end(typeof body === "string" ? body : JSON.stringify(body));
139
+ }
140
+
141
+ // Translate a legacy session config (flat: voice + input_audio_format +
142
+ // turn_detection at top level) into the GA `{session:{audio:{input,output}}}`
143
+ // shape required by /v1/realtime/client_secrets.
144
+ function toGASessionShape(legacy) {
145
+ const fmtMap = {
146
+ pcm16: { type: "audio/pcm", rate: 24000 },
147
+ g711_ulaw: { type: "audio/pcmu" },
148
+ g711_alaw: { type: "audio/pcma" },
149
+ };
150
+ const inputFmt = fmtMap[legacy.input_audio_format] || { type: "audio/pcm", rate: 24000 };
151
+ const outputFmt = fmtMap[legacy.output_audio_format] || { type: "audio/pcm", rate: 24000 };
152
+ return {
153
+ session: {
154
+ type: "realtime",
155
+ model: legacy.model,
156
+ output_modalities: legacy.output_modalities || ["audio"],
157
+ instructions: legacy.instructions,
158
+ tools: legacy.tools || [],
159
+ tool_choice: legacy.tool_choice || "auto",
160
+ audio: {
161
+ input: {
162
+ format: inputFmt,
163
+ transcription: legacy.input_audio_transcription || null,
164
+ turn_detection: legacy.turn_detection || null,
165
+ },
166
+ output: {
167
+ format: outputFmt,
168
+ voice: legacy.voice || "cedar",
169
+ },
170
+ },
171
+ },
172
+ };
173
+ }
174
+
175
+ async function readBody(req) {
176
+ return new Promise((resolve, reject) => {
177
+ let buf = "";
178
+ req.on("data", (c) => { buf += c; });
179
+ req.on("end", () => resolve(buf));
180
+ req.on("error", reject);
181
+ });
182
+ }
183
+
184
+ function verifyTwilioSignature(req, rawBody) {
185
+ // Twilio signs: HMAC-SHA1(authToken, url + sorted form params).
186
+ // For JSON bodies we sign url + raw body as documented in
187
+ // https://www.twilio.com/docs/usage/webhooks/webhooks-security
188
+ const token = process.env.TWILIO_AUTH_TOKEN;
189
+ const sig = req.headers["x-twilio-signature"];
190
+ if (!token || !sig) return !token; // when no token configured, accept (dev mode)
191
+ const url = `https://${req.headers.host}${req.url}`;
192
+ const data = url + (rawBody || "");
193
+ const expected = createHmac("sha1", token).update(data).digest("base64");
194
+ const a = Buffer.from(expected);
195
+ const b = Buffer.from(sig);
196
+ if (a.length !== b.length) return false;
197
+ return timingSafeEqual(a, b);
198
+ }
199
+
200
+ /**
201
+ * Verify an OpenAI webhook signature. OpenAI signs realtime webhook
202
+ * payloads using HMAC-SHA256 with the webhook's signing secret. The
203
+ * signature header is `openai-signature: <hex>` (or sometimes
204
+ * `webhook-signature: t=<ts>,v1=<hex>` depending on the API era).
205
+ * We accept either format. Configure the secret via OPENAI_WEBHOOK_SECRET.
206
+ *
207
+ * When OPENAI_WEBHOOK_SECRET is not set (dev mode), we accept all
208
+ * payloads but log a warning so the operator notices.
209
+ */
210
+ function verifyOpenAISignature(req, rawBody) {
211
+ const secret = process.env.OPENAI_WEBHOOK_SECRET;
212
+ if (!secret) {
213
+ log("warn", "openai_webhook_unverified", { reason: "OPENAI_WEBHOOK_SECRET not set" });
214
+ return true; // dev mode
215
+ }
216
+ const headerSig = req.headers["openai-signature"] || req.headers["webhook-signature"] || "";
217
+ if (!headerSig) return false;
218
+ // Format A: bare hex digest
219
+ // Format B: "t=<ts>,v1=<hex>" (Stripe-style)
220
+ let providedHex = headerSig;
221
+ if (headerSig.includes(",")) {
222
+ const parts = Object.fromEntries(
223
+ headerSig.split(",").map((kv) => kv.split("=").map((s) => s.trim()))
224
+ );
225
+ providedHex = parts.v1 || parts.v0 || "";
226
+ }
227
+ const expected = createHmac("sha256", secret).update(rawBody || "").digest("hex");
228
+ if (providedHex.length !== expected.length) return false;
229
+ return timingSafeEqual(Buffer.from(providedHex), Buffer.from(expected));
230
+ }
231
+
232
+ function checkBrowserAuth(req) {
233
+ const required = process.env.VOICE_BROWSER_TOKEN;
234
+ if (!required) return true; // no token set → open (dev mode)
235
+ const header = req.headers.authorization || "";
236
+ if (!header.startsWith("Bearer ")) return false;
237
+ const provided = header.slice("Bearer ".length).trim();
238
+ if (provided.length !== required.length) return false;
239
+ return timingSafeEqual(Buffer.from(provided), Buffer.from(required));
240
+ }
241
+
242
+ // ---------------------------------------------------------------------------
243
+ // Inbox writer — emits voice events as JSON files under
244
+ // state/inbox/voice/ so voice-poller.mjs (and via it, the daemon's
245
+ // classifier → dispatcher pipeline) can consume them like any other
246
+ // channel.
247
+ // ---------------------------------------------------------------------------
248
+
249
+ import { mkdirSync, writeFileSync as fsWriteFile } from "node:fs";
250
+ const VOICE_INBOX_DIR = join(AGENT_DIR, "state/inbox/voice");
251
+ try { mkdirSync(VOICE_INBOX_DIR, { recursive: true }); } catch { /* */ }
252
+
253
+ function writeInboxEvent(payload) {
254
+ try {
255
+ const id = (payload.call_id || "noid").toString().replace(/\W/g, "_").slice(0, 24);
256
+ const evt = (payload.event_type || "event").replace(/\W/g, "_");
257
+ const filename = `${Date.now()}-${id}-${evt}.json`;
258
+ fsWriteFile(join(VOICE_INBOX_DIR, filename), JSON.stringify(payload, null, 2));
259
+ } catch (err) {
260
+ log("warn", "inbox_write_failed", { error: err.message });
261
+ }
262
+ }
263
+
264
+ // ---------------------------------------------------------------------------
265
+ // Tool bridge attach helper — also accumulates the transcript +
266
+ // tool-call timeline for this call and writes call.started /
267
+ // call.ended events into the daemon's voice inbox.
268
+ // ---------------------------------------------------------------------------
269
+
270
+ async function attachBridges(provider, context = {}) {
271
+ const callId = context.call_id || `call-${Date.now()}`;
272
+ const startedAt = new Date().toISOString();
273
+ const transcriptChunks = [];
274
+ const toolCalls = [];
275
+
276
+ // Surface the inbound call to the daemon's inbox right away so the
277
+ // classifier can mark it Critical if it's the CEO / leadership, etc.
278
+ writeInboxEvent({
279
+ event_type: "call.started",
280
+ call_id: callId,
281
+ transport: context.transport || "unknown",
282
+ peer: context.peer || null,
283
+ peer_name: context.peer_name || null,
284
+ started_at: startedAt,
285
+ timestamp: startedAt,
286
+ });
287
+
288
+ // Buffer transcript + tools as the call progresses.
289
+ const onTranscriptDelta = (e) => {
290
+ if (e?.delta) transcriptChunks.push(e.delta);
291
+ };
292
+ const onTranscriptDone = (e) => {
293
+ if (e?.transcript) transcriptChunks.push(e.transcript);
294
+ };
295
+ const onTurnDone = () => { transcriptChunks.push("\n"); };
296
+
297
+ provider.on("transcript.delta", onTranscriptDelta);
298
+ provider.on("transcript.done", onTranscriptDone);
299
+ provider.on("turn.done", onTurnDone);
300
+
301
+ let detachTools = () => {};
302
+ try {
303
+ detachTools = registerToolBridge(provider, {
304
+ allow: voiceConfig.tools?.include,
305
+ classification: undefined,
306
+ audit: (entry) => {
307
+ toolCalls.push({
308
+ name: entry.name,
309
+ args: entry.args,
310
+ risk: entry.risk,
311
+ error: entry.error,
312
+ started_at: entry.started_at,
313
+ finished_at: entry.finished_at,
314
+ });
315
+ log("info", "voice.tool", { ...entry, ...context });
316
+ // Surface high-risk tool calls as their own inbox items so the
317
+ // audit / decision-capture pipeline sees them immediately
318
+ // (rather than waiting for the call to end).
319
+ if (entry.risk === "high") {
320
+ writeInboxEvent({
321
+ event_type: "call.tool_call",
322
+ call_id: callId,
323
+ tool_name: entry.name,
324
+ args: entry.args,
325
+ risk: entry.risk,
326
+ result_summary: entry.error ? `error: ${entry.error}` : "ok",
327
+ timestamp: entry.finished_at,
328
+ });
329
+ }
330
+ },
331
+ });
332
+ } catch (err) {
333
+ log("error", "tool_bridge_failed", { error: err.message });
334
+ }
335
+
336
+ // On session close: write the call summary to the inbox.
337
+ const onClosed = (info) => {
338
+ const endedAt = new Date().toISOString();
339
+ const durationMs = Date.now() - new Date(startedAt).getTime();
340
+ writeInboxEvent({
341
+ event_type: "call.ended",
342
+ call_id: callId,
343
+ transport: context.transport || "unknown",
344
+ peer: context.peer || null,
345
+ peer_name: context.peer_name || null,
346
+ started_at: startedAt,
347
+ ended_at: endedAt,
348
+ duration_ms: durationMs,
349
+ transcript: transcriptChunks.join("").trim(),
350
+ tool_calls: toolCalls,
351
+ reason: info?.reason || "client_close",
352
+ timestamp: endedAt,
353
+ });
354
+ // Also persist a transcript-only file under logs/voice/sessions/
355
+ // so it's discoverable without going through inbox processing.
356
+ try {
357
+ const sessionsDir = join(AGENT_DIR, "logs/voice/sessions");
358
+ mkdirSync(sessionsDir, { recursive: true });
359
+ fsWriteFile(
360
+ join(sessionsDir, `${callId}.json`),
361
+ JSON.stringify({
362
+ call_id: callId, started_at: startedAt, ended_at: endedAt,
363
+ duration_ms: durationMs, transcript: transcriptChunks.join(""),
364
+ tool_calls: toolCalls,
365
+ }, null, 2)
366
+ );
367
+ } catch { /* */ }
368
+ };
369
+ provider.on("closed", onClosed);
370
+
371
+ return () => {
372
+ try { detachTools(); } catch { /* */ }
373
+ provider.off?.("transcript.delta", onTranscriptDelta);
374
+ provider.off?.("transcript.done", onTranscriptDone);
375
+ provider.off?.("turn.done", onTurnDone);
376
+ provider.off?.("closed", onClosed);
377
+ };
378
+ }
379
+
380
+ // ---------------------------------------------------------------------------
381
+ // HTTP server + handlers
382
+ // ---------------------------------------------------------------------------
383
+
384
+ const server = http.createServer(async (req, res) => {
385
+ try {
386
+ if (req.url === "/health" && req.method === "GET") {
387
+ respond(res, 200, {
388
+ ok: true,
389
+ agent: identity.name,
390
+ sipEnabled, streamsEnabled, browserEnabled,
391
+ openai_key_loaded: !!process.env.OPENAI_API_KEY,
392
+ ts: nowIso(),
393
+ });
394
+ return;
395
+ }
396
+
397
+ // Polite no-op for the browser favicon request (avoids noisy 404s in
398
+ // the JS console of the /voice/test page).
399
+ if (req.url === "/favicon.ico") {
400
+ respond(res, 204, "", "image/x-icon");
401
+ return;
402
+ }
403
+
404
+ // Serve cached outbound-audio files so Twilio <Play> can fetch them.
405
+ // URL: /voice/audio/<filename>. Path-traversal is blocked by
406
+ // restricting filename to a safe character set that MUST start with
407
+ // [A-Za-z0-9_-] so naked `.` / `..` are rejected, then defence-in-depth
408
+ // verifying the resolved path stays inside the cache dir.
409
+ if (req.url.startsWith("/voice/audio/") && req.method === "GET") {
410
+ const filename = req.url.slice("/voice/audio/".length).split("?")[0];
411
+ if (!/^[A-Za-z0-9_-][A-Za-z0-9._-]*$/.test(filename) || filename.length > 200) {
412
+ respond(res, 400, { ok: false, error: "bad_filename" });
413
+ return;
414
+ }
415
+ const audioDir = join(AGENT_DIR, "state/voice/outbound-audio");
416
+ const audioPath = join(audioDir, filename);
417
+ // Defence-in-depth: resolved path must remain inside audioDir even
418
+ // if the regex was bypassed.
419
+ const { resolve: pathResolve } = await import("node:path");
420
+ if (!pathResolve(audioPath).startsWith(pathResolve(audioDir) + "/")) {
421
+ respond(res, 400, { ok: false, error: "path_traversal" });
422
+ return;
423
+ }
424
+ if (!existsSync(audioPath)) {
425
+ respond(res, 404, { ok: false, error: "not_found" });
426
+ return;
427
+ }
428
+ const buf = readFileSync(audioPath);
429
+ const ext = filename.split(".").pop().toLowerCase();
430
+ const mime = {
431
+ mp3: "audio/mpeg", ogg: "audio/ogg", opus: "audio/ogg",
432
+ wav: "audio/wav", flac: "audio/flac", aac: "audio/aac",
433
+ }[ext] || "application/octet-stream";
434
+ res.writeHead(200, {
435
+ "Content-Type": mime,
436
+ "Content-Length": buf.length,
437
+ "Cache-Control": "public, max-age=3600",
438
+ });
439
+ res.end(buf);
440
+ return;
441
+ }
442
+
443
+ // SIP path — also functions as the OpenAI realtime.call.incoming webhook.
444
+ if (sipEnabled && req.url === (inbound.twilio_sip?.inbound_webhook_path || "/voice/sip/incoming") && req.method === "POST") {
445
+ // OpenAI signs the webhook body — verify before accepting.
446
+ const rawBody = await readBody(req);
447
+ if (!verifyOpenAISignature(req, rawBody)) {
448
+ log("warn", "openai_webhook_signature_mismatch", {});
449
+ respond(res, 403, { ok: false, error: "openai_signature_invalid" });
450
+ return;
451
+ }
452
+ // Re-attach the body so the downstream handler can read it.
453
+ req._cachedBody = rawBody;
454
+ let parsedBody;
455
+ try { parsedBody = JSON.parse(rawBody || "{}"); } catch { parsedBody = {}; }
456
+
457
+ // Detect outbound-initiated calls (we set X-Maestro-Call-Id when
458
+ // bridging via Twilio outbound → SIP). If present, load the brief
459
+ // from state/voice/outbound-briefs/ and use it as the session
460
+ // instructions instead of the inbound context.
461
+ const xMaestro = parsedBody?.data?.headers?.["X-Maestro-Call-Id"] ||
462
+ parsedBody?.headers?.["X-Maestro-Call-Id"] || null;
463
+
464
+ const sessionConfigFactory = async ({ callId, body }) => {
465
+ const tools = await actionsToRealtimeTools({
466
+ accessLevel: voiceConfig.tools?.access_level || "voice",
467
+ include: voiceConfig.tools?.include,
468
+ exclude: voiceConfig.tools?.exclude,
469
+ agentRoot: AGENT_DIR,
470
+ });
471
+ const peer = body?.from || body?.caller_id || parsedBody?.data?.from || null;
472
+ let callKind = "inbound";
473
+ let brief = null;
474
+ if (xMaestro) {
475
+ callKind = "outbound";
476
+ try {
477
+ const briefPath = join(AGENT_DIR, "state/voice/outbound-briefs", `${xMaestro}.json`);
478
+ if (existsSync(briefPath)) {
479
+ const j = JSON.parse(readFileSync(briefPath, "utf-8"));
480
+ brief = j.brief;
481
+ }
482
+ } catch { /* */ }
483
+ }
484
+ const instructions = await buildContextualInstructions({
485
+ agentRoot: AGENT_DIR,
486
+ peer,
487
+ callKind,
488
+ brief,
489
+ });
490
+ return buildSessionConfig({ tools, instructions, ...voiceConfig.session }, identity);
491
+ };
492
+ // The cloud-relay accepts the call itself before buffering (because
493
+ // the Mac→Railway round-trip blows OpenAI's accept window). When it
494
+ // does, the relay-poller forwards the event with this header set so
495
+ // the local handler doesn't try to re-accept (which would 404).
496
+ const acceptedByRelay = req.headers["x-maestro-relay-accepted"] === "1";
497
+ const handler = sipBridge.buildSipInboundHandler({
498
+ sessionConfig: sessionConfigFactory,
499
+ acceptedByRelay,
500
+ attachBridges: (provider, { callId }) =>
501
+ attachBridges(provider, {
502
+ call_id: callId,
503
+ transport: "sip",
504
+ peer: parsedBody?.data?.from || null,
505
+ kind: xMaestro ? "outbound" : "inbound",
506
+ }),
507
+ });
508
+ // Patch readBody so the handler doesn't try to re-read the consumed stream.
509
+ const patchedReq = Object.create(req);
510
+ patchedReq.body = parsedBody;
511
+ await handler(patchedReq, res);
512
+ return;
513
+ }
514
+
515
+ // TwiML for Media Streams
516
+ if (streamsEnabled && req.url === (inbound.twilio_media_streams.voice_webhook_path || "/voice/twiml") && req.method === "POST") {
517
+ const rawBody = await readBody(req);
518
+ if (!verifyTwilioSignature(req, rawBody)) {
519
+ respond(res, 403, { ok: false, error: "twilio_signature_invalid" });
520
+ return;
521
+ }
522
+ const host = req.headers.host;
523
+ const proto = host?.startsWith("localhost") ? "ws" : "wss";
524
+ const streamUrl = `${proto}://${host}${inbound.twilio_media_streams.stream_path || "/voice/stream"}`;
525
+ const twiml = sipBridge.issueTwiml({ streamUrl });
526
+ respond(res, 200, twiml, "text/xml");
527
+ return;
528
+ }
529
+
530
+ // Mint ephemeral OpenAI token for browser
531
+ if (browserEnabled && req.url === (inbound.browser_ws.ephemeral_token_path || "/voice/token") && req.method === "GET") {
532
+ if (!checkBrowserAuth(req)) {
533
+ respond(res, 401, { ok: false, error: "unauthorized" });
534
+ return;
535
+ }
536
+ const apiKey = process.env.OPENAI_API_KEY;
537
+ if (!apiKey) {
538
+ respond(res, 500, { ok: false, error: "OPENAI_API_KEY not set" });
539
+ return;
540
+ }
541
+ let tools;
542
+ try {
543
+ tools = await actionsToRealtimeTools({
544
+ accessLevel: voiceConfig.tools?.access_level || "voice",
545
+ include: voiceConfig.tools?.include,
546
+ exclude: voiceConfig.tools?.exclude,
547
+ agentRoot: AGENT_DIR,
548
+ });
549
+ } catch (err) {
550
+ log("warn", "tool_load_failed", { error: err.message });
551
+ tools = [];
552
+ }
553
+ // Pre-load context so browser-side voice sessions also benefit from
554
+ // the agent's memory — the model walks into the call with the
555
+ // current queues / decisions / priorities already in its prompt.
556
+ let instructions;
557
+ try {
558
+ instructions = await buildContextualInstructions({
559
+ agentRoot: AGENT_DIR,
560
+ callKind: "browser_test",
561
+ });
562
+ } catch (err) {
563
+ log("warn", "context_load_failed", { error: err.message });
564
+ instructions = undefined; // fall back to buildSessionConfig's defaults
565
+ }
566
+ const fullSession = buildSessionConfig({ tools, instructions, ...voiceConfig.session }, identity);
567
+ // The GA endpoint (POST /v1/realtime/client_secrets) uses a nested
568
+ // audio: { input: {format,transcription,turn_detection},
569
+ // output: {format,voice} } schema. buildSessionConfig
570
+ // emits the LEGACY flat shape (voice/input_audio_format at top
571
+ // level) — translate here so the existing helper stays compatible
572
+ // with /v1/realtime/sessions consumers.
573
+ const isGAModel = /realtime-(2|translate|whisper)/.test(String(fullSession.model || ""));
574
+ const mintBody = isGAModel
575
+ ? toGASessionShape(fullSession)
576
+ : { session: fullSession };
577
+ try {
578
+ // Use GA `/client_secrets` endpoint for gpt-realtime-2 et al.
579
+ // The older `/sessions` endpoint is for preview models.
580
+ const endpoint = isGAModel
581
+ ? "https://api.openai.com/v1/realtime/client_secrets"
582
+ : "https://api.openai.com/v1/realtime/sessions";
583
+ const headers = {
584
+ Authorization: `Bearer ${apiKey}`,
585
+ "Content-Type": "application/json",
586
+ };
587
+ if (!isGAModel) headers["OpenAI-Beta"] = "realtime=v1";
588
+ const r = await fetch(endpoint, {
589
+ method: "POST",
590
+ headers,
591
+ body: JSON.stringify(mintBody),
592
+ });
593
+ const text = await r.text();
594
+ if (!r.ok) log("warn", "session_mint_failed", { endpoint, status: r.status, body: text.slice(0, 500) });
595
+ respond(res, r.status, text, "application/json");
596
+ } catch (err) {
597
+ respond(res, 502, { ok: false, error: err.message });
598
+ }
599
+ return;
600
+ }
601
+
602
+ // Minimal HTML widget for manual testing.
603
+ if (req.url === "/voice/test" && req.method === "GET") {
604
+ respond(res, 200, renderTestPage(identity, voiceConfig), "text/html");
605
+ return;
606
+ }
607
+
608
+ // Catch-all
609
+ respond(res, 404, { ok: false, error: "not_found", url: req.url });
610
+ } catch (err) {
611
+ log("error", "handler_failed", { url: req.url, error: err.message });
612
+ try { respond(res, 500, { ok: false, error: err.message }); } catch { /* */ }
613
+ }
614
+ });
615
+
616
+ // ---------------------------------------------------------------------------
617
+ // WebSocket upgrade — Twilio Media Streams + browser WS
618
+ // ---------------------------------------------------------------------------
619
+
620
+ server.on("upgrade", async (req, socket) => {
621
+ try {
622
+ const url = new URL(req.url, `http://${req.headers.host}`);
623
+ if (streamsEnabled && url.pathname === (inbound.twilio_media_streams.stream_path || "/voice/stream")) {
624
+ // Twilio Media Streams WS — we need to upgrade then forward µ-law frames.
625
+ // We deliberately don't pull in `ws` here — for a minimal proof-of-life
626
+ // we close politely and direct operators to enable SIP path instead.
627
+ // A full Media Streams adapter belongs in lib/voice/twilio-media-streams-server.mjs.
628
+ socket.write("HTTP/1.1 501 Not Implemented\r\n\r\n");
629
+ socket.destroy();
630
+ return;
631
+ }
632
+ if (browserEnabled && url.pathname === (inbound.browser_ws.path || "/voice/browser")) {
633
+ // Same treatment — browser path is best served by the official
634
+ // WebRTC flow via /voice/token; raw WS bridging is left as a
635
+ // future enhancement.
636
+ socket.write("HTTP/1.1 501 Not Implemented\r\n\r\n");
637
+ socket.destroy();
638
+ return;
639
+ }
640
+ socket.write("HTTP/1.1 404 Not Found\r\n\r\n");
641
+ socket.destroy();
642
+ } catch (err) {
643
+ log("error", "upgrade_failed", { error: err.message });
644
+ try { socket.destroy(); } catch { /* */ }
645
+ }
646
+ });
647
+
648
+ // ---------------------------------------------------------------------------
649
+ // Test page (HTML/JS)
650
+ // ---------------------------------------------------------------------------
651
+
652
+ function renderTestPage(identity, voiceConfig) {
653
+ const safeName = String(identity.name).replace(/[<>]/g, "");
654
+ const voice = (voiceConfig.voice || "cedar").replace(/[<>]/g, "");
655
+ return `<!doctype html>
656
+ <meta charset="utf-8" />
657
+ <title>Voice test — ${safeName}</title>
658
+ <style>
659
+ body { font: 14px/1.4 system-ui, sans-serif; max-width: 720px; margin: 2em auto; padding: 0 1em; }
660
+ h1 { font-weight: 600; font-size: 1.4em; }
661
+ .pill { display: inline-block; padding: 2px 8px; border-radius: 999px; background: #eee; font-size: 12px; }
662
+ button { padding: 8px 16px; border-radius: 6px; border: 1px solid #ccc; background: #fff; cursor: pointer; font: inherit; }
663
+ button:hover { background: #f5f5f5; }
664
+ pre { background: #f8f8f8; padding: 8px; border-radius: 4px; font-size: 12px; overflow-x: auto; }
665
+ .ok { color: #060; }
666
+ .err { color: #c00; }
667
+ </style>
668
+ <h1>Voice mode — ${safeName} <span class="pill">${voice}</span></h1>
669
+ <p>This page mints an OpenAI Realtime ephemeral token from the agent's server, then opens a WebRTC peer connection so your browser's microphone talks directly to OpenAI. The agent's tool bridge (Slack, Gmail, etc.) is wired server-side.</p>
670
+ <p><label>Auth token: <input id="tok" type="password" placeholder="VOICE_BROWSER_TOKEN (leave blank in dev mode)" style="width: 360px" /></label></p>
671
+ <p><label>Mic: <select id="mic" style="min-width: 280px"></select></label> <button id="rescan">Rescan</button></p>
672
+ <p>
673
+ <button id="connect">Connect</button>
674
+ <button id="disconnect" disabled>Disconnect</button>
675
+ <span id="status" class="pill">idle</span>
676
+ </p>
677
+ <h2>Log</h2>
678
+ <pre id="log"></pre>
679
+ <script type="module">
680
+ const $ = (id) => document.getElementById(id);
681
+ const log = (msg, cls) => { const p = document.createElement("div"); p.textContent = new Date().toISOString() + " " + msg; if (cls) p.className = cls; $("log").appendChild(p); };
682
+ let pc, dc, micStream;
683
+
684
+ function clearChildren(el) { while (el.firstChild) el.removeChild(el.firstChild); }
685
+ function makeOption(value, label) {
686
+ const o = document.createElement("option");
687
+ o.value = value;
688
+ o.textContent = label;
689
+ return o;
690
+ }
691
+
692
+ async function listMics() {
693
+ const sel = $("mic");
694
+ clearChildren(sel);
695
+ sel.appendChild(makeOption("", "— default —"));
696
+ try {
697
+ // First-time call: triggers permission prompt so labels are populated.
698
+ const tmp = await navigator.mediaDevices.getUserMedia({ audio: true }).catch(() => null);
699
+ tmp?.getTracks().forEach((t) => t.stop());
700
+ const devs = await navigator.mediaDevices.enumerateDevices();
701
+ const mics = devs.filter((d) => d.kind === "audioinput");
702
+ if (mics.length === 0) {
703
+ log("No microphone detected. Connect AirPods, a USB mic, or use 'iPhone as Microphone' (Continuity).", "err");
704
+ } else {
705
+ log("Detected " + mics.length + " mic(s)");
706
+ for (const m of mics) {
707
+ sel.appendChild(makeOption(m.deviceId, m.label || m.deviceId.slice(0, 20)));
708
+ }
709
+ }
710
+ } catch (err) {
711
+ log("device enumeration failed: " + err.message, "err");
712
+ }
713
+ }
714
+ listMics();
715
+ $("rescan").onclick = listMics;
716
+
717
+ $("connect").onclick = async () => {
718
+ try {
719
+ $("status").textContent = "minting token…";
720
+ const r = await fetch("/voice/token", { headers: { Authorization: "Bearer " + $("tok").value } });
721
+ if (!r.ok) {
722
+ const body = await r.text();
723
+ throw new Error("token mint failed: HTTP " + r.status + " " + body.slice(0, 200));
724
+ }
725
+ const session = await r.json();
726
+ const clientSecret = session.value || session.client_secret?.value || session.client_secret;
727
+ const model = session.session?.model || session.model || "gpt-realtime-2";
728
+ log("token minted, model=" + model);
729
+
730
+ $("status").textContent = "opening mic…";
731
+ const deviceId = $("mic").value;
732
+ const constraints = { audio: deviceId ? { deviceId: { exact: deviceId } } : true };
733
+ try {
734
+ micStream = await navigator.mediaDevices.getUserMedia(constraints);
735
+ } catch (err) {
736
+ const why = err.name === "NotFoundError"
737
+ ? "No microphone available. Connect AirPods/USB mic/iPhone-as-mic and click Rescan."
738
+ : err.name === "NotAllowedError"
739
+ ? "Mic permission denied. Allow it in browser settings for this site."
740
+ : err.message;
741
+ throw new Error(why);
742
+ }
743
+ log("mic opened: " + (micStream.getAudioTracks()[0]?.label || "(unlabelled)"));
744
+
745
+ $("status").textContent = "creating peer connection…";
746
+ pc = new RTCPeerConnection();
747
+ pc.ontrack = (e) => {
748
+ const audio = new Audio();
749
+ audio.autoplay = true;
750
+ audio.srcObject = e.streams[0];
751
+ };
752
+ dc = pc.createDataChannel("oai-events");
753
+ dc.onmessage = (e) => log("← " + e.data.slice(0, 200));
754
+ micStream.getTracks().forEach((t) => pc.addTrack(t, micStream));
755
+ const offer = await pc.createOffer();
756
+ await pc.setLocalDescription(offer);
757
+ $("status").textContent = "negotiating with OpenAI…";
758
+ const sdpResp = await fetch("https://api.openai.com/v1/realtime/calls?model=" + encodeURIComponent(model), {
759
+ method: "POST",
760
+ body: offer.sdp,
761
+ headers: { "Content-Type": "application/sdp", Authorization: "Bearer " + clientSecret },
762
+ });
763
+ if (!sdpResp.ok) throw new Error("OpenAI SDP exchange failed: HTTP " + sdpResp.status + " " + (await sdpResp.text()).slice(0, 200));
764
+ const answer = { type: "answer", sdp: await sdpResp.text() };
765
+ await pc.setRemoteDescription(answer);
766
+ $("status").textContent = "CONNECTED";
767
+ $("connect").disabled = true;
768
+ $("disconnect").disabled = false;
769
+ log("connected — speak now", "ok");
770
+ } catch (err) {
771
+ $("status").textContent = "error";
772
+ log(err.message, "err");
773
+ }
774
+ };
775
+ $("disconnect").onclick = () => {
776
+ micStream?.getTracks().forEach((t) => t.stop());
777
+ dc?.close(); pc?.close();
778
+ $("status").textContent = "idle";
779
+ $("connect").disabled = false;
780
+ $("disconnect").disabled = true;
781
+ log("disconnected");
782
+ };
783
+ </script>
784
+ `;
785
+ }
786
+
787
+ // ---------------------------------------------------------------------------
788
+ // Start
789
+ // ---------------------------------------------------------------------------
790
+
791
+ server.listen(PORT, "127.0.0.1", () => {
792
+ log("info", "voice_webhook_started", {
793
+ port: PORT,
794
+ sipEnabled, streamsEnabled, browserEnabled,
795
+ endpoints: {
796
+ health: `http://localhost:${PORT}/health`,
797
+ test_widget: browserEnabled ? `http://localhost:${PORT}/voice/test` : null,
798
+ sip: sipEnabled ? `http://localhost:${PORT}${inbound.twilio_sip.inbound_webhook_path || "/voice/sip/incoming"}` : null,
799
+ },
800
+ });
801
+ });
802
+
803
+ process.on("SIGINT", () => { server.close(); process.exit(0); });
804
+ process.on("SIGTERM", () => { server.close(); process.exit(0); });