@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.
- package/.claude/commands/init-agent.md +104 -0
- package/.claude/commands/init-maestro.md +1187 -0
- package/.claude/settings.json +161 -0
- package/.env.example +216 -0
- package/README.md +632 -0
- package/agents/browser-operator/agent.md +52 -0
- package/agents/calendar-ops/agent.md +50 -0
- package/agents/communications/agent.md +96 -0
- package/agents/decision-log/agent.md +65 -0
- package/agents/desktop-operator/agent.md +59 -0
- package/agents/gmail-operator/agent.md +62 -0
- package/agents/inbound-dispatcher/agent.md +66 -0
- package/agents/inbox-processor/agent.md +39 -0
- package/agents/pmo-execution/agent.md +60 -0
- package/agents/session-spawner/agent.md +64 -0
- package/agents/slack-operator/agent.md +60 -0
- package/agents/whatsapp-operator/agent.md +60 -0
- package/agents/workflow-automation/agent.md +61 -0
- package/archetypes/altitudes/c-suite.yaml +58 -0
- package/archetypes/altitudes/founder.yaml +68 -0
- package/archetypes/altitudes/senior-manager.yaml +63 -0
- package/archetypes/altitudes/svp.yaml +60 -0
- package/archetypes/altitudes/vp.yaml +50 -0
- package/archetypes/archetype.schema.json +77 -0
- package/archetypes/base.yaml +47 -0
- package/archetypes/capabilities/commercial-leader.yaml +159 -0
- package/archetypes/capabilities/compliance-officer.yaml +159 -0
- package/archetypes/capabilities/executive-operator.yaml +169 -0
- package/archetypes/capabilities/finance-leader.yaml +162 -0
- package/archetypes/capabilities/operations-leader.yaml +154 -0
- package/archetypes/capabilities/product-leader.yaml +148 -0
- package/archetypes/capabilities/technical-leader.yaml +146 -0
- package/archetypes/functions/commercial-leader.yaml +62 -0
- package/archetypes/functions/compliance-officer.yaml +64 -0
- package/archetypes/functions/executive-operator.yaml +70 -0
- package/archetypes/functions/finance-leader.yaml +70 -0
- package/archetypes/functions/operations-leader.yaml +62 -0
- package/archetypes/functions/product-leader.yaml +61 -0
- package/archetypes/functions/technical-leader.yaml +57 -0
- package/bin/cohort-mcp.mjs +81 -0
- package/bin/maestro.mjs +3516 -0
- package/bin/maestro.test.mjs +1015 -0
- package/desktop-control/README.md +56 -0
- package/desktop-control/app-profiles/gmail.yaml +120 -0
- package/desktop-control/app-profiles/slack.yaml +315 -0
- package/desktop-control/app-profiles/whatsapp.yaml +107 -0
- package/docs/architecture/agent-topology.md +2239 -0
- package/docs/architecture/archetype-agent-factory.md +110 -0
- package/docs/architecture/collective-memory-and-org-mesh.md +115 -0
- package/docs/architecture/continuous-monitoring.md +221 -0
- package/docs/architecture/mcp-capability-map.md +585 -0
- package/docs/architecture/system-architecture.md +1272 -0
- package/docs/company-context/README.md +40 -0
- package/docs/guides/agent-persona-setup.md +600 -0
- package/docs/guides/agents-observe-setup.md +64 -0
- package/docs/guides/billing-console-keys.md +88 -0
- package/docs/guides/ccxray-diagnostics.md +65 -0
- package/docs/guides/channel-bus.md +127 -0
- package/docs/guides/claude-mem-setup.md +79 -0
- package/docs/guides/claude-pace-setup.md +56 -0
- package/docs/guides/claudraband-sessions.md +98 -0
- package/docs/guides/clawteam-swarm.md +116 -0
- package/docs/guides/code-review-graph-setup.md +86 -0
- package/docs/guides/email-setup.md +431 -0
- package/docs/guides/mac-mini.md +119 -0
- package/docs/guides/media-generation-setup.md +349 -0
- package/docs/guides/model-routing.md +162 -0
- package/docs/guides/observability-otel.md +265 -0
- package/docs/guides/org-onboarding.md +132 -0
- package/docs/guides/outbound-governance-setup.md +437 -0
- package/docs/guides/pdf-generation-setup.md +315 -0
- package/docs/guides/poller-daemon-setup.md +563 -0
- package/docs/guides/rag-context-setup.md +459 -0
- package/docs/guides/self-optimization-pattern.md +82 -0
- package/docs/guides/setup-wizard.md +178 -0
- package/docs/guides/slack-setup.md +350 -0
- package/docs/guides/telegram-setup.md +227 -0
- package/docs/guides/twilio-subaccounts-setup.md +223 -0
- package/docs/guides/verification.md +128 -0
- package/docs/guides/voice-mode.md +188 -0
- package/docs/guides/voice-sms-setup.md +698 -0
- package/docs/guides/webhook-relay-setup.md +349 -0
- package/docs/guides/whatsapp-setup.md +288 -0
- package/docs/prompts/board-pack-cover-template.md +36 -0
- package/docs/prompts/decision-recommendation-template.md +88 -0
- package/docs/prompts/followup-message-template.md +141 -0
- package/docs/prompts/investor-letter-template.md +52 -0
- package/docs/prompts/morning-brief-template.md +82 -0
- package/docs/prompts/presentation-template.md +58 -0
- package/docs/prompts/weekly-strategic-memo-template.md +104 -0
- package/docs/research/hallucinated-tool-output-investigation.md +151 -0
- package/docs/runbooks/backup-restore.md +205 -0
- package/docs/runbooks/cohort-cutover.md +129 -0
- package/docs/runbooks/fleet-operations.md +200 -0
- package/docs/runbooks/incident-response.md +226 -0
- package/docs/runbooks/mac-mini-bootstrap.md +431 -0
- package/docs/runbooks/perpetual-operations.md +509 -0
- package/docs/runbooks/recovery-and-failover.md +260 -0
- package/framework-features.json +267 -0
- package/ingest/README.md +87 -0
- package/lib/action-executor.js +689 -0
- package/lib/action-executor.test.mjs +871 -0
- package/lib/agent-root.mjs +37 -0
- package/lib/archetype.mjs +236 -0
- package/lib/archetype.test.mjs +132 -0
- package/lib/autonomy.mjs +114 -0
- package/lib/autonomy.test.mjs +66 -0
- package/lib/backlog.mjs +358 -0
- package/lib/backlog.test.mjs +266 -0
- package/lib/budget-guard.mjs +279 -0
- package/lib/budget-guard.test.mjs +291 -0
- package/lib/cadence-bus-schedule.test.mjs +194 -0
- package/lib/cadence-bus.mjs +1120 -0
- package/lib/cadence-bus.test.mjs +720 -0
- package/lib/cadences.mjs +205 -0
- package/lib/cadences.test.mjs +125 -0
- package/lib/capability.mjs +154 -0
- package/lib/capability.test.mjs +78 -0
- package/lib/channels/base-adapter.mjs +719 -0
- package/lib/channels/base-adapter.test.mjs +590 -0
- package/lib/channels/channel.mjs +128 -0
- package/lib/channels/channels.test.mjs +371 -0
- package/lib/channels/contract.mjs +215 -0
- package/lib/channels/contract.test.mjs +137 -0
- package/lib/channels/conversation-resolver.mjs +95 -0
- package/lib/channels/gmail/adapter.mjs +87 -0
- package/lib/channels/inbox-item.mjs +255 -0
- package/lib/channels/inbox-item.test.mjs +335 -0
- package/lib/channels/index.mjs +94 -0
- package/lib/channels/orgmail/adapter.mjs +353 -0
- package/lib/channels/orgmail/adapter.test.mjs +311 -0
- package/lib/channels/pairing.mjs +363 -0
- package/lib/channels/pairing.test.mjs +270 -0
- package/lib/channels/registry.mjs +164 -0
- package/lib/channels/slack/adapter.mjs +317 -0
- package/lib/channels/slack-adapter.test.mjs +212 -0
- package/lib/channels/sms/adapter.mjs +43 -0
- package/lib/channels/telegram/adapter.mjs +432 -0
- package/lib/channels/telegram-adapter.test.mjs +306 -0
- package/lib/channels/voice/adapter.mjs +301 -0
- package/lib/channels/voice/adapter.test.mjs +278 -0
- package/lib/channels/whatsapp/adapter-baileys.mjs +587 -0
- package/lib/channels/whatsapp/adapter-baileys.test.mjs +359 -0
- package/lib/channels/whatsapp/adapter-twilio.mjs +65 -0
- package/lib/channels/whatsapp/baileys-typing.test.mjs +154 -0
- package/lib/charter.mjs +256 -0
- package/lib/charter.test.mjs +89 -0
- package/lib/claude-bin.mjs +134 -0
- package/lib/claude-bin.test.mjs +75 -0
- package/lib/collective/capture.mjs +185 -0
- package/lib/collective/capture.test.mjs +121 -0
- package/lib/collective/cards.mjs +201 -0
- package/lib/collective/cards.test.mjs +114 -0
- package/lib/collective/config.mjs +186 -0
- package/lib/collective/config.test.mjs +123 -0
- package/lib/collective/global-config.mjs +113 -0
- package/lib/collective/global-config.test.mjs +75 -0
- package/lib/collective/presence.mjs +201 -0
- package/lib/collective/presence.test.mjs +95 -0
- package/lib/collective/recall.mjs +215 -0
- package/lib/collective/recall.test.mjs +116 -0
- package/lib/comms/send-gate.mjs +554 -0
- package/lib/comms/send-gate.test.mjs +577 -0
- package/lib/comms.mjs +67 -0
- package/lib/comms.test.mjs +41 -0
- package/lib/diagnostics/alerts.mjs +424 -0
- package/lib/diagnostics/alerts.test.mjs +318 -0
- package/lib/diagnostics/backup-freshness.mjs +188 -0
- package/lib/diagnostics/backup-freshness.test.mjs +185 -0
- package/lib/diagnostics/counters.mjs +269 -0
- package/lib/diagnostics/counters.test.mjs +206 -0
- package/lib/diagnostics/events.mjs +188 -0
- package/lib/diagnostics/events.test.mjs +290 -0
- package/lib/diagnostics/otel.mjs +237 -0
- package/lib/diagnostics/otel.test.mjs +196 -0
- package/lib/diagnostics/trace.mjs +216 -0
- package/lib/diagnostics/trace.test.mjs +251 -0
- package/lib/env-compat.mjs +74 -0
- package/lib/env-compat.test.mjs +104 -0
- package/lib/feature-init.mjs +331 -0
- package/lib/fs-atomic.mjs +112 -0
- package/lib/fs-atomic.test.mjs +72 -0
- package/lib/fs-ownership.mjs +111 -0
- package/lib/fs-ownership.test.mjs +158 -0
- package/lib/hooks/bus.mjs +347 -0
- package/lib/hooks/bus.test.mjs +387 -0
- package/lib/index.js +16 -0
- package/lib/learning/config.mjs +106 -0
- package/lib/learning/config.test.mjs +75 -0
- package/lib/learning/counters.mjs +156 -0
- package/lib/learning/counters.test.mjs +69 -0
- package/lib/learning/curator-consolidate.test.mjs +238 -0
- package/lib/learning/curator.mjs +453 -0
- package/lib/learning/curator.test.mjs +106 -0
- package/lib/learning/log.mjs +40 -0
- package/lib/learning/reflect.mjs +534 -0
- package/lib/learning/reflect.test.mjs +0 -0
- package/lib/learning/session-index.mjs +352 -0
- package/lib/learning/session-index.test.mjs +125 -0
- package/lib/learning/skill-writer.mjs +474 -0
- package/lib/learning/skill-writer.test.mjs +210 -0
- package/lib/mcp/server.mjs +328 -0
- package/lib/mcp/server.test.mjs +400 -0
- package/lib/model-router/auth-profiles.mjs +758 -0
- package/lib/model-router/auth-profiles.test.mjs +580 -0
- package/lib/model-router/catalog/anthropic.yaml +153 -0
- package/lib/model-router/catalog/deepseek.yaml +86 -0
- package/lib/model-router/catalog/moonshot.yaml +81 -0
- package/lib/model-router/catalog/qwen.yaml +114 -0
- package/lib/model-router/catalog.mjs +925 -0
- package/lib/model-router/catalog.test.mjs +385 -0
- package/lib/model-router/economics.mjs +564 -0
- package/lib/model-router/economics.test.mjs +344 -0
- package/lib/model-router/failover.mjs +298 -0
- package/lib/model-router/failover.test.mjs +439 -0
- package/lib/model-router/health.mjs +453 -0
- package/lib/model-router/health.test.mjs +338 -0
- package/lib/model-router/integration-coverage.test.mjs +829 -0
- package/lib/model-router/integration.test.mjs +564 -0
- package/lib/model-router/ledger.mjs +402 -0
- package/lib/model-router/ledger.test.mjs +382 -0
- package/lib/model-router/llm-task.mjs +515 -0
- package/lib/model-router/llm-task.test.mjs +392 -0
- package/lib/model-router/org-credentials.mjs +260 -0
- package/lib/model-router/org-credentials.test.mjs +265 -0
- package/lib/model-router/pricing-refresh.mjs +463 -0
- package/lib/model-router/pricing-refresh.test.mjs +286 -0
- package/lib/model-router/reconcile.mjs +429 -0
- package/lib/model-router/reconcile.test.mjs +316 -0
- package/lib/model-router/repair.mjs +471 -0
- package/lib/model-router/repair.test.mjs +180 -0
- package/lib/model-router/resolve.mjs +1206 -0
- package/lib/model-router/spawn.mjs +497 -0
- package/lib/model-router/spawn.test.mjs +425 -0
- package/lib/model-router/taxonomy.mjs +893 -0
- package/lib/model-router/taxonomy.test.mjs +410 -0
- package/lib/model-router.mjs +677 -0
- package/lib/model-router.test.mjs +907 -0
- package/lib/org/activity.mjs +211 -0
- package/lib/org/activity.test.mjs +134 -0
- package/lib/org/approvals.mjs +448 -0
- package/lib/org/approvals.test.mjs +216 -0
- package/lib/org/awareness.mjs +222 -0
- package/lib/org/awareness.test.mjs +159 -0
- package/lib/org/board.mjs +229 -0
- package/lib/org/board.test.mjs +177 -0
- package/lib/org/bootstrap-context.mjs +169 -0
- package/lib/org/bootstrap-context.test.mjs +153 -0
- package/lib/org/client.mjs +1628 -0
- package/lib/org/client.test.mjs +1107 -0
- package/lib/org/cohort-client.mjs +67 -0
- package/lib/org/cohort-client.test.mjs +126 -0
- package/lib/org/cost-sync.mjs +227 -0
- package/lib/org/cost-sync.test.mjs +153 -0
- package/lib/org/doctor.mjs +212 -0
- package/lib/org/doctor.test.mjs +212 -0
- package/lib/org/handoff.mjs +293 -0
- package/lib/org/handoff.test.mjs +269 -0
- package/lib/org/integration-tools.mjs +182 -0
- package/lib/org/integration-tools.test.mjs +160 -0
- package/lib/org/keys.mjs +131 -0
- package/lib/org/keys.test.mjs +92 -0
- package/lib/org/knowledge.mjs +463 -0
- package/lib/org/knowledge.test.mjs +319 -0
- package/lib/org/leases.mjs +335 -0
- package/lib/org/leases.test.mjs +235 -0
- package/lib/org/mesh-integration.test.mjs +127 -0
- package/lib/org/mesh.mjs +459 -0
- package/lib/org/mesh.test.mjs +345 -0
- package/lib/org/messaging.mjs +503 -0
- package/lib/org/messaging.test.mjs +238 -0
- package/lib/org/policy.mjs +345 -0
- package/lib/org/policy.test.mjs +237 -0
- package/lib/org/protocol.checksum +1 -0
- package/lib/org/protocol.checksum.test.mjs +90 -0
- package/lib/org/protocol.mjs +967 -0
- package/lib/org/protocol.test.mjs +264 -0
- package/lib/org/registry.mjs +194 -0
- package/lib/org/registry.test.mjs +100 -0
- package/lib/org/tool-surface-integration.test.mjs +120 -0
- package/lib/org/tool-surface.mjs +2535 -0
- package/lib/org/tool-surface.test.mjs +589 -0
- package/lib/org/ui-parity.mjs +3236 -0
- package/lib/org/ui-parity.test.mjs +348 -0
- package/lib/org/verify.mjs +176 -0
- package/lib/org/verify.test.mjs +194 -0
- package/lib/rag/embed.mjs +188 -0
- package/lib/rag/indexer.mjs +425 -0
- package/lib/rag/rag.test.mjs +505 -0
- package/lib/rag/search.mjs +475 -0
- package/lib/rate-guard.mjs +246 -0
- package/lib/rate-guard.test.mjs +201 -0
- package/lib/render.mjs +112 -0
- package/lib/render.test.mjs +68 -0
- package/lib/resource-governor.mjs +297 -0
- package/lib/resource-governor.test.mjs +262 -0
- package/lib/scheduling/dynamic-jobs.mjs +675 -0
- package/lib/scheduling/dynamic-jobs.test.mjs +344 -0
- package/lib/scheduling/jitter.mjs +0 -0
- package/lib/scheduling/jitter.test.mjs +140 -0
- package/lib/secrets/broker.mjs +315 -0
- package/lib/secrets/broker.test.mjs +280 -0
- package/lib/secrets/providers.mjs +461 -0
- package/lib/secrets/providers.test.mjs +274 -0
- package/lib/security/audit-engine.mjs +684 -0
- package/lib/security/audit-engine.test.mjs +389 -0
- package/lib/security/coerce-args.mjs +552 -0
- package/lib/security/coerce-args.test.mjs +281 -0
- package/lib/security/dangerous-tools.mjs +97 -0
- package/lib/security/dangerous-tools.test.mjs +68 -0
- package/lib/security/external-content.mjs +145 -0
- package/lib/security/external-content.test.mjs +67 -0
- package/lib/security/redact.mjs +592 -0
- package/lib/security/redact.test.mjs +441 -0
- package/lib/security/secret-equal.mjs +73 -0
- package/lib/security/secret-equal.test.mjs +55 -0
- package/lib/session-permissions.mjs +101 -0
- package/lib/session-permissions.test.mjs +100 -0
- package/lib/setup/claude-probe.mjs +74 -0
- package/lib/setup/completeness.mjs +175 -0
- package/lib/setup/completeness.test.mjs +110 -0
- package/lib/setup/context-pack.mjs +173 -0
- package/lib/setup/context-pack.test.mjs +89 -0
- package/lib/setup/enrich.mjs +277 -0
- package/lib/setup/enrich.test.mjs +115 -0
- package/lib/setup/enroll-from-cohort.mjs +441 -0
- package/lib/setup/enroll-from-cohort.test.mjs +233 -0
- package/lib/setup/integration.test.mjs +162 -0
- package/lib/setup/io.mjs +360 -0
- package/lib/setup/io.test.mjs +77 -0
- package/lib/setup/run-generator.mjs +81 -0
- package/lib/setup/runner.mjs +244 -0
- package/lib/setup/runner.test.mjs +132 -0
- package/lib/setup/sections/comms.mjs +173 -0
- package/lib/setup/sections/company.mjs +120 -0
- package/lib/setup/sections/enrich.mjs +138 -0
- package/lib/setup/sections/identity.mjs +182 -0
- package/lib/setup/sections/identity.test.mjs +140 -0
- package/lib/setup/sections/learning.mjs +153 -0
- package/lib/setup/sections/learning.test.mjs +81 -0
- package/lib/setup/sections/messaging.mjs +219 -0
- package/lib/setup/sections/messaging.test.mjs +127 -0
- package/lib/setup/sections/model.mjs +102 -0
- package/lib/setup/sections/operating-model.mjs +78 -0
- package/lib/setup/sections/org.mjs +475 -0
- package/lib/setup/sections/org.test.mjs +313 -0
- package/lib/setup/sections/orgmail.mjs +173 -0
- package/lib/setup/sections/orgmail.test.mjs +118 -0
- package/lib/setup/sections/recovery.mjs +159 -0
- package/lib/setup/sections/recovery.test.mjs +98 -0
- package/lib/setup/sections/tools.mjs +132 -0
- package/lib/setup/sections/verify.mjs +97 -0
- package/lib/setup/sot.mjs +205 -0
- package/lib/setup/sot.test.mjs +81 -0
- package/lib/setup/state.mjs +151 -0
- package/lib/setup/state.test.mjs +92 -0
- package/lib/singleton.js +229 -0
- package/lib/singleton.test.mjs +135 -0
- package/lib/telemetry/alerts.mjs +216 -0
- package/lib/telemetry/alerts.test.mjs +109 -0
- package/lib/telemetry/collect.mjs +512 -0
- package/lib/telemetry/collect.test.mjs +202 -0
- package/lib/tool-definitions-integration.test.mjs +83 -0
- package/lib/tool-definitions.js +738 -0
- package/lib/tool-definitions.test.mjs +437 -0
- package/lib/util/fetch-timeout.mjs +136 -0
- package/lib/util/fetch-timeout.test.mjs +202 -0
- package/lib/util/reconnect.mjs +343 -0
- package/lib/util/reconnect.test.mjs +369 -0
- package/lib/util/unhandled.mjs +205 -0
- package/lib/util/unhandled.test.mjs +216 -0
- package/lib/voice/context-loader.mjs +466 -0
- package/lib/voice/index.mjs +100 -0
- package/lib/voice/openai-realtime.mjs +510 -0
- package/lib/voice/outbound.mjs +542 -0
- package/lib/voice/outbound.test.mjs +69 -0
- package/lib/voice/post-call-brief.mjs +428 -0
- package/lib/voice/provider.mjs +52 -0
- package/lib/voice/session-rotation.mjs +257 -0
- package/lib/voice/stt.mjs +161 -0
- package/lib/voice/stt.test.mjs +226 -0
- package/lib/voice/tool-bridge.mjs +370 -0
- package/lib/voice/tts.mjs +104 -0
- package/lib/voice/twilio-sip-bridge.mjs +288 -0
- package/lib/voice/voice.test.mjs +990 -0
- package/mcp/README.md +80 -0
- package/package.json +151 -0
- package/plugins/maestro-skills/plugin.json +139 -0
- package/plugins/maestro-skills/skills/agents-observe.md +110 -0
- package/plugins/maestro-skills/skills/board-deck.md +68 -0
- package/plugins/maestro-skills/skills/books-close.md +77 -0
- package/plugins/maestro-skills/skills/brand-steward.md +121 -0
- package/plugins/maestro-skills/skills/calendar-plan.md +57 -0
- package/plugins/maestro-skills/skills/call-working-sessions.md +124 -0
- package/plugins/maestro-skills/skills/ccxray-diagnostics.md +91 -0
- package/plugins/maestro-skills/skills/claude-pace.md +61 -0
- package/plugins/maestro-skills/skills/code-review-graph.md +99 -0
- package/plugins/maestro-skills/skills/crm-pipeline.md +65 -0
- package/plugins/maestro-skills/skills/decision-brief.md +89 -0
- package/plugins/maestro-skills/skills/directory-hygiene.md +125 -0
- package/plugins/maestro-skills/skills/draft-comms.md +84 -0
- package/plugins/maestro-skills/skills/evening-wrap.md +53 -0
- package/plugins/maestro-skills/skills/files-find.md +65 -0
- package/plugins/maestro-skills/skills/generative-ui.md +228 -0
- package/plugins/maestro-skills/skills/hiring-triage.md +74 -0
- package/plugins/maestro-skills/skills/inbox-triage.md +61 -0
- package/plugins/maestro-skills/skills/mail-triage.md +86 -0
- package/plugins/maestro-skills/skills/morning-brief.md +54 -0
- package/plugins/maestro-skills/skills/native-artifacts.md +157 -0
- package/plugins/maestro-skills/skills/org-board.md +133 -0
- package/plugins/maestro-skills/skills/org-credential.md +68 -0
- package/plugins/maestro-skills/skills/org-recall.md +81 -0
- package/plugins/maestro-skills/skills/pipeline-review.md +76 -0
- package/plugins/maestro-skills/skills/regulatory-status.md +81 -0
- package/plugins/maestro-skills/skills/router-why.md +78 -0
- package/plugins/maestro-skills/skills/schedule-meeting.md +91 -0
- package/plugins/maestro-skills/skills/session-search.md +71 -0
- package/plugins/maestro-skills/skills/set-reminder.md +93 -0
- package/plugins/maestro-skills/skills/slack-followup.md +64 -0
- package/plugins/maestro-skills/skills/team-activity.md +86 -0
- package/plugins/maestro-skills/skills/weekly-memo.md +70 -0
- package/policies/action-classification.yaml +114 -0
- package/policies/ai-disclosure.yaml +294 -0
- package/policies/communication-style.md +139 -0
- package/policies/information-barriers.yaml +118 -0
- package/policies/prompt-injection-defence.yaml +138 -0
- package/public/assets/icon-dark.png +0 -0
- package/public/assets/icon-dark.svg +9 -0
- package/public/assets/icon-light.svg +9 -0
- package/public/assets/logo-dark.svg +15 -0
- package/public/assets/logo-light.svg +15 -0
- package/scaffold/.mcp.json +7 -0
- package/scaffold/CLAUDE.md +368 -0
- package/scaffold/config/agent.json +55 -0
- package/scaffold/config/agent.ts +76 -0
- package/scaffold/config/agent.ts.example +89 -0
- package/scaffold/config/alerts.yaml +23 -0
- package/scaffold/config/allowlist.yaml.example +25 -0
- package/scaffold/config/caller-id-map.yaml +46 -0
- package/scaffold/config/collective.yaml +49 -0
- package/scaffold/config/company.json +20 -0
- package/scaffold/config/known-agents.json +6 -0
- package/scaffold/config/learning.yaml +55 -0
- package/scaffold/config/model-routing.yaml.example +104 -0
- package/scaffold/config/org.yaml +25 -0
- package/scaffold/config/orgmail.yaml.example +19 -0
- package/scaffold/config/recovery.yaml +72 -0
- package/scaffold/config/secrets.yaml +27 -0
- package/scaffold/config/slack.yaml.example +35 -0
- package/scaffold/config/telegram.yaml.example +38 -0
- package/scaffold/config/voice.yaml.example +89 -0
- package/scaffold/config/whatsapp.yaml.example +39 -0
- package/schedules/README.md +49 -0
- package/schedules/triggers/backlog-executor.md +102 -0
- package/schedules/triggers/brand-steward.md +72 -0
- package/schedules/triggers/daily-evening-wrap.md +159 -0
- package/schedules/triggers/daily-midday-sweep.md +58 -0
- package/schedules/triggers/daily-morning-brief.md +55 -0
- package/schedules/triggers/directory-hygiene.md +81 -0
- package/schedules/triggers/dynamic-jobs.md +40 -0
- package/schedules/triggers/inbox-processor.md +115 -0
- package/schedules/triggers/meeting-action-capture.md +60 -0
- package/schedules/triggers/meeting-prep.md +69 -0
- package/schedules/triggers/messaging-inbound.md +50 -0
- package/schedules/triggers/org-pulse.md +24 -0
- package/schedules/triggers/quarterly-self-assessment.md +54 -0
- package/schedules/triggers/weekly-engineering-health.md +37 -0
- package/schedules/triggers/weekly-execution.md +65 -0
- package/schedules/triggers/weekly-hiring.md +53 -0
- package/schedules/triggers/weekly-priorities.md +38 -0
- package/schedules/triggers/weekly-strategic-memo.md +124 -0
- package/scripts/archive-email.sh +55 -0
- package/scripts/cadence/cadence-status.mjs +36 -0
- package/scripts/cadence/enqueue-cadence-tick.mjs +174 -0
- package/scripts/cadence/enqueue-cadence-tick.test.mjs +187 -0
- package/scripts/cadence/launchd-cadence-wrapper.sh +85 -0
- package/scripts/cadence/launchd-cloud-relay-wrapper.sh +95 -0
- package/scripts/cadence/launchd-socket-mode-wrapper.sh +95 -0
- package/scripts/ci/check-docs-accuracy.mjs +493 -0
- package/scripts/ci/check-docs-accuracy.test.mjs +409 -0
- package/scripts/ci/check-exports-exist.mjs +140 -0
- package/scripts/ci/check-files-exist.mjs +107 -0
- package/scripts/ci/check-no-build-artifacts.mjs +111 -0
- package/scripts/ci/check-no-build-artifacts.test.mjs +71 -0
- package/scripts/ci/check-no-confidential.mjs +198 -0
- package/scripts/ci/check-no-conflict-markers.mjs +169 -0
- package/scripts/ci/check-no-residual-identity.mjs +163 -0
- package/scripts/ci/check-no-residual-identity.test.mjs +89 -0
- package/scripts/ci/check-tarball-fidelity.mjs +205 -0
- package/scripts/ci/check-unresolved-tokens.mjs +83 -0
- package/scripts/ci/check.mjs +109 -0
- package/scripts/ci/check.test.mjs +194 -0
- package/scripts/ci/run-coverage.mjs +82 -0
- package/scripts/ci/run-tests.mjs +71 -0
- package/scripts/cloud-relay/README.md +59 -0
- package/scripts/cloud-relay/index.mjs +233 -0
- package/scripts/cloud-relay/package.json +15 -0
- package/scripts/cloud-relay/railway.json +13 -0
- package/scripts/cloud-relay/voice/README.md +94 -0
- package/scripts/cloud-relay/voice/package-lock.json +39 -0
- package/scripts/cloud-relay/voice/package.json +16 -0
- package/scripts/cloud-relay/voice/railway.json +13 -0
- package/scripts/cloud-relay/voice/server.mjs +532 -0
- package/scripts/collective/hook-runner.mjs +211 -0
- package/scripts/collective/hook-runner.test.mjs +90 -0
- package/scripts/collective/org-pulse.mjs +72 -0
- package/scripts/collective/org-sync.mjs +61 -0
- package/scripts/collective/recall.mjs +45 -0
- package/scripts/collective/who.mjs +30 -0
- package/scripts/comms-monitor.sh +288 -0
- package/scripts/configure-whatsapp-sandbox.sh +201 -0
- package/scripts/continuous-monitor.sh +91 -0
- package/scripts/cost/fleet-digest.mjs +407 -0
- package/scripts/cost/fleet-digest.test.mjs +207 -0
- package/scripts/cost/track-claude-usage.mjs +169 -0
- package/scripts/daemon/agent-daemon.mjs +989 -0
- package/scripts/daemon/agent-daemon.test.mjs +525 -0
- package/scripts/daemon/cadence-consumer-governance.test.mjs +220 -0
- package/scripts/daemon/cadence-consumer.mjs +1080 -0
- package/scripts/daemon/cadence-consumer.test.mjs +770 -0
- package/scripts/daemon/cadence-handlers.mjs +1121 -0
- package/scripts/daemon/cadence-handlers.test.mjs +617 -0
- package/scripts/daemon/classifier.mjs +704 -0
- package/scripts/daemon/classifier.test.mjs +238 -0
- package/scripts/daemon/classify-kind.mjs +54 -0
- package/scripts/daemon/classify-kind.test.mjs +40 -0
- package/scripts/daemon/context-compiler.mjs +605 -0
- package/scripts/daemon/context-compiler.test.mjs +300 -0
- package/scripts/daemon/dispatcher-cooldown.test.mjs +122 -0
- package/scripts/daemon/dispatcher-governance.test.mjs +886 -0
- package/scripts/daemon/dispatcher.mjs +1516 -0
- package/scripts/daemon/health.mjs +72 -0
- package/scripts/daemon/inbox-deferral.mjs +210 -0
- package/scripts/daemon/inbox-deferral.test.mjs +242 -0
- package/scripts/daemon/integration.test.mjs +149 -0
- package/scripts/daemon/launchd-wrapper-generic.sh +96 -0
- package/scripts/daemon/launchd-wrapper-slack-events.sh +37 -0
- package/scripts/daemon/launchd-wrapper.sh +91 -0
- package/scripts/daemon/lib/session-router.mjs +274 -0
- package/scripts/daemon/lib/session-router.test.mjs +295 -0
- package/scripts/daemon/maestro-daemon.mjs +275 -0
- package/scripts/daemon/prompt-builder.mjs +685 -0
- package/scripts/daemon/prompt-builder.test.mjs +213 -0
- package/scripts/daemon/responder.mjs +854 -0
- package/scripts/daemon/session-lock.mjs +721 -0
- package/scripts/daemon/session-lock.test.mjs +252 -0
- package/scripts/daemon/session-outcomes.mjs +640 -0
- package/scripts/daemon/session-outcomes.test.mjs +533 -0
- package/scripts/daemon/typing-registry.mjs +90 -0
- package/scripts/daemon/typing-registry.test.mjs +77 -0
- package/scripts/daemon/voice-webhook-server.mjs +804 -0
- package/scripts/decisions/capture-decision.mjs +116 -0
- package/scripts/disclosure_assessment.py +873 -0
- package/scripts/disclosure_boundaries.py +562 -0
- package/scripts/email-signature-principal.html +52 -0
- package/scripts/email-signature.html +60 -0
- package/scripts/email_quote_thread.py +167 -0
- package/scripts/email_thread_dedup.py +362 -0
- package/scripts/emergency-stop.sh +81 -0
- package/scripts/healthcheck.sh +116 -0
- package/scripts/hooks/block-mcp-cohort-send.sh +15 -0
- package/scripts/hooks/block-mcp-slack-send.sh +7 -0
- package/scripts/hooks/post-action-log.sh +126 -0
- package/scripts/hooks/pre-send-audit.sh +174 -0
- package/scripts/hooks/pre-send-audit.test.mjs +215 -0
- package/scripts/hooks/session-end-log.sh +27 -0
- package/scripts/hooks/session-start-banner.sh +115 -0
- package/scripts/huddle/audio-bridge.mjs +664 -0
- package/scripts/huddle/boot-slack-cdp.sh +102 -0
- package/scripts/huddle/huddle-controller.mjs +942 -0
- package/scripts/huddle/huddle-server.mjs +1229 -0
- package/scripts/huddle/launch-slack.sh +232 -0
- package/scripts/huddle/openai-realtime-bridge.mjs +462 -0
- package/scripts/huddle/package-lock.json +62 -0
- package/scripts/huddle/package.json +22 -0
- package/scripts/huddle/setup-audio.sh +239 -0
- package/scripts/huddle/start-call.mjs +318 -0
- package/scripts/huddle/test-pipeline.mjs +263 -0
- package/scripts/learning/consolidate-skills.mjs +72 -0
- package/scripts/learning/session-search.mjs +125 -0
- package/scripts/llm_email_dedup.py +442 -0
- package/scripts/local-triggers/generate-plists.sh +432 -0
- package/scripts/local-triggers/generate-plists.test.mjs +413 -0
- package/scripts/local-triggers/install-all.sh +49 -0
- package/scripts/local-triggers/plists/.gitkeep +0 -0
- package/scripts/local-triggers/run-trigger.sh +63 -0
- package/scripts/local-triggers/templates/rag-reindex.plist.template +47 -0
- package/scripts/local-triggers/templates/voice-relay-poller.plist.template +54 -0
- package/scripts/local-triggers/templates/voice-tunnel.plist.template +55 -0
- package/scripts/local-triggers/templates/voice-webhook.plist.template +51 -0
- package/scripts/maintenance/backup-to-cloud.sh +124 -0
- package/scripts/maintenance/health-check.sh +377 -0
- package/scripts/media-generation/README.md +105 -0
- package/scripts/media-generation/gemini-image-client.mjs +173 -0
- package/scripts/media-generation/generate-assets.mjs +289 -0
- package/scripts/media-generation/veo-video-client.mjs +219 -0
- package/scripts/org/send-orgmail.mjs +227 -0
- package/scripts/outbound-dedup-cleanup.sh +43 -0
- package/scripts/outbound-dedup.sh +477 -0
- package/scripts/outbound_dedup.py +115 -0
- package/scripts/parse-voice-transcript.mjs +481 -0
- package/scripts/pdf-generation/README.md +63 -0
- package/scripts/pdf-generation/build-document.mjs +247 -0
- package/scripts/pdf-generation/templates/board-pack.latex +136 -0
- package/scripts/pdf-generation/templates/corporate-letter.latex +126 -0
- package/scripts/pdf-generation/templates/memo.latex +114 -0
- package/scripts/poll-slack-events.sh +35 -0
- package/scripts/poller/calendar-poller.mjs +12 -0
- package/scripts/poller/gmail-poller.mjs +192 -0
- package/scripts/poller/imap-client.mjs +289 -0
- package/scripts/poller/inbox-scan-poller.mjs +156 -0
- package/scripts/poller/inbox-scan-poller.test.mjs +231 -0
- package/scripts/poller/index.mjs +73 -0
- package/scripts/poller/intra-session-check.mjs +285 -0
- package/scripts/poller/lib/cloud-relay-dedup.mjs +88 -0
- package/scripts/poller/lib/cloud-relay-dedup.test.mjs +133 -0
- package/scripts/poller/lib/slash-command-handlers.mjs +177 -0
- package/scripts/poller/secondary-gmail-poller.mjs +132 -0
- package/scripts/poller/slack-cloud-relay-client.mjs +368 -0
- package/scripts/poller/slack-poller.mjs +854 -0
- package/scripts/poller/slack-socket-mode.mjs +917 -0
- package/scripts/poller/slack-socket-mode.test.mjs +753 -0
- package/scripts/poller/trigger.mjs +75 -0
- package/scripts/poller/utils.mjs +371 -0
- package/scripts/poller/voice-cloud-relay-client.mjs +179 -0
- package/scripts/poller/voice-poller.mjs +236 -0
- package/scripts/poller-launchd/install.sh +66 -0
- package/scripts/poller-launchd/poller.plist.template +40 -0
- package/scripts/poller-launchd/whatsapp-handler.plist.template +39 -0
- package/scripts/post-interaction-indexer.py +1598 -0
- package/scripts/pre-draft-context.py +994 -0
- package/scripts/pre_draft_lookup.py +258 -0
- package/scripts/rag/build-index.mjs +47 -0
- package/scripts/rag/ingest.mjs +111 -0
- package/scripts/rag/search.mjs +119 -0
- package/scripts/rag-indexer.py +629 -0
- package/scripts/restore-from-backup.sh +248 -0
- package/scripts/restore-from-backup.test.mjs +178 -0
- package/scripts/resume-operations.sh +80 -0
- package/scripts/search-secondary-inbox.py +181 -0
- package/scripts/secondary-inbox-poller.py +437 -0
- package/scripts/self-optimization/compute-metrics.py +398 -0
- package/scripts/send-email-as-principal.py +369 -0
- package/scripts/send-email-threaded.py +392 -0
- package/scripts/send-email-with-attachment.py +377 -0
- package/scripts/send-email.sh +131 -0
- package/scripts/send-sms.sh +175 -0
- package/scripts/send-whatsapp.sh +292 -0
- package/scripts/session-start.sh +106 -0
- package/scripts/setup/boot-claude-session.sh +94 -0
- package/scripts/setup/configure-macos.sh +674 -0
- package/scripts/setup/configure-twilio-sip-trunk.mjs +207 -0
- package/scripts/setup/configure-voice-tunnel.mjs +182 -0
- package/scripts/setup/generate-agent-env.mjs +92 -0
- package/scripts/setup/generate-agent-package-json.mjs +222 -0
- package/scripts/setup/generate-agent-package-json.test.mjs +143 -0
- package/scripts/setup/generate-autonomy.mjs +60 -0
- package/scripts/setup/generate-backlog.mjs +101 -0
- package/scripts/setup/generate-cadences.mjs +92 -0
- package/scripts/setup/generate-capability.mjs +162 -0
- package/scripts/setup/generate-charter.mjs +76 -0
- package/scripts/setup/generate-comms.mjs +59 -0
- package/scripts/setup/generate-company.mjs +92 -0
- package/scripts/setup/init-agent.sh +547 -0
- package/scripts/setup/init-agent.test.mjs +151 -0
- package/scripts/setup/init-archetype.mjs +74 -0
- package/scripts/setup/init-backup.mjs +54 -0
- package/scripts/setup/init-cadence-bus.mjs +60 -0
- package/scripts/setup/init-channel-bus.mjs +46 -0
- package/scripts/setup/init-cost-tracking.mjs +45 -0
- package/scripts/setup/init-decision-capture.mjs +66 -0
- package/scripts/setup/init-known-agents.mjs +57 -0
- package/scripts/setup/init-learning.mjs +70 -0
- package/scripts/setup/init-memory-executive.mjs +45 -0
- package/scripts/setup/init-model-router.mjs +124 -0
- package/scripts/setup/init-rag.mjs +174 -0
- package/scripts/setup/init-session-router.mjs +38 -0
- package/scripts/setup/init-slack-socket-mode.mjs +260 -0
- package/scripts/setup/init-telegram.mjs +165 -0
- package/scripts/setup/init-voice-realtime.mjs +204 -0
- package/scripts/setup/init-whatsapp-baileys.mjs +77 -0
- package/scripts/setup/install-dev-tools.sh +150 -0
- package/scripts/setup/lib/install-plist.mjs +95 -0
- package/scripts/setup/migrate-agent-to-sot.mjs +192 -0
- package/scripts/setup/render-environment-yaml.mjs +133 -0
- package/scripts/slack-events-ctl.sh +177 -0
- package/scripts/slack-events-server.mjs +1045 -0
- package/scripts/slack-react.mjs +89 -0
- package/scripts/slack-responded.sh +232 -0
- package/scripts/slack-send.sh +287 -0
- package/scripts/slack-typing.mjs +196 -0
- package/scripts/slack-upload-v2.py +95 -0
- package/scripts/sms-handler.mjs +450 -0
- package/scripts/spawn-session.sh +120 -0
- package/scripts/sync-protocol.mjs +217 -0
- package/scripts/system-verify.sh +184 -0
- package/scripts/test-email-thread-dedup.py +239 -0
- package/scripts/test-information-barriers.py +484 -0
- package/scripts/test-llm-email-dedup.py +251 -0
- package/scripts/test-pre-draft-integration.py +203 -0
- package/scripts/test-rag-phase2.sh +442 -0
- package/scripts/test-rag-search.sh +251 -0
- package/scripts/test-voice-parser.mjs +316 -0
- package/scripts/user-context-search.py +659 -0
- package/scripts/validate_outbound.py +1504 -0
- package/scripts/watchdog/ai.maestro.memory-watchdog.plist +41 -0
- package/scripts/watchdog/force-reboot.sh +157 -0
- package/scripts/watchdog/memory-watchdog.sh +473 -0
- package/scripts/whatsapp-handler.mjs +538 -0
- package/teams/desktop-operations.yaml +34 -0
- package/teams/executive-office.yaml +27 -0
- package/teams/legal-and-regulatory.yaml +24 -0
- package/teams/platform-and-engineering.yaml +23 -0
- package/teams/strategy-and-growth.yaml +29 -0
- package/workflows/continuous/backlog-executor.yaml +141 -0
- package/workflows/continuous/inbound-monitor.yaml +168 -0
- package/workflows/daily/applicant-triage.yaml +197 -0
- package/workflows/daily/comms-triage.yaml +80 -0
- package/workflows/daily/evening-wrap.yaml +105 -0
- package/workflows/daily/morning-brief.yaml +164 -0
- package/workflows/daily/slack-followup-sweep.yaml +87 -0
- package/workflows/event-driven/README.md +50 -0
- package/workflows/event-driven/agent-failure-investigation.yaml +137 -0
- package/workflows/event-driven/pr-review.yaml +107 -0
- package/workflows/monthly/board-readiness.yaml +76 -0
- package/workflows/quarterly/strategic-scenario-analysis.yaml +85 -0
- package/workflows/session-protocol.md +171 -0
- package/workflows/weekly/engineering-health.yaml +154 -0
- package/workflows/weekly/hiring-review.yaml +169 -0
- package/workflows/weekly/rollup-pipeline-review.yaml +76 -0
- package/workflows/weekly/strategic-memo.yaml +79 -0
|
@@ -0,0 +1,532 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Maestro — OpenAI Realtime Voice Cloud Relay (Railway)
|
|
3
|
+
*
|
|
4
|
+
* Public HTTPS service deployed on Railway that bridges OpenAI's
|
|
5
|
+
* `realtime.call.incoming` webhook into a Mac-mini-resident maestro
|
|
6
|
+
* daemon over a buffer-and-poll channel.
|
|
7
|
+
*
|
|
8
|
+
* Why this exists: cloudflared "trycloudflare.com" URLs are ephemeral
|
|
9
|
+
* (rotate on every restart). For a production OpenAI webhook config
|
|
10
|
+
* we need a stable URL that never changes.
|
|
11
|
+
*
|
|
12
|
+
* Architecture:
|
|
13
|
+
* 1. OpenAI POSTs realtime.call.incoming → this service
|
|
14
|
+
* 2. Service verifies signature (HMAC-SHA256 against OPENAI_WEBHOOK_SECRET)
|
|
15
|
+
* 3. Service buffers the event in memory (TTL 10 min, dedup by call_id)
|
|
16
|
+
* 4. Service returns 200 immediately so OpenAI's call setup proceeds
|
|
17
|
+
* 5. Local Mac polls /openai/realtime/calls every 2-5s, sees new
|
|
18
|
+
* events, calls OpenAI's /v1/realtime/calls/<id>/accept directly
|
|
19
|
+
* from the Mac, then opens the WebSocket from the Mac. Tool
|
|
20
|
+
* bridge / RAG / context-loading all happen on the Mac with
|
|
21
|
+
* zero round-trips through Railway.
|
|
22
|
+
*
|
|
23
|
+
* Endpoints:
|
|
24
|
+
* POST /openai/realtime/incoming — OpenAI webhook receiver. Verifies
|
|
25
|
+
* signature, buffers event, ack 200.
|
|
26
|
+
* GET /openai/realtime/calls — Drain buffer (auth-required: bearer
|
|
27
|
+
* VOICE_RELAY_POLL_TOKEN).
|
|
28
|
+
* POST /openai/realtime/test — Echo back; for connectivity probes.
|
|
29
|
+
* GET /health — Liveness probe.
|
|
30
|
+
*
|
|
31
|
+
* Env (set via Railway dashboard or `railway variables`):
|
|
32
|
+
* PORT — provided by Railway
|
|
33
|
+
* OPENAI_WEBHOOK_SECRET — copy from OpenAI dashboard "signing secret"
|
|
34
|
+
* VOICE_RELAY_POLL_TOKEN — random hex; the Mac uses this as bearer
|
|
35
|
+
* BUFFER_TTL_MS — optional, default 600000 (10 min)
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
import http from "node:http";
|
|
39
|
+
import crypto from "node:crypto";
|
|
40
|
+
import { WebSocketServer, WebSocket } from "ws";
|
|
41
|
+
|
|
42
|
+
const PORT = parseInt(process.env.PORT || "8080", 10);
|
|
43
|
+
const OPENAI_WEBHOOK_SECRET = process.env.OPENAI_WEBHOOK_SECRET || "";
|
|
44
|
+
const POLL_TOKEN = process.env.VOICE_RELAY_POLL_TOKEN || "";
|
|
45
|
+
const OPENAI_API_KEY = process.env.OPENAI_API_KEY || "";
|
|
46
|
+
const VOICE_TWIML_TOKEN = process.env.VOICE_TWIML_TOKEN || ""; // bearer for /twilio/twiml endpoints
|
|
47
|
+
const BUFFER_TTL_MS = parseInt(process.env.BUFFER_TTL_MS || "600000", 10);
|
|
48
|
+
const MAX_BUFFER_SIZE = parseInt(process.env.MAX_BUFFER_SIZE || "500", 10);
|
|
49
|
+
|
|
50
|
+
if (!OPENAI_WEBHOOK_SECRET) console.warn("[warn] OPENAI_WEBHOOK_SECRET not set — webhook signature verification disabled");
|
|
51
|
+
if (!POLL_TOKEN) console.warn("[warn] VOICE_RELAY_POLL_TOKEN not set — poll endpoint is unauthenticated");
|
|
52
|
+
if (!OPENAI_API_KEY) console.warn("[warn] OPENAI_API_KEY not set — outbound Media Streams bridge disabled");
|
|
53
|
+
|
|
54
|
+
// ── In-memory buffer ───────────────────────────────────────────────────────
|
|
55
|
+
|
|
56
|
+
class EventBuffer {
|
|
57
|
+
constructor(name) { this.name = name; this.events = []; this.seen = new Map(); }
|
|
58
|
+
push(event) {
|
|
59
|
+
const id = event.id;
|
|
60
|
+
if (id && this.seen.has(id)) return false;
|
|
61
|
+
if (id) this.seen.set(id, Date.now());
|
|
62
|
+
this.events.push(event);
|
|
63
|
+
this.gc();
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
drain() { return this.events.splice(0, this.events.length); }
|
|
67
|
+
gc() {
|
|
68
|
+
const now = Date.now();
|
|
69
|
+
while (this.events.length && now - new Date(this.events[0].received_at).getTime() > BUFFER_TTL_MS) {
|
|
70
|
+
this.events.shift();
|
|
71
|
+
}
|
|
72
|
+
while (this.events.length > MAX_BUFFER_SIZE) this.events.shift();
|
|
73
|
+
for (const [id, ts] of this.seen) if (now - ts > BUFFER_TTL_MS) this.seen.delete(id);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
const buffer = new EventBuffer("openai_voice");
|
|
77
|
+
setInterval(() => buffer.gc(), 60000).unref();
|
|
78
|
+
|
|
79
|
+
// ── HTTP helpers ───────────────────────────────────────────────────────────
|
|
80
|
+
|
|
81
|
+
function readBody(req) {
|
|
82
|
+
return new Promise((resolve, reject) => {
|
|
83
|
+
let data = "";
|
|
84
|
+
req.setEncoding("utf-8");
|
|
85
|
+
req.on("data", (c) => { data += c; });
|
|
86
|
+
req.on("end", () => resolve(data));
|
|
87
|
+
req.on("error", reject);
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
function json(res, status, payload) {
|
|
91
|
+
res.writeHead(status, { "content-type": "application/json" });
|
|
92
|
+
res.end(JSON.stringify(payload));
|
|
93
|
+
}
|
|
94
|
+
function text(res, status, body) {
|
|
95
|
+
res.writeHead(status, { "content-type": "text/plain" });
|
|
96
|
+
res.end(body);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ── OpenAI webhook signature verification ──────────────────────────────────
|
|
100
|
+
// OpenAI sends `openai-signature: <hex>` (or sometimes `webhook-signature:
|
|
101
|
+
// t=<ts>,v1=<hex>` Stripe-style). We accept either.
|
|
102
|
+
|
|
103
|
+
function verifyOpenAISignature(rawBody, headers) {
|
|
104
|
+
if (!OPENAI_WEBHOOK_SECRET) return true; // dev mode
|
|
105
|
+
const headerSig = headers["openai-signature"] || headers["webhook-signature"] || "";
|
|
106
|
+
if (!headerSig) return false;
|
|
107
|
+
let providedHex = headerSig;
|
|
108
|
+
if (headerSig.includes(",")) {
|
|
109
|
+
const parts = Object.fromEntries(
|
|
110
|
+
headerSig.split(",").map((kv) => kv.split("=").map((s) => s.trim()))
|
|
111
|
+
);
|
|
112
|
+
providedHex = parts.v1 || parts.v0 || "";
|
|
113
|
+
}
|
|
114
|
+
const expected = crypto.createHmac("sha256", OPENAI_WEBHOOK_SECRET).update(rawBody || "").digest("hex");
|
|
115
|
+
if (providedHex.length !== expected.length) return false;
|
|
116
|
+
try {
|
|
117
|
+
return crypto.timingSafeEqual(Buffer.from(providedHex), Buffer.from(expected));
|
|
118
|
+
} catch {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function checkPollAuth(req, res) {
|
|
124
|
+
if (!POLL_TOKEN) return true;
|
|
125
|
+
const auth = req.headers["authorization"] || "";
|
|
126
|
+
if (auth === `Bearer ${POLL_TOKEN}`) return true;
|
|
127
|
+
text(res, 401, "unauthorized");
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ── Handlers ───────────────────────────────────────────────────────────────
|
|
132
|
+
|
|
133
|
+
async function handleIncoming(req, res) {
|
|
134
|
+
const rawBody = await readBody(req);
|
|
135
|
+
if (!verifyOpenAISignature(rawBody, req.headers)) {
|
|
136
|
+
console.warn("[openai-voice] signature mismatch");
|
|
137
|
+
return json(res, 403, { ok: false, error: "openai_signature_invalid" });
|
|
138
|
+
}
|
|
139
|
+
let parsed;
|
|
140
|
+
try { parsed = JSON.parse(rawBody); }
|
|
141
|
+
catch { return json(res, 400, { ok: false, error: "invalid_json" }); }
|
|
142
|
+
|
|
143
|
+
const callId = parsed.data?.call_id || parsed.call_id || `nocallid-${Date.now()}`;
|
|
144
|
+
const eventType = parsed.type || "unknown";
|
|
145
|
+
|
|
146
|
+
// Critical-path: OpenAI gives us roughly 5 seconds to POST /accept on a
|
|
147
|
+
// realtime.call.incoming webhook. The Mac-mini poller runs at ~750ms
|
|
148
|
+
// intervals, plus there's a ~50–200ms hop home, plus accept latency —
|
|
149
|
+
// worst-case we miss the window. So accept FROM RAILWAY immediately;
|
|
150
|
+
// the local handler then just attaches the event WebSocket whenever
|
|
151
|
+
// its next poll picks the event up. Audio is bridged inside OpenAI, so
|
|
152
|
+
// the local WS attach can lag by hundreds of ms without harm.
|
|
153
|
+
let acceptedByRelay = false;
|
|
154
|
+
let acceptError = null;
|
|
155
|
+
if (eventType === "realtime.call.incoming" && OPENAI_API_KEY && callId && !callId.startsWith("nocallid-")) {
|
|
156
|
+
try {
|
|
157
|
+
const acceptResp = await fetch(`https://api.openai.com/v1/realtime/calls/${encodeURIComponent(callId)}/accept`, {
|
|
158
|
+
method: "POST",
|
|
159
|
+
headers: { Authorization: `Bearer ${OPENAI_API_KEY}` },
|
|
160
|
+
});
|
|
161
|
+
if (acceptResp.ok) {
|
|
162
|
+
acceptedByRelay = true;
|
|
163
|
+
console.log(`[openai-voice] relay-accepted call=${callId} (status=${acceptResp.status})`);
|
|
164
|
+
} else {
|
|
165
|
+
acceptError = `accept ${acceptResp.status}: ${(await acceptResp.text()).slice(0, 200)}`;
|
|
166
|
+
console.warn(`[openai-voice] relay-accept failed call=${callId}: ${acceptError}`);
|
|
167
|
+
}
|
|
168
|
+
} catch (err) {
|
|
169
|
+
acceptError = err.message;
|
|
170
|
+
console.warn(`[openai-voice] relay-accept threw call=${callId}: ${err.message}`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const accepted = buffer.push({
|
|
175
|
+
id: `${callId}-${eventType}`,
|
|
176
|
+
received_at: new Date().toISOString(),
|
|
177
|
+
event_type: eventType,
|
|
178
|
+
call_id: callId,
|
|
179
|
+
accepted_by_relay: acceptedByRelay,
|
|
180
|
+
accept_error: acceptError,
|
|
181
|
+
// Store the parsed object for human inspection AND the raw body
|
|
182
|
+
// bytes so the local poller can re-verify HMAC against what OpenAI
|
|
183
|
+
// actually signed (re-stringifying parsed loses key-order/whitespace
|
|
184
|
+
// and breaks signature validation downstream).
|
|
185
|
+
payload: parsed,
|
|
186
|
+
raw_body: rawBody,
|
|
187
|
+
});
|
|
188
|
+
console.log(`[openai-voice] ${accepted ? "buffered" : "dedup"} ${eventType} call=${callId} accepted_by_relay=${acceptedByRelay}`);
|
|
189
|
+
return json(res, 200, { ok: true, dedup: !accepted, call_id: callId, accepted_by_relay: acceptedByRelay });
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function handlePoll(req, res) {
|
|
193
|
+
if (!checkPollAuth(req, res)) return;
|
|
194
|
+
const events = buffer.drain();
|
|
195
|
+
return json(res, 200, {
|
|
196
|
+
ok: true,
|
|
197
|
+
count: events.length,
|
|
198
|
+
events,
|
|
199
|
+
server_time: new Date().toISOString(),
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function handleTest(req, res) {
|
|
204
|
+
return json(res, 200, {
|
|
205
|
+
ok: true,
|
|
206
|
+
service: "openai-voice-cloud-relay",
|
|
207
|
+
signature_required: !!OPENAI_WEBHOOK_SECRET,
|
|
208
|
+
poll_auth_required: !!POLL_TOKEN,
|
|
209
|
+
server_time: new Date().toISOString(),
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function handleHealth(req, res) {
|
|
214
|
+
return json(res, 200, {
|
|
215
|
+
ok: true,
|
|
216
|
+
service: "openai-voice-cloud-relay",
|
|
217
|
+
uptime_sec: Math.round(process.uptime()),
|
|
218
|
+
buffer: { events: buffer.events.length, seen: buffer.seen.size },
|
|
219
|
+
bridges: { active: activeBridges.size },
|
|
220
|
+
config: {
|
|
221
|
+
signature_required: !!OPENAI_WEBHOOK_SECRET,
|
|
222
|
+
poll_auth_required: !!POLL_TOKEN,
|
|
223
|
+
twiml_auth_required: !!VOICE_TWIML_TOKEN,
|
|
224
|
+
openai_key_loaded: !!OPENAI_API_KEY,
|
|
225
|
+
media_streams_enabled: !!OPENAI_API_KEY,
|
|
226
|
+
buffer_ttl_ms: BUFFER_TTL_MS,
|
|
227
|
+
},
|
|
228
|
+
server_time: new Date().toISOString(),
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// ── Outbound bridge state ──────────────────────────────────────────────────
|
|
233
|
+
// When a call is queued via /twilio/queue-outbound, we store its brief +
|
|
234
|
+
// session config keyed by callSid (Twilio's identifier) so the WebSocket
|
|
235
|
+
// handler can fetch it when Twilio connects with that callSid.
|
|
236
|
+
const queuedOutbound = new Map(); // callSid → { brief, voice, instructions, identity }
|
|
237
|
+
const QUEUED_TTL_MS = 5 * 60_000;
|
|
238
|
+
function gcQueued() {
|
|
239
|
+
const now = Date.now();
|
|
240
|
+
for (const [k, v] of queuedOutbound) {
|
|
241
|
+
if (now - v.queued_at > QUEUED_TTL_MS) queuedOutbound.delete(k);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
setInterval(gcQueued, 60_000).unref();
|
|
245
|
+
|
|
246
|
+
// Active bridges (Twilio WS ↔ OpenAI WS), keyed by Twilio streamSid.
|
|
247
|
+
const activeBridges = new Map();
|
|
248
|
+
|
|
249
|
+
// ── Twilio outbound endpoints ──────────────────────────────────────────────
|
|
250
|
+
|
|
251
|
+
function checkTwimlAuth(req, res, url) {
|
|
252
|
+
// Bearer token via Authorization header OR ?token= query param (Twilio
|
|
253
|
+
// may not set Authorization on TwiML callbacks, so query param is the
|
|
254
|
+
// fallback).
|
|
255
|
+
if (!VOICE_TWIML_TOKEN) return true;
|
|
256
|
+
const tokenQ = url.searchParams.get("token");
|
|
257
|
+
const auth = req.headers["authorization"] || "";
|
|
258
|
+
if (auth === `Bearer ${VOICE_TWIML_TOKEN}`) return true;
|
|
259
|
+
if (tokenQ && tokenQ === VOICE_TWIML_TOKEN) return true;
|
|
260
|
+
text(res, 401, "unauthorized");
|
|
261
|
+
return false;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* POST /twilio/queue-outbound
|
|
266
|
+
* Body: { call_sid, brief, voice?, identity?, session?, peer?, peer_name? }
|
|
267
|
+
*
|
|
268
|
+
* Called by the LOCAL agent right before / after it tells Twilio to dial
|
|
269
|
+
* the user's PSTN number. Stores the session config keyed by the Twilio
|
|
270
|
+
* call_sid so when Twilio connects to /twilio/stream with that streamSid,
|
|
271
|
+
* we know what to inject as session instructions.
|
|
272
|
+
*/
|
|
273
|
+
async function handleQueueOutbound(req, res, url) {
|
|
274
|
+
if (!checkTwimlAuth(req, res, url)) return;
|
|
275
|
+
const rawBody = await readBody(req);
|
|
276
|
+
let parsed;
|
|
277
|
+
try { parsed = JSON.parse(rawBody); }
|
|
278
|
+
catch { return json(res, 400, { ok: false, error: "invalid_json" }); }
|
|
279
|
+
|
|
280
|
+
const callSid = parsed.call_sid;
|
|
281
|
+
if (!callSid) return json(res, 400, { ok: false, error: "missing call_sid" });
|
|
282
|
+
queuedOutbound.set(callSid, { ...parsed, queued_at: Date.now() });
|
|
283
|
+
console.log(`[twilio-outbound] queued config for call_sid=${callSid} (${parsed.peer_name || parsed.peer || "?"})`);
|
|
284
|
+
return json(res, 200, { ok: true, call_sid: callSid });
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* GET/POST /twilio/twiml
|
|
289
|
+
* Returns TwiML that tells Twilio to <Connect><Stream> to our WSS endpoint.
|
|
290
|
+
* Called by Twilio when an outbound call is answered (we set this URL as
|
|
291
|
+
* the call's TwiML when invoking the Twilio Voice API).
|
|
292
|
+
*/
|
|
293
|
+
async function handleTwiml(req, res, url) {
|
|
294
|
+
if (!checkTwimlAuth(req, res, url)) return;
|
|
295
|
+
const host = req.headers["x-forwarded-host"] || req.headers.host;
|
|
296
|
+
const proto = (req.headers["x-forwarded-proto"] || "https") === "http" ? "ws" : "wss";
|
|
297
|
+
const streamUrl = `${proto}://${host}/twilio/stream${VOICE_TWIML_TOKEN ? `?token=${encodeURIComponent(VOICE_TWIML_TOKEN)}` : ""}`;
|
|
298
|
+
const twiml = `<?xml version="1.0" encoding="UTF-8"?>
|
|
299
|
+
<Response>
|
|
300
|
+
<Connect>
|
|
301
|
+
<Stream url="${streamUrl.replace(/&/g, "&")}" />
|
|
302
|
+
</Connect>
|
|
303
|
+
</Response>`;
|
|
304
|
+
console.log(`[twilio-twiml] served TwiML (req from ${req.socket.remoteAddress}, ua=${(req.headers["user-agent"] || "?").slice(0, 60)})`);
|
|
305
|
+
res.writeHead(200, { "content-type": "text/xml" });
|
|
306
|
+
res.end(twiml);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// ── Server ────────────────────────────────────────────────────────────────
|
|
310
|
+
|
|
311
|
+
const server = http.createServer(async (req, res) => {
|
|
312
|
+
try {
|
|
313
|
+
const url = new URL(req.url, `http://${req.headers.host || "localhost"}`);
|
|
314
|
+
const path = url.pathname;
|
|
315
|
+
if (req.method === "POST" && path === "/openai/realtime/incoming") return handleIncoming(req, res);
|
|
316
|
+
if (req.method === "GET" && path === "/openai/realtime/calls") return handlePoll(req, res);
|
|
317
|
+
if (req.method === "POST" && path === "/openai/realtime/test") return handleTest(req, res);
|
|
318
|
+
if (req.method === "POST" && path === "/twilio/queue-outbound") return handleQueueOutbound(req, res, url);
|
|
319
|
+
if ((req.method === "GET" || req.method === "POST") && path === "/twilio/twiml") return handleTwiml(req, res, url);
|
|
320
|
+
if (req.method === "GET" && (path === "/health" || path === "/")) return handleHealth(req, res);
|
|
321
|
+
return text(res, 404, "not found");
|
|
322
|
+
} catch (err) {
|
|
323
|
+
console.error("[error]", err);
|
|
324
|
+
return text(res, 500, "internal error");
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
// ── WebSocket: Twilio Media Streams ⇄ OpenAI Realtime ──────────────────────
|
|
329
|
+
// Twilio opens a WSS connection to /twilio/stream. We:
|
|
330
|
+
// 1. Wait for Twilio's `start` event (gives us the streamSid + callSid)
|
|
331
|
+
// 2. Look up the queued session config for that callSid
|
|
332
|
+
// 3. Open a WSS to OpenAI's realtime endpoint
|
|
333
|
+
// 4. session.update with input/output audio = g711_ulaw + the queued
|
|
334
|
+
// instructions / tools
|
|
335
|
+
// 5. Bridge audio frames bidirectionally (no transcoding — both sides
|
|
336
|
+
// speak g711_ulaw natively)
|
|
337
|
+
|
|
338
|
+
const wss = new WebSocketServer({ noServer: true });
|
|
339
|
+
|
|
340
|
+
server.on("upgrade", (req, socket, head) => {
|
|
341
|
+
const url = new URL(req.url, `http://${req.headers.host || "localhost"}`);
|
|
342
|
+
if (url.pathname !== "/twilio/stream") {
|
|
343
|
+
socket.destroy();
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
// Twilio Media Streams strips query params on WS upgrade, so we can't
|
|
347
|
+
// require ?token=. The endpoint is rate-limited by OpenAI's per-minute
|
|
348
|
+
// billing for any connection that does land here, and we log
|
|
349
|
+
// remoteAddress + user-agent so abuse is detectable. The TwiML
|
|
350
|
+
// endpoint that publishes the stream URL still requires
|
|
351
|
+
// VOICE_TWIML_TOKEN, so a malicious caller would need to know
|
|
352
|
+
// the WSS URL out-of-band to abuse this endpoint.
|
|
353
|
+
console.log(`[bridge] WS upgrade from ${req.socket.remoteAddress} ua=${(req.headers["user-agent"] || "?").slice(0, 60)} path=${url.pathname} qs=${url.search}`);
|
|
354
|
+
wss.handleUpgrade(req, socket, head, (ws) => {
|
|
355
|
+
handleTwilioStream(ws, req);
|
|
356
|
+
});
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
function handleTwilioStream(twilioWs, req) {
|
|
360
|
+
if (!OPENAI_API_KEY) {
|
|
361
|
+
console.warn("[bridge] cannot bridge — OPENAI_API_KEY not set");
|
|
362
|
+
twilioWs.close(1011, "openai key missing");
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
let openaiWs = null;
|
|
367
|
+
let streamSid = null;
|
|
368
|
+
let callSid = null;
|
|
369
|
+
let openaiReady = false;
|
|
370
|
+
let bufferedFrames = []; // Twilio frames received before OpenAI is ready
|
|
371
|
+
let responseInProgress = false; // tracks whether a response.create is mid-flight
|
|
372
|
+
|
|
373
|
+
console.log(`[bridge] Twilio Media Stream connected from ${req.socket.remoteAddress}`);
|
|
374
|
+
|
|
375
|
+
const cleanup = (reason) => {
|
|
376
|
+
if (openaiWs && openaiWs.readyState !== WebSocket.CLOSED) {
|
|
377
|
+
try { openaiWs.close(1000, reason); } catch {}
|
|
378
|
+
}
|
|
379
|
+
if (twilioWs && twilioWs.readyState !== WebSocket.CLOSED) {
|
|
380
|
+
try { twilioWs.close(1000, reason); } catch {}
|
|
381
|
+
}
|
|
382
|
+
if (streamSid) activeBridges.delete(streamSid);
|
|
383
|
+
console.log(`[bridge] closed (${reason}) stream=${streamSid || "?"} call=${callSid || "?"}`);
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
twilioWs.on("error", (err) => {
|
|
387
|
+
console.error(`[bridge] twilio ws error: ${err.message}`);
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
twilioWs.on("close", () => cleanup("twilio_closed"));
|
|
391
|
+
|
|
392
|
+
twilioWs.on("message", (raw) => {
|
|
393
|
+
let evt;
|
|
394
|
+
try { evt = JSON.parse(raw.toString("utf-8")); }
|
|
395
|
+
catch { return; }
|
|
396
|
+
|
|
397
|
+
if (evt.event === "start") {
|
|
398
|
+
streamSid = evt.start?.streamSid;
|
|
399
|
+
callSid = evt.start?.callSid;
|
|
400
|
+
activeBridges.set(streamSid, { callSid, started: Date.now() });
|
|
401
|
+
console.log(`[bridge] start stream=${streamSid} call=${callSid}`);
|
|
402
|
+
const queued = queuedOutbound.get(callSid) || {};
|
|
403
|
+
openOpenAIWs(queued);
|
|
404
|
+
} else if (evt.event === "media") {
|
|
405
|
+
// Twilio sends 20ms of µ-law @ 8kHz, base64-encoded.
|
|
406
|
+
// OpenAI accepts the same bytes when session.audio.input.format = g711_ulaw.
|
|
407
|
+
const payload = evt.media?.payload;
|
|
408
|
+
if (!payload) return;
|
|
409
|
+
if (openaiReady) {
|
|
410
|
+
openaiWs.send(JSON.stringify({ type: "input_audio_buffer.append", audio: payload }));
|
|
411
|
+
} else {
|
|
412
|
+
bufferedFrames.push(payload);
|
|
413
|
+
if (bufferedFrames.length > 1500) bufferedFrames.shift(); // ~30s cap
|
|
414
|
+
}
|
|
415
|
+
} else if (evt.event === "stop") {
|
|
416
|
+
console.log(`[bridge] twilio stop stream=${streamSid}`);
|
|
417
|
+
// Don't close OpenAI yet — let the model finish current response if any
|
|
418
|
+
setTimeout(() => cleanup("twilio_stop"), 1500);
|
|
419
|
+
} else if (evt.event === "mark") {
|
|
420
|
+
// Twilio playback marker (we don't use these but log for debugging)
|
|
421
|
+
}
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
function openOpenAIWs(queued) {
|
|
425
|
+
const model = queued.model || "gpt-realtime-2";
|
|
426
|
+
const url = `wss://api.openai.com/v1/realtime?model=${encodeURIComponent(model)}`;
|
|
427
|
+
openaiWs = new WebSocket(url, {
|
|
428
|
+
headers: { Authorization: `Bearer ${OPENAI_API_KEY}` },
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
openaiWs.on("open", () => {
|
|
432
|
+
console.log(`[bridge] openai ws open model=${model}`);
|
|
433
|
+
// Send session config — use g711_ulaw on both sides so we don't transcode.
|
|
434
|
+
const sessionUpdate = buildSessionUpdate(queued);
|
|
435
|
+
openaiWs.send(JSON.stringify(sessionUpdate));
|
|
436
|
+
openaiReady = true;
|
|
437
|
+
// Drain any frames Twilio sent before OpenAI was ready
|
|
438
|
+
for (const payload of bufferedFrames) {
|
|
439
|
+
openaiWs.send(JSON.stringify({ type: "input_audio_buffer.append", audio: payload }));
|
|
440
|
+
}
|
|
441
|
+
bufferedFrames = [];
|
|
442
|
+
// Nudge the model to greet first.
|
|
443
|
+
openaiWs.send(JSON.stringify({ type: "response.create" }));
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
openaiWs.on("error", (err) => {
|
|
447
|
+
console.error(`[bridge] openai ws error: ${err.message}`);
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
openaiWs.on("close", (code, reason) => {
|
|
451
|
+
console.log(`[bridge] openai ws closed code=${code} reason=${reason}`);
|
|
452
|
+
cleanup("openai_closed");
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
openaiWs.on("message", (raw) => {
|
|
456
|
+
let evt;
|
|
457
|
+
try { evt = JSON.parse(raw.toString("utf-8")); }
|
|
458
|
+
catch { return; }
|
|
459
|
+
|
|
460
|
+
// Forward the model's audio back to Twilio.
|
|
461
|
+
if (evt.type === "response.audio.delta" || evt.type === "response.output_audio.delta") {
|
|
462
|
+
if (twilioWs.readyState === WebSocket.OPEN && streamSid && evt.delta) {
|
|
463
|
+
twilioWs.send(JSON.stringify({
|
|
464
|
+
event: "media",
|
|
465
|
+
streamSid,
|
|
466
|
+
media: { payload: evt.delta },
|
|
467
|
+
}));
|
|
468
|
+
}
|
|
469
|
+
} else if (evt.type === "input_audio_buffer.speech_started") {
|
|
470
|
+
// User started speaking → barge-in: tell Twilio to clear playback buffer.
|
|
471
|
+
// Only send response.cancel if a response is ACTUALLY running, otherwise
|
|
472
|
+
// OpenAI rejects with "response_cancel_not_active" (harmless but noisy).
|
|
473
|
+
if (twilioWs.readyState === WebSocket.OPEN && streamSid) {
|
|
474
|
+
twilioWs.send(JSON.stringify({ event: "clear", streamSid }));
|
|
475
|
+
}
|
|
476
|
+
if (responseInProgress) {
|
|
477
|
+
openaiWs.send(JSON.stringify({ type: "response.cancel" }));
|
|
478
|
+
responseInProgress = false;
|
|
479
|
+
}
|
|
480
|
+
} else if (evt.type === "response.created") {
|
|
481
|
+
responseInProgress = true;
|
|
482
|
+
} else if (evt.type === "error") {
|
|
483
|
+
console.error(`[bridge] openai error event: ${JSON.stringify(evt).slice(0, 300)}`);
|
|
484
|
+
} else if (evt.type === "session.created" || evt.type === "session.updated") {
|
|
485
|
+
// Quiet — no action needed
|
|
486
|
+
} else if (evt.type === "response.done") {
|
|
487
|
+
responseInProgress = false;
|
|
488
|
+
}
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
function buildSessionUpdate(queued) {
|
|
494
|
+
const voice = queued.voice || "cedar";
|
|
495
|
+
const identity = queued.identity || {};
|
|
496
|
+
// PREFER `instructions` (the full pre-loaded contextual prompt the
|
|
497
|
+
// local agent shipped — includes identity, memory, RAG, peer info,
|
|
498
|
+
// voice-mode rules) over the bare `brief`. The `brief` describes
|
|
499
|
+
// the task; `instructions` describes the whole agent.
|
|
500
|
+
const instructions = queued.instructions || queued.brief ||
|
|
501
|
+
`You are ${identity.name || "the agent"}, ${identity.role || "an executive operator"} at ${identity.company || "Northwind"}. Reply briefly in your voice.`;
|
|
502
|
+
return {
|
|
503
|
+
type: "session.update",
|
|
504
|
+
session: {
|
|
505
|
+
type: "realtime",
|
|
506
|
+
output_modalities: ["audio"],
|
|
507
|
+
instructions,
|
|
508
|
+
audio: {
|
|
509
|
+
input: {
|
|
510
|
+
format: { type: "audio/pcmu" }, // g711_ulaw — matches Twilio
|
|
511
|
+
turn_detection: {
|
|
512
|
+
type: "server_vad",
|
|
513
|
+
threshold: 0.5,
|
|
514
|
+
prefix_padding_ms: 300,
|
|
515
|
+
silence_duration_ms: 500,
|
|
516
|
+
},
|
|
517
|
+
},
|
|
518
|
+
output: {
|
|
519
|
+
format: { type: "audio/pcmu" },
|
|
520
|
+
voice,
|
|
521
|
+
},
|
|
522
|
+
},
|
|
523
|
+
tools: queued.tools || [],
|
|
524
|
+
tool_choice: queued.tool_choice || "auto",
|
|
525
|
+
},
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
server.listen(PORT, () => {
|
|
530
|
+
console.log(`[start] openai-voice-cloud-relay listening on :${PORT}`);
|
|
531
|
+
console.log(`[start] sig=${!!OPENAI_WEBHOOK_SECRET} poll_auth=${!!POLL_TOKEN} openai_key=${!!OPENAI_API_KEY} twiml_auth=${!!VOICE_TWIML_TOKEN}`);
|
|
532
|
+
});
|