@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,542 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Maestro — Outbound voice message dispatcher.
|
|
3
|
+
*
|
|
4
|
+
* Delivers a synthesized voice message to a peer over the right transport:
|
|
5
|
+
*
|
|
6
|
+
* WhatsApp (E.164 phone via Baileys) ── inline voice note (PTT, opus)
|
|
7
|
+
* Slack (channel / DM ID) ── audio file upload to channel
|
|
8
|
+
* PSTN (E.164 phone via Twilio) ── one-shot Twilio <Play> call
|
|
9
|
+
* Live conversational PSTN ── Twilio outbound + OpenAI SIP bridge
|
|
10
|
+
*
|
|
11
|
+
* The agent doesn't need to know which transport will be picked: the
|
|
12
|
+
* `to` field is parsed and we route accordingly. Operators can force a
|
|
13
|
+
* specific transport via the `transport` option.
|
|
14
|
+
*
|
|
15
|
+
* Public API:
|
|
16
|
+
* sendVoiceMessage(opts) → { transport, message_id, duration_ms, cost_usd }
|
|
17
|
+
* placeOutboundCall(opts) → { call_id, twilio_call_sid, status, context_delivered } (Stage 2)
|
|
18
|
+
*
|
|
19
|
+
* Cost notes:
|
|
20
|
+
* - TTS: ~$0.015/1k chars (gpt-4o-mini-tts)
|
|
21
|
+
* - WhatsApp Baileys send: free (your own WA Web session)
|
|
22
|
+
* - Slack upload: free
|
|
23
|
+
* - Twilio outbound: ~$0.013-0.014/min for US numbers, varies internationally
|
|
24
|
+
* - OpenAI Realtime via SIP: $0.40-0.80/min
|
|
25
|
+
*
|
|
26
|
+
* Operator-level safety: this module honours policies/action-classification.yaml
|
|
27
|
+
* indirectly — it's expected to be called via lib/voice/tool-bridge.mjs or
|
|
28
|
+
* via the daemon's action-executor, both of which already gate by risk.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import { writeFileSync, mkdirSync, existsSync, readFileSync } from "node:fs";
|
|
32
|
+
import { join, resolve } from "node:path";
|
|
33
|
+
import { tmpdir } from "node:os";
|
|
34
|
+
import { synthesizeSpeech, pickDefaultVoice, ttsCostEstimate } from "./tts.mjs";
|
|
35
|
+
|
|
36
|
+
const TWILIO_API_BASE = "https://api.twilio.com/2010-04-01";
|
|
37
|
+
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
// Transport detection
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
|
|
42
|
+
const E164_REGEX = /^\+[1-9]\d{6,14}$/;
|
|
43
|
+
const SLACK_CHANNEL_REGEX = /^[CDG][A-Z0-9]{8,}$/; // C/D/G prefix: channel/DM/group
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Decide which transport to use for an outbound voice message.
|
|
47
|
+
*
|
|
48
|
+
* Order of preference when only `to` is given:
|
|
49
|
+
* 1. Slack channel/DM id (C/D/G prefix) → slack
|
|
50
|
+
* 2. WhatsApp JID ("...@s.whatsapp.net") or
|
|
51
|
+
* E.164 phone when WhatsApp is configured → whatsapp
|
|
52
|
+
* 3. E.164 phone otherwise → pstn (Twilio)
|
|
53
|
+
*/
|
|
54
|
+
export function pickTransport(opts) {
|
|
55
|
+
if (opts.transport) return opts.transport;
|
|
56
|
+
const to = String(opts.to || "").trim();
|
|
57
|
+
if (!to) throw new Error("pickTransport: to required");
|
|
58
|
+
|
|
59
|
+
if (to.includes("@s.whatsapp.net") || to.includes("@g.us")) return "whatsapp";
|
|
60
|
+
if (SLACK_CHANNEL_REGEX.test(to)) return "slack";
|
|
61
|
+
if (E164_REGEX.test(to)) {
|
|
62
|
+
// Prefer WhatsApp if the agent has the Baileys auth state populated;
|
|
63
|
+
// operators can override per-call by passing transport: "pstn".
|
|
64
|
+
if (opts.agentRoot && existsSync(join(opts.agentRoot, "state/whatsapp/auth/creds.json"))) {
|
|
65
|
+
return "whatsapp";
|
|
66
|
+
}
|
|
67
|
+
return "pstn";
|
|
68
|
+
}
|
|
69
|
+
throw new Error(`pickTransport: cannot infer transport for "${to}" (use transport: option to force)`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
// sendVoiceMessage — top-level entry point
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* @typedef {object} VoiceMessageOpts
|
|
78
|
+
* @property {string} to Destination (E.164, Slack id, or WA jid)
|
|
79
|
+
* @property {string} text Spoken content (max 4096 chars)
|
|
80
|
+
* @property {string} [voice] TTS voice (default: pickDefaultVoice from agent config)
|
|
81
|
+
* @property {string} [transport] Force "whatsapp" | "slack" | "pstn" (defaults: auto)
|
|
82
|
+
* @property {string} [agentRoot] Agent repo root (default: $AGENT_ROOT)
|
|
83
|
+
* @property {string} [model] TTS model (default: gpt-4o-mini-tts)
|
|
84
|
+
* @property {string} [format] Audio format (default: per-transport)
|
|
85
|
+
* @property {object} [meta] Transport-specific metadata (slack channel id, etc.)
|
|
86
|
+
*/
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* @param {VoiceMessageOpts} opts
|
|
90
|
+
* @returns {Promise<{transport, message_id, audio_bytes, cost_usd}>}
|
|
91
|
+
*/
|
|
92
|
+
export async function sendVoiceMessage(opts = {}) {
|
|
93
|
+
const agentRoot = resolve(opts.agentRoot || process.env.AGENT_ROOT || process.env.AGENT_DIR || process.cwd());
|
|
94
|
+
const voiceConfig = loadAgentVoiceConfig(agentRoot);
|
|
95
|
+
const transport = pickTransport({ ...opts, agentRoot });
|
|
96
|
+
|
|
97
|
+
// Per-transport defaults: WhatsApp wants opus PTT, Slack/PSTN are fine with mp3.
|
|
98
|
+
const fmt = opts.format || (transport === "whatsapp" ? "opus" : "mp3");
|
|
99
|
+
const voice = opts.voice || pickDefaultVoice(voiceConfig);
|
|
100
|
+
|
|
101
|
+
const audio = await synthesizeSpeech({
|
|
102
|
+
text: opts.text,
|
|
103
|
+
voice,
|
|
104
|
+
format: fmt,
|
|
105
|
+
model: opts.model,
|
|
106
|
+
});
|
|
107
|
+
const costUsd = ttsCostEstimate(opts.text, opts.model);
|
|
108
|
+
|
|
109
|
+
switch (transport) {
|
|
110
|
+
case "whatsapp": {
|
|
111
|
+
const result = await deliverViaWhatsApp({ to: opts.to, audio, fmt, agentRoot, text: opts.text });
|
|
112
|
+
return { transport, ...result, cost_usd: costUsd };
|
|
113
|
+
}
|
|
114
|
+
case "slack": {
|
|
115
|
+
const result = await deliverViaSlack({ to: opts.to, audio, fmt, agentRoot, text: opts.text, meta: opts.meta });
|
|
116
|
+
return { transport, ...result, cost_usd: costUsd };
|
|
117
|
+
}
|
|
118
|
+
case "pstn": {
|
|
119
|
+
const result = await deliverViaPSTN({ to: opts.to, audio, fmt, agentRoot, text: opts.text });
|
|
120
|
+
return { transport, ...result, cost_usd: costUsd };
|
|
121
|
+
}
|
|
122
|
+
default:
|
|
123
|
+
throw new Error(`sendVoiceMessage: unknown transport "${transport}"`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ---------------------------------------------------------------------------
|
|
128
|
+
// WhatsApp (Baileys) — sends an inline voice note (PTT)
|
|
129
|
+
// ---------------------------------------------------------------------------
|
|
130
|
+
|
|
131
|
+
async function deliverViaWhatsApp({ to, audio, fmt, agentRoot, text }) {
|
|
132
|
+
// Reuse the channel-bus registry — the daemon already keeps a Baileys
|
|
133
|
+
// session alive there. We import lazily so this module doesn't pull
|
|
134
|
+
// baileys for non-WA use cases.
|
|
135
|
+
const channelsMod = await import("../channels/index.mjs");
|
|
136
|
+
let channel = channelsMod.getChannel?.("whatsapp");
|
|
137
|
+
if (!channel) {
|
|
138
|
+
throw new Error(
|
|
139
|
+
"WhatsApp channel not registered. Either rename config/whatsapp.yaml.disabled → .yaml " +
|
|
140
|
+
"and restart the daemon, or pass transport: 'pstn' for Twilio."
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
if (typeof channel.send !== "function") {
|
|
144
|
+
throw new Error("WhatsApp channel has no send() method");
|
|
145
|
+
}
|
|
146
|
+
// Cache the audio to disk for replay/audit.
|
|
147
|
+
const outboundDir = join(agentRoot, "state/whatsapp/outbound");
|
|
148
|
+
mkdirSync(outboundDir, { recursive: true });
|
|
149
|
+
const filename = `${Date.now()}-voice-${slugify(text).slice(0, 24)}.${fmt}`;
|
|
150
|
+
writeFileSync(join(outboundDir, filename), audio);
|
|
151
|
+
|
|
152
|
+
const result = await channel.send({
|
|
153
|
+
to,
|
|
154
|
+
text: text, // human-readable fallback if the recipient's client can't play audio
|
|
155
|
+
attachments: [
|
|
156
|
+
{
|
|
157
|
+
type: "voice_note",
|
|
158
|
+
mime: fmt === "opus" ? "audio/ogg; codecs=opus" : "audio/mpeg",
|
|
159
|
+
buffer: audio,
|
|
160
|
+
},
|
|
161
|
+
],
|
|
162
|
+
});
|
|
163
|
+
return {
|
|
164
|
+
message_id: result?.message_id || filename,
|
|
165
|
+
audio_bytes: audio.length,
|
|
166
|
+
cached_at: join(outboundDir, filename),
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// ---------------------------------------------------------------------------
|
|
171
|
+
// Slack — uploads the audio as a file attachment
|
|
172
|
+
// ---------------------------------------------------------------------------
|
|
173
|
+
|
|
174
|
+
async function deliverViaSlack({ to, audio, fmt, agentRoot, text: textArg, meta }) {
|
|
175
|
+
// Coerce text defensively: callers may pass null/undefined/non-string and we
|
|
176
|
+
// immediately .slice() it for the filename / title / initial_comment below.
|
|
177
|
+
const text = String(textArg || "");
|
|
178
|
+
const token = process.env.SLACK_USER_TOKEN || process.env.SLACK_BOT_TOKEN || process.env.SLACK_TOKEN;
|
|
179
|
+
if (!token) throw new Error("Slack delivery requires SLACK_USER_TOKEN or SLACK_BOT_TOKEN");
|
|
180
|
+
|
|
181
|
+
const ext = fmt === "opus" ? "ogg" : fmt;
|
|
182
|
+
const filename = `voice-${slugify(text).slice(0, 32)}.${ext}`;
|
|
183
|
+
|
|
184
|
+
// Slack's modern flow: files.getUploadURLExternal → PUT audio → files.completeUploadExternal
|
|
185
|
+
// We use it because files.upload was deprecated in 2024.
|
|
186
|
+
const getUrl = `https://slack.com/api/files.getUploadURLExternal?length=${audio.length}&filename=${encodeURIComponent(filename)}`;
|
|
187
|
+
const getResp = await (await fetch(getUrl, {
|
|
188
|
+
method: "POST",
|
|
189
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
190
|
+
})).json();
|
|
191
|
+
if (!getResp.ok) throw new Error(`slack getUploadURL failed: ${JSON.stringify(getResp)}`);
|
|
192
|
+
|
|
193
|
+
const putResp = await fetch(getResp.upload_url, {
|
|
194
|
+
method: "POST",
|
|
195
|
+
headers: { "Content-Type": fmt === "opus" ? "audio/ogg" : "audio/mpeg" },
|
|
196
|
+
body: audio,
|
|
197
|
+
});
|
|
198
|
+
if (!putResp.ok) throw new Error(`slack file upload failed: HTTP ${putResp.status}`);
|
|
199
|
+
|
|
200
|
+
const completeBody = {
|
|
201
|
+
files: [{ id: getResp.file_id, title: text.slice(0, 80) }],
|
|
202
|
+
channel_id: to,
|
|
203
|
+
initial_comment: text,
|
|
204
|
+
};
|
|
205
|
+
if (meta?.thread_ref) completeBody.thread_ts = meta.thread_ref;
|
|
206
|
+
|
|
207
|
+
const completeResp = await (await fetch("https://slack.com/api/files.completeUploadExternal", {
|
|
208
|
+
method: "POST",
|
|
209
|
+
headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
|
|
210
|
+
body: JSON.stringify(completeBody),
|
|
211
|
+
})).json();
|
|
212
|
+
if (!completeResp.ok) throw new Error(`slack files.completeUploadExternal: ${JSON.stringify(completeResp)}`);
|
|
213
|
+
|
|
214
|
+
return {
|
|
215
|
+
message_id: getResp.file_id,
|
|
216
|
+
audio_bytes: audio.length,
|
|
217
|
+
file_url: completeResp.files?.[0]?.permalink || null,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// ---------------------------------------------------------------------------
|
|
222
|
+
// PSTN (Twilio) — one-shot outbound call that plays the synthesized audio
|
|
223
|
+
// ---------------------------------------------------------------------------
|
|
224
|
+
|
|
225
|
+
async function deliverViaPSTN({ to, audio, fmt, agentRoot, text: textArg }) {
|
|
226
|
+
// Coerce text defensively: it is fed to slugify()/escapeXml() below, both of
|
|
227
|
+
// which expect a string (escapeXml in particular does text.replace()).
|
|
228
|
+
const text = String(textArg || "");
|
|
229
|
+
const sid = process.env.TWILIO_ACCOUNT_SID;
|
|
230
|
+
const token = process.env.TWILIO_AUTH_TOKEN;
|
|
231
|
+
const fromNumber = process.env.TWILIO_FROM_NUMBER || process.env.TWILIO_VOICE_FROM;
|
|
232
|
+
if (!sid || !token) throw new Error("PSTN delivery requires TWILIO_ACCOUNT_SID + TWILIO_AUTH_TOKEN");
|
|
233
|
+
if (!fromNumber) throw new Error("PSTN delivery requires TWILIO_FROM_NUMBER or TWILIO_VOICE_FROM");
|
|
234
|
+
|
|
235
|
+
// For Twilio <Play>, the audio must be reachable via a public HTTPS URL.
|
|
236
|
+
// We host it ourselves at the voice-webhook server's /voice/audio/<id>
|
|
237
|
+
// endpoint, which the voice-webhook server should serve. For now we
|
|
238
|
+
// fall back to Twilio's <Say> TwiML so the call works without a tunnel
|
|
239
|
+
// — the user gets the message spoken by Twilio's TTS instead of our
|
|
240
|
+
// higher-quality OpenAI TTS. Operators with a public webhook URL can
|
|
241
|
+
// configure VOICE_PUBLIC_BASE_URL to use <Play> instead.
|
|
242
|
+
const publicBase = process.env.VOICE_PUBLIC_BASE_URL;
|
|
243
|
+
let twiml;
|
|
244
|
+
if (publicBase) {
|
|
245
|
+
// Cache the audio and reference it via <Play>.
|
|
246
|
+
const cacheDir = join(agentRoot, "state/voice/outbound-audio");
|
|
247
|
+
mkdirSync(cacheDir, { recursive: true });
|
|
248
|
+
const audioId = `${Date.now()}-${slugify(text).slice(0, 16)}`;
|
|
249
|
+
const filename = `${audioId}.${fmt === "opus" ? "ogg" : fmt}`;
|
|
250
|
+
writeFileSync(join(cacheDir, filename), audio);
|
|
251
|
+
const audioUrl = `${publicBase.replace(/\/$/, "")}/voice/audio/${filename}`;
|
|
252
|
+
twiml = `<?xml version="1.0" encoding="UTF-8"?><Response><Play>${audioUrl}</Play></Response>`;
|
|
253
|
+
} else {
|
|
254
|
+
// Fall back to Twilio Say (Twilio synthesizes from text directly).
|
|
255
|
+
const safe = escapeXml(text);
|
|
256
|
+
twiml = `<?xml version="1.0" encoding="UTF-8"?><Response><Say voice="Polly.Joanna">${safe}</Say></Response>`;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const auth = Buffer.from(`${sid}:${token}`).toString("base64");
|
|
260
|
+
const params = new URLSearchParams({ To: to, From: fromNumber, Twiml: twiml });
|
|
261
|
+
const r = await fetch(`${TWILIO_API_BASE}/Accounts/${sid}/Calls.json`, {
|
|
262
|
+
method: "POST",
|
|
263
|
+
headers: {
|
|
264
|
+
Authorization: `Basic ${auth}`,
|
|
265
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
266
|
+
},
|
|
267
|
+
body: params.toString(),
|
|
268
|
+
});
|
|
269
|
+
const data = await r.json();
|
|
270
|
+
if (!r.ok) throw new Error(`twilio call failed: ${JSON.stringify(data)}`);
|
|
271
|
+
return {
|
|
272
|
+
message_id: data.sid,
|
|
273
|
+
audio_bytes: audio.length,
|
|
274
|
+
twilio_status: data.status,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// ---------------------------------------------------------------------------
|
|
279
|
+
// Stage 2 — placeOutboundCall: live conversational PSTN via SIP bridge
|
|
280
|
+
// ---------------------------------------------------------------------------
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Place an outbound PSTN call and bridge it to OpenAI Realtime for a
|
|
284
|
+
* live conversation. The agent's "brief" is loaded as the session
|
|
285
|
+
* instructions so the model knows what to say.
|
|
286
|
+
*
|
|
287
|
+
* Two transports (auto-selected based on env config):
|
|
288
|
+
*
|
|
289
|
+
* 1. **media_streams** (DEFAULT, recommended) — The Twilio call uses
|
|
290
|
+
* TwiML <Connect><Stream> to open a WebSocket to our Railway voice
|
|
291
|
+
* cloud-relay. The relay opens a parallel WebSocket to OpenAI's
|
|
292
|
+
* Realtime API and bridges audio bidirectionally (g711_ulaw on
|
|
293
|
+
* both sides — no transcoding). Works without OpenAI dashboard
|
|
294
|
+
* SIP config and without OpenAI accepting inbound INVITES from
|
|
295
|
+
* Twilio's outbound infrastructure.
|
|
296
|
+
* Requires: VOICE_RELAY_URL, VOICE_TWIML_TOKEN, OPENAI_API_KEY (on Railway).
|
|
297
|
+
*
|
|
298
|
+
* 2. **sip** (legacy / experimental) — TwiML <Dial><Sip>OpenAI</Sip>.
|
|
299
|
+
* Requires OpenAI's project to accept inbound SIP INVITES from
|
|
300
|
+
* arbitrary Twilio outbound — which empirically does not work
|
|
301
|
+
* today (Twilio's outbound SIP gets rejected by OpenAI for missing
|
|
302
|
+
* auth/registration). Kept for future when OpenAI documents the
|
|
303
|
+
* auth scheme.
|
|
304
|
+
* Requires: OPENAI_SIP_URI or TWILIO_SIP_TRUNK_DOMAIN.
|
|
305
|
+
*
|
|
306
|
+
* @param {{ to: string, brief: string, agentRoot?: string, voice?: string,
|
|
307
|
+
* transport?: "media_streams"|"sip", cancelOnContextFail?: boolean }} opts
|
|
308
|
+
* cancelOnContextFail: when true, hard-cancel the dialed call if the brief
|
|
309
|
+
* could not be pre-registered with the relay (default false — call proceeds
|
|
310
|
+
* with the relay's fallback prompt, and context_delivered:false is returned).
|
|
311
|
+
* @returns {Promise<{ call_id, twilio_call_sid, status, transport,
|
|
312
|
+
* context_delivered: boolean }>}
|
|
313
|
+
* context_delivered is false when a media_streams brief failed to register
|
|
314
|
+
* with the relay; true otherwise (including sip / no-relay paths that do not
|
|
315
|
+
* pre-register context).
|
|
316
|
+
*/
|
|
317
|
+
export async function placeOutboundCall(opts = {}) {
|
|
318
|
+
const sid = process.env.TWILIO_ACCOUNT_SID;
|
|
319
|
+
const token = process.env.TWILIO_AUTH_TOKEN;
|
|
320
|
+
const fromNumber = process.env.TWILIO_FROM_NUMBER || process.env.TWILIO_VOICE_FROM;
|
|
321
|
+
if (!sid || !token || !fromNumber) {
|
|
322
|
+
throw new Error("placeOutboundCall requires TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN, TWILIO_FROM_NUMBER");
|
|
323
|
+
}
|
|
324
|
+
const agentRoot = resolve(opts.agentRoot || process.env.AGENT_ROOT || process.env.AGENT_DIR || process.cwd());
|
|
325
|
+
|
|
326
|
+
// Pick transport. Default to media_streams when relay is configured
|
|
327
|
+
// (it's the only one that empirically works today). Fall back to SIP
|
|
328
|
+
// only when explicitly requested.
|
|
329
|
+
const relayUrl = (process.env.VOICE_RELAY_URL || "").replace(/\/$/, "");
|
|
330
|
+
const twimlToken = process.env.VOICE_TWIML_TOKEN || "";
|
|
331
|
+
const transport = opts.transport || (relayUrl ? "media_streams" : "sip");
|
|
332
|
+
|
|
333
|
+
const callId = `out-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
334
|
+
|
|
335
|
+
// Persist brief locally (always — for audit + later post-call processing)
|
|
336
|
+
const briefsDir = join(agentRoot, "state/voice/outbound-briefs");
|
|
337
|
+
mkdirSync(briefsDir, { recursive: true });
|
|
338
|
+
writeFileSync(join(briefsDir, `${callId}.json`), JSON.stringify({
|
|
339
|
+
call_id: callId,
|
|
340
|
+
to: opts.to,
|
|
341
|
+
brief: opts.brief,
|
|
342
|
+
voice: opts.voice || "cedar",
|
|
343
|
+
transport,
|
|
344
|
+
created_at: new Date().toISOString(),
|
|
345
|
+
}, null, 2));
|
|
346
|
+
|
|
347
|
+
// For media_streams, we use URL-based TwiML — Twilio fetches the TwiML
|
|
348
|
+
// from /twilio/twiml when the call connects, instead of us passing it
|
|
349
|
+
// inline. This gives us a server-side log of every TwiML request
|
|
350
|
+
// (proving Twilio is actually reaching Railway) and avoids any
|
|
351
|
+
// inline-TwiML parsing quirks.
|
|
352
|
+
let twiml = null;
|
|
353
|
+
let twimlUrl = null;
|
|
354
|
+
if (transport === "media_streams") {
|
|
355
|
+
if (!relayUrl) {
|
|
356
|
+
throw new Error("placeOutboundCall(media_streams) requires VOICE_RELAY_URL");
|
|
357
|
+
}
|
|
358
|
+
const tokenQp = twimlToken ? `?token=${encodeURIComponent(twimlToken)}` : "";
|
|
359
|
+
twimlUrl = `${relayUrl}/twilio/twiml${tokenQp}`;
|
|
360
|
+
} else if (transport === "sip") {
|
|
361
|
+
const sipTarget = process.env.OPENAI_SIP_URI || process.env.TWILIO_SIP_TRUNK_DOMAIN;
|
|
362
|
+
if (!sipTarget) {
|
|
363
|
+
throw new Error("placeOutboundCall(sip) requires OPENAI_SIP_URI or TWILIO_SIP_TRUNK_DOMAIN");
|
|
364
|
+
}
|
|
365
|
+
const baseUri = sipTarget.startsWith("sip:") ? sipTarget : `sip:${sipTarget}`;
|
|
366
|
+
const sipUri = `${baseUri}?X-Maestro-Call-Id=${encodeURIComponent(callId)}`;
|
|
367
|
+
twiml = `<?xml version="1.0" encoding="UTF-8"?><Response><Dial><Sip>${escapeXml(sipUri)}</Sip></Dial></Response>`;
|
|
368
|
+
} else {
|
|
369
|
+
throw new Error(`unknown transport: ${transport}`);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Initiate the call via Twilio. Use Url= (Twilio fetches TwiML from
|
|
373
|
+
// our endpoint) for media_streams so we get a server-side log of every
|
|
374
|
+
// TwiML request; use Twiml= (inline) for SIP since there's no benefit
|
|
375
|
+
// to a round-trip there.
|
|
376
|
+
const auth = Buffer.from(`${sid}:${token}`).toString("base64");
|
|
377
|
+
const params = new URLSearchParams({ To: opts.to, From: fromNumber });
|
|
378
|
+
if (twimlUrl) {
|
|
379
|
+
params.set("Url", twimlUrl);
|
|
380
|
+
params.set("Method", "GET");
|
|
381
|
+
} else {
|
|
382
|
+
params.set("Twiml", twiml);
|
|
383
|
+
}
|
|
384
|
+
const r = await fetch(`${TWILIO_API_BASE}/Accounts/${sid}/Calls.json`, {
|
|
385
|
+
method: "POST",
|
|
386
|
+
headers: { Authorization: `Basic ${auth}`, "Content-Type": "application/x-www-form-urlencoded" },
|
|
387
|
+
body: params.toString(),
|
|
388
|
+
});
|
|
389
|
+
const data = await r.json();
|
|
390
|
+
if (!r.ok) throw new Error(`twilio outbound failed: ${JSON.stringify(data)}`);
|
|
391
|
+
|
|
392
|
+
// For media_streams transport: pre-register this call's session config
|
|
393
|
+
// with the relay so it's ready when Twilio connects.
|
|
394
|
+
//
|
|
395
|
+
// Critical: we build the FULL contextual system prompt locally (agent
|
|
396
|
+
// identity from config/agent.json, top queue items, recent decisions,
|
|
397
|
+
// RAG hits, peer history, voice-mode rules) and ship it to Railway.
|
|
398
|
+
// The relay only injects strings into session.update — it has no
|
|
399
|
+
// access to ravi-ai's filesystem, so all context must come from here.
|
|
400
|
+
// Whether the call's context (system prompt / identity) successfully
|
|
401
|
+
// pre-registered with the relay. For transports that don't pre-register
|
|
402
|
+
// (sip, or media_streams with no relay) there is nothing to deliver, so the
|
|
403
|
+
// call is not context-dependent and this stays true. For media_streams with
|
|
404
|
+
// a relay it reflects the real outcome of the queue-outbound POST so callers
|
|
405
|
+
// can decide whether to keep the dialed call or hang up.
|
|
406
|
+
let contextDelivered = true;
|
|
407
|
+
if (transport === "media_streams" && relayUrl) {
|
|
408
|
+
contextDelivered = false;
|
|
409
|
+
let instructions = null;
|
|
410
|
+
try {
|
|
411
|
+
const ctxLoader = await import("./context-loader.mjs");
|
|
412
|
+
instructions = await ctxLoader.buildContextualInstructions({
|
|
413
|
+
agentRoot,
|
|
414
|
+
peer: opts.to,
|
|
415
|
+
callKind: "outbound",
|
|
416
|
+
brief: opts.brief,
|
|
417
|
+
});
|
|
418
|
+
} catch (err) {
|
|
419
|
+
// Fallback: hand-crafted minimal prompt that at least disambiguates
|
|
420
|
+
// who's the agent vs the caller. Better than nothing.
|
|
421
|
+
const id = opts.identity || {};
|
|
422
|
+
instructions = [
|
|
423
|
+
`# Identity`,
|
|
424
|
+
`You are ${id.fullName || id.name || "the agent"}, ${id.role || "an executive operator"} at ${id.company || "the company"}.`,
|
|
425
|
+
``,
|
|
426
|
+
`# Your task on this call`,
|
|
427
|
+
`${opts.brief}`,
|
|
428
|
+
``,
|
|
429
|
+
`# Caller`,
|
|
430
|
+
`You are calling ${opts.peer_name || opts.to}. They picked up. Greet them by name (NOT by your own name) and proceed with the task above.`,
|
|
431
|
+
``,
|
|
432
|
+
`# Voice mode rules`,
|
|
433
|
+
`- Speak briefly: 1-2 sentences before pausing.`,
|
|
434
|
+
`- Confirm the listener can hear you on first turn.`,
|
|
435
|
+
`- If asked to invoke a high-risk action, read back the parameters and get verbal confirmation in the same turn before doing it.`,
|
|
436
|
+
].join("\n");
|
|
437
|
+
console.warn(`[outbound] context-loader failed (${err.message}) — using fallback prompt`);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
try {
|
|
441
|
+
const queueResp = await fetch(`${relayUrl}/twilio/queue-outbound`, {
|
|
442
|
+
method: "POST",
|
|
443
|
+
headers: {
|
|
444
|
+
"Content-Type": "application/json",
|
|
445
|
+
...(twimlToken ? { Authorization: `Bearer ${twimlToken}` } : {}),
|
|
446
|
+
},
|
|
447
|
+
body: JSON.stringify({
|
|
448
|
+
call_sid: data.sid,
|
|
449
|
+
call_id: callId,
|
|
450
|
+
peer: opts.to,
|
|
451
|
+
peer_name: opts.peer_name || opts.to,
|
|
452
|
+
brief: opts.brief,
|
|
453
|
+
// Full pre-loaded system prompt (identity + memory + RAG + rules)
|
|
454
|
+
instructions,
|
|
455
|
+
voice: opts.voice || "cedar",
|
|
456
|
+
model: opts.model || "gpt-realtime-2",
|
|
457
|
+
identity: opts.identity || null,
|
|
458
|
+
tools: opts.tools || [],
|
|
459
|
+
}),
|
|
460
|
+
});
|
|
461
|
+
if (queueResp.ok) {
|
|
462
|
+
contextDelivered = true;
|
|
463
|
+
} else {
|
|
464
|
+
const errText = await queueResp.text();
|
|
465
|
+
console.warn(`[outbound] failed to queue brief at relay: HTTP ${queueResp.status} ${errText.slice(0, 200)}`);
|
|
466
|
+
}
|
|
467
|
+
} catch (err) {
|
|
468
|
+
console.warn(`[outbound] queue-outbound failed: ${err.message}`);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// The call is already dialing at this point. If the brief could not be
|
|
472
|
+
// pre-registered the relay will fall back to its own default prompt, so the
|
|
473
|
+
// model would speak without this agent's identity/brief. Operators can opt
|
|
474
|
+
// in to hard-cancelling the dialed call in that case via cancelOnContextFail
|
|
475
|
+
// (default off — preserves the existing "let the call proceed" behaviour).
|
|
476
|
+
if (!contextDelivered && opts.cancelOnContextFail) {
|
|
477
|
+
await cancelTwilioCall({ sid, token, callSid: data.sid }).catch((err) => {
|
|
478
|
+
console.warn(`[outbound] failed to cancel call after context-delivery failure: ${err.message}`);
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
return {
|
|
484
|
+
call_id: callId,
|
|
485
|
+
twilio_call_sid: data.sid,
|
|
486
|
+
status: data.status,
|
|
487
|
+
transport,
|
|
488
|
+
context_delivered: contextDelivered,
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* Best-effort cancel of an in-flight Twilio call (sets Status=completed).
|
|
494
|
+
* Used when a media_streams brief could not be pre-registered and the operator
|
|
495
|
+
* asked us not to let a context-less call proceed.
|
|
496
|
+
*
|
|
497
|
+
* @param {{ sid: string, token: string, callSid: string }} args
|
|
498
|
+
*/
|
|
499
|
+
async function cancelTwilioCall({ sid, token, callSid }) {
|
|
500
|
+
const auth = Buffer.from(`${sid}:${token}`).toString("base64");
|
|
501
|
+
const r = await fetch(`${TWILIO_API_BASE}/Accounts/${sid}/Calls/${encodeURIComponent(callSid)}.json`, {
|
|
502
|
+
method: "POST",
|
|
503
|
+
headers: { Authorization: `Basic ${auth}`, "Content-Type": "application/x-www-form-urlencoded" },
|
|
504
|
+
body: new URLSearchParams({ Status: "completed" }).toString(),
|
|
505
|
+
});
|
|
506
|
+
if (!r.ok) {
|
|
507
|
+
const t = await r.text();
|
|
508
|
+
throw new Error(`twilio cancel failed: HTTP ${r.status} ${t.slice(0, 200)}`);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// ---------------------------------------------------------------------------
|
|
513
|
+
// Helpers
|
|
514
|
+
// ---------------------------------------------------------------------------
|
|
515
|
+
|
|
516
|
+
function loadAgentVoiceConfig(agentRoot) {
|
|
517
|
+
const path = join(agentRoot, "config/voice.yaml");
|
|
518
|
+
if (!existsSync(path)) return {};
|
|
519
|
+
try {
|
|
520
|
+
// Tolerant minimal parse — we only need the top-level `voice:` key.
|
|
521
|
+
const body = readFileSync(path, "utf-8");
|
|
522
|
+
const m = body.match(/^voice:\s*([^\s#]+)/m);
|
|
523
|
+
return { voice: m ? m[1].trim() : null };
|
|
524
|
+
} catch {
|
|
525
|
+
return {};
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function slugify(s) {
|
|
530
|
+
return String(s || "")
|
|
531
|
+
.toLowerCase()
|
|
532
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
533
|
+
.replace(/^-|-$/g, "");
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function escapeXml(s) {
|
|
537
|
+
return String(s).replace(/[<>&"']/g, (c) => ({
|
|
538
|
+
"<": "<", ">": ">", "&": "&", '"': """, "'": "'",
|
|
539
|
+
}[c]));
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
export default { sendVoiceMessage, placeOutboundCall, pickTransport };
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* outbound.test.mjs — node:test coverage for outbound voice.
|
|
3
|
+
*
|
|
4
|
+
* No real network: synthesizeSpeech, Baileys, Slack, Twilio all stubbed
|
|
5
|
+
* via dependency injection / process.env manipulation.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { test } from "node:test";
|
|
9
|
+
import assert from "node:assert/strict";
|
|
10
|
+
|
|
11
|
+
import { pickTransport } from "./outbound.mjs";
|
|
12
|
+
import { pickDefaultVoice, ttsCostEstimate } from "./tts.mjs";
|
|
13
|
+
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// pickTransport
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
test("pickTransport: explicit override wins", () => {
|
|
19
|
+
assert.equal(pickTransport({ to: "+14155551234", transport: "pstn" }), "pstn");
|
|
20
|
+
assert.equal(pickTransport({ to: "C12345678", transport: "whatsapp" }), "whatsapp");
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("pickTransport: Slack channel id (C/D/G prefix)", () => {
|
|
24
|
+
assert.equal(pickTransport({ to: "C012345678" }), "slack");
|
|
25
|
+
assert.equal(pickTransport({ to: "D099N1JE7NE" }), "slack");
|
|
26
|
+
assert.equal(pickTransport({ to: "GABC123456" }), "slack");
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("pickTransport: WhatsApp JID", () => {
|
|
30
|
+
assert.equal(pickTransport({ to: "447825004124@s.whatsapp.net" }), "whatsapp");
|
|
31
|
+
assert.equal(pickTransport({ to: "120363041234567890@g.us" }), "whatsapp");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("pickTransport: E.164 without WA → pstn", () => {
|
|
35
|
+
// agentRoot pointing at a tmp dir without state/whatsapp/auth/creds.json
|
|
36
|
+
assert.equal(pickTransport({ to: "+14155551234", agentRoot: "/tmp/no-wa-here-xyz" }), "pstn");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("pickTransport: refuses ambiguous targets", () => {
|
|
40
|
+
assert.throws(() => pickTransport({ to: "" }), /to required/);
|
|
41
|
+
assert.throws(() => pickTransport({ to: "Alex" }), /cannot infer/);
|
|
42
|
+
assert.throws(() => pickTransport({ to: "alex@example.com" }), /cannot infer/);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
// TTS helpers
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
|
|
49
|
+
test("pickDefaultVoice: maps Realtime-only voices to TTS equivalents", () => {
|
|
50
|
+
assert.equal(pickDefaultVoice({ voice: "cedar" }), "ash");
|
|
51
|
+
assert.equal(pickDefaultVoice({ voice: "marin" }), "shimmer");
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("pickDefaultVoice: passes through valid TTS voices", () => {
|
|
55
|
+
for (const v of ["alloy", "ash", "ballad", "coral", "echo", "sage", "shimmer", "verse"]) {
|
|
56
|
+
assert.equal(pickDefaultVoice({ voice: v }), v);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("pickDefaultVoice: fallback to alloy on unknown/empty", () => {
|
|
61
|
+
assert.equal(pickDefaultVoice({}), "alloy");
|
|
62
|
+
assert.equal(pickDefaultVoice({ voice: "nonexistent" }), "alloy");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("ttsCostEstimate: matches OpenAI's $0.015/1k chars", () => {
|
|
66
|
+
assert.equal(ttsCostEstimate(""), 0);
|
|
67
|
+
assert.equal(ttsCostEstimate("x".repeat(1000)), 0.015);
|
|
68
|
+
assert.equal(ttsCostEstimate("x".repeat(10000)), 0.15);
|
|
69
|
+
});
|