@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,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); });
|