@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,75 @@
|
|
|
1
|
+
// Priority Trigger — writes a priority task file when urgent items detected
|
|
2
|
+
import { writeFileSync, mkdirSync, readFileSync } from "fs";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
import { AGENT_REPO_DIR } from "./utils.mjs";
|
|
5
|
+
|
|
6
|
+
const COOLDOWN_MS = 30_000;
|
|
7
|
+
let lastTrigger = 0;
|
|
8
|
+
|
|
9
|
+
// Cached agent identity — used to label the "mentions agent" reason
|
|
10
|
+
// without hardcoding any one agent's name in the reason string.
|
|
11
|
+
let _agent = null;
|
|
12
|
+
function loadAgent() {
|
|
13
|
+
if (_agent) return _agent;
|
|
14
|
+
try {
|
|
15
|
+
_agent = JSON.parse(readFileSync(join(AGENT_REPO_DIR, "config/agent.json"), "utf-8"));
|
|
16
|
+
} catch {
|
|
17
|
+
_agent = { firstName: "Agent" };
|
|
18
|
+
}
|
|
19
|
+
return _agent;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Back-compat export: callers used to invoke `triggerSophie`. The function
|
|
23
|
+
// is fully agent-neutral now; both names map to the same implementation.
|
|
24
|
+
export function triggerAgent(item) {
|
|
25
|
+
const now = Date.now();
|
|
26
|
+
if (now - lastTrigger < COOLDOWN_MS) {
|
|
27
|
+
console.log(
|
|
28
|
+
`[trigger] Cooldown active, skipping trigger for: ${item.subject}`,
|
|
29
|
+
);
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
lastTrigger = now;
|
|
33
|
+
|
|
34
|
+
const reasons = [];
|
|
35
|
+
if (item.priority_signals?.from_ceo) reasons.push("Message from CEO");
|
|
36
|
+
if (item.priority_signals?.tagged_urgent) reasons.push("Tagged urgent");
|
|
37
|
+
if (item.priority_signals?.mentions_agent) reasons.push(`Mentions ${loadAgent().firstName}`);
|
|
38
|
+
const reason = reasons.join(", ") || "Priority item";
|
|
39
|
+
|
|
40
|
+
console.log(`[trigger] Priority event detected: ${reason}`);
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
const dir = join(AGENT_REPO_DIR, "state", "inbox", "internal");
|
|
44
|
+
mkdirSync(dir, { recursive: true });
|
|
45
|
+
const taskFile = join(dir, `priority-${Date.now()}.yaml`);
|
|
46
|
+
let content = `type: priority_trigger
|
|
47
|
+
reason: "${reason}"
|
|
48
|
+
source_item: "${item.raw_ref}"
|
|
49
|
+
timestamp: "${new Date().toISOString()}"
|
|
50
|
+
sender: "${item.sender}"
|
|
51
|
+
channel: "${item.channel}"
|
|
52
|
+
content: |
|
|
53
|
+
${(item.content || "").replace(/\n/g, "\n ")}
|
|
54
|
+
`;
|
|
55
|
+
// Include attachment metadata so downstream sessions can view files
|
|
56
|
+
if (item.attachments && item.attachments.length > 0) {
|
|
57
|
+
content += `attachments:\n`;
|
|
58
|
+
for (const att of item.attachments) {
|
|
59
|
+
content += ` - name: "${att.name || "unnamed"}"\n`;
|
|
60
|
+
content += ` mimetype: "${att.mimetype || "unknown"}"\n`;
|
|
61
|
+
content += ` size: ${att.size || 0}\n`;
|
|
62
|
+
if (att.local_path) content += ` local_path: "${att.local_path}"\n`;
|
|
63
|
+
if (att.url_private) content += ` url_private: "${att.url_private}"\n`;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
writeFileSync(taskFile, content);
|
|
67
|
+
console.log(`[trigger] Priority task written to ${taskFile}`);
|
|
68
|
+
return true;
|
|
69
|
+
} catch (err) {
|
|
70
|
+
console.error(`[trigger] Failed to trigger: ${err.message}`);
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export const triggerSophie = triggerAgent;
|
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, appendFileSync, mkdirSync, existsSync, readdirSync, renameSync } from "fs";
|
|
2
|
+
import { join, dirname, extname } from "path";
|
|
3
|
+
import { fileURLToPath } from "url";
|
|
4
|
+
|
|
5
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
export const AGENT_REPO_DIR = process.env.AGENT_DIR || join(__dirname, "../..");
|
|
7
|
+
|
|
8
|
+
// Re-export under legacy name for any external script still importing
|
|
9
|
+
// SOPHIE_AI_DIR from this module. New code should use AGENT_REPO_DIR.
|
|
10
|
+
export const SOPHIE_AI_DIR = AGENT_REPO_DIR;
|
|
11
|
+
export const ATTACHMENTS_DIR = join(AGENT_REPO_DIR, "state", "inbox", "attachments");
|
|
12
|
+
|
|
13
|
+
export function writeInboxItem(service, item, agentRoot = AGENT_REPO_DIR) {
|
|
14
|
+
const dir = join(agentRoot, "state", "inbox", service);
|
|
15
|
+
mkdirSync(dir, { recursive: true });
|
|
16
|
+
const ts = item.timestamp.replace(/[:.]/g, "-");
|
|
17
|
+
const filename = `${ts}-${item.id}.yaml`;
|
|
18
|
+
const filePath = join(dir, filename);
|
|
19
|
+
|
|
20
|
+
// Skip if this item was already written or already processed/deferred/in-flight.
|
|
21
|
+
// The daemon renames files to .processed after handling, to .deferred
|
|
22
|
+
// when parked behind an active channel lock, to .processed-bundled
|
|
23
|
+
// when older entries are folded into a newer message during promotion,
|
|
24
|
+
// and to .dispatched while a session is in-flight (durable admission, F1).
|
|
25
|
+
// Without all of these checks the 30-min thread lookback re-creates the
|
|
26
|
+
// file every poll cycle, causing duplicate responses, surprise re-promotion
|
|
27
|
+
// of work that was already bundled, or double-delivery of an in-flight item.
|
|
28
|
+
if (
|
|
29
|
+
existsSync(filePath) ||
|
|
30
|
+
existsSync(filePath + ".processed") ||
|
|
31
|
+
existsSync(filePath + ".deferred") ||
|
|
32
|
+
existsSync(filePath + ".processed-bundled") ||
|
|
33
|
+
existsSync(filePath + ".dispatched")
|
|
34
|
+
) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
writeFileSync(filePath, inboxItemToYaml(item));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ── Durable inbox admission (F1 / H2) ────────────────────────────────────────
|
|
42
|
+
//
|
|
43
|
+
// The crash window: the daemon used to rename an inbox item straight to
|
|
44
|
+
// `.processed` the instant `dispatch()` returned — but `dispatch()` may only
|
|
45
|
+
// have parked the item in an in-memory queue (governor/budget/rate gate, or all
|
|
46
|
+
// slots full). A crash/restart then loses the in-memory queue while the durable
|
|
47
|
+
// file already reads `.processed` and `writeInboxItem` dedupe blocks re-creation
|
|
48
|
+
// — the user's message is silently dropped.
|
|
49
|
+
//
|
|
50
|
+
// Fix: a `.dispatched` intermediate state. `markDispatched` parks an *admitted*
|
|
51
|
+
// item as `.dispatched` (durable, but NOT terminal); `markInboxProcessed`
|
|
52
|
+
// retires it to `.processed` only on session close (success). The scanner skips
|
|
53
|
+
// fresh `.dispatched` files (no double-delivery on the happy path) but RECLAIMS
|
|
54
|
+
// stale ones (older than the reclaim window) so a crash mid-flight re-delivers.
|
|
55
|
+
//
|
|
56
|
+
// File-matching mirrors the daemon's markProcessed/markDeferred (id or raw_ref
|
|
57
|
+
// substring) so we hit the same file those would have. All paths are idempotent.
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Rename a live inbox file to `<name>.dispatched` at admission time. Durable
|
|
61
|
+
* but re-deliverable: a crash before session close leaves a `.dispatched` file
|
|
62
|
+
* the scanner can reclaim. No-op (returns 0) when there is no matching live
|
|
63
|
+
* file (e.g. backlog items, or already dispatched/processed).
|
|
64
|
+
*
|
|
65
|
+
* @returns {number} number of files renamed (usually 1, 0 for non-inbox items)
|
|
66
|
+
*/
|
|
67
|
+
export function markDispatched(item, service, agentRoot = AGENT_REPO_DIR) {
|
|
68
|
+
return renameLiveInbox(item, service, agentRoot, ".dispatched");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Retire an inbox item to `<name>.processed` on session close (success).
|
|
73
|
+
* Matches both the live `.yaml` (item never reached dispatch — e.g. ignored or
|
|
74
|
+
* quick-reply) and an in-flight `.dispatched` sibling. Terminal: the scanner
|
|
75
|
+
* never re-emits a `.processed` file.
|
|
76
|
+
*
|
|
77
|
+
* @returns {number} number of files renamed
|
|
78
|
+
*/
|
|
79
|
+
export function markInboxProcessed(item, service, agentRoot = AGENT_REPO_DIR) {
|
|
80
|
+
if (!item || !service || !agentRoot) return 0;
|
|
81
|
+
const inboxDir = join(agentRoot, "state", "inbox", service);
|
|
82
|
+
if (!existsSync(inboxDir)) return 0;
|
|
83
|
+
const needles = [item.id, item.raw_ref].filter(Boolean);
|
|
84
|
+
if (needles.length === 0) return 0;
|
|
85
|
+
let renamed = 0;
|
|
86
|
+
try {
|
|
87
|
+
const files = readdirSync(inboxDir).filter(
|
|
88
|
+
(f) =>
|
|
89
|
+
!f.endsWith(".processed") &&
|
|
90
|
+
!f.endsWith(".processed-bundled") &&
|
|
91
|
+
(f.endsWith(".yaml") || f.endsWith(".dispatched")) &&
|
|
92
|
+
needles.some((n) => f.includes(n))
|
|
93
|
+
);
|
|
94
|
+
for (const file of files) {
|
|
95
|
+
// Strip the live/.dispatched suffix, then append .processed so the result
|
|
96
|
+
// is always `<base>.yaml.processed` — the terminal name the scanner skips.
|
|
97
|
+
const base = file.replace(/\.dispatched$/, "");
|
|
98
|
+
renameSync(join(inboxDir, file), join(inboxDir, base + ".processed"));
|
|
99
|
+
renamed++;
|
|
100
|
+
}
|
|
101
|
+
} catch {
|
|
102
|
+
// Inbox dir may not exist for non-poller-backed items (e.g. backlog).
|
|
103
|
+
}
|
|
104
|
+
return renamed;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Shared helper: rename matching live `.yaml` inbox files to `<name><suffix>`. */
|
|
108
|
+
function renameLiveInbox(item, service, agentRoot, suffix) {
|
|
109
|
+
if (!item || !service || !agentRoot) return 0;
|
|
110
|
+
const inboxDir = join(agentRoot, "state", "inbox", service);
|
|
111
|
+
if (!existsSync(inboxDir)) return 0;
|
|
112
|
+
const needles = [item.id, item.raw_ref].filter(Boolean);
|
|
113
|
+
if (needles.length === 0) return 0;
|
|
114
|
+
let renamed = 0;
|
|
115
|
+
try {
|
|
116
|
+
const files = readdirSync(inboxDir).filter(
|
|
117
|
+
(f) => f.endsWith(".yaml") && needles.some((n) => f.includes(n))
|
|
118
|
+
);
|
|
119
|
+
for (const file of files) {
|
|
120
|
+
renameSync(join(inboxDir, file), join(inboxDir, file + suffix));
|
|
121
|
+
renamed++;
|
|
122
|
+
}
|
|
123
|
+
} catch {
|
|
124
|
+
// Inbox dir may not exist for non-poller-backed items.
|
|
125
|
+
}
|
|
126
|
+
return renamed;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function inboxItemToYaml(item) {
|
|
130
|
+
const content = (item.content || "").replace(/\n/g, "\n ");
|
|
131
|
+
let yaml = `id: "${item.id}"
|
|
132
|
+
service: "${item.service}"
|
|
133
|
+
channel: "${item.channel}"
|
|
134
|
+
sender: "${item.sender}"
|
|
135
|
+
sender_privilege: "${item.sender_privilege}"
|
|
136
|
+
timestamp: "${item.timestamp}"
|
|
137
|
+
subject: "${item.subject || ""}"
|
|
138
|
+
content: |
|
|
139
|
+
${content}
|
|
140
|
+
thread_id: "${item.thread_id || ""}"
|
|
141
|
+
is_reply: ${item.is_reply || false}
|
|
142
|
+
is_voice_transcript: ${item.is_voice_transcript || false}
|
|
143
|
+
priority_signals:
|
|
144
|
+
from_ceo: ${item.priority_signals?.from_ceo || false}
|
|
145
|
+
tagged_urgent: ${item.priority_signals?.tagged_urgent || false}
|
|
146
|
+
contains_deadline: ${item.priority_signals?.contains_deadline || false}
|
|
147
|
+
mentions_agent: ${item.priority_signals?.mentions_agent || false}
|
|
148
|
+
raw_ref: "${item.raw_ref || ""}"
|
|
149
|
+
`;
|
|
150
|
+
|
|
151
|
+
// Add thread context if present (conversation history before this message)
|
|
152
|
+
if (item.thread_context) {
|
|
153
|
+
const tc = item.thread_context.replace(/\n/g, "\n ");
|
|
154
|
+
yaml += `thread_context: |\n ${tc}\n`;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Add channel_id if present
|
|
158
|
+
if (item.channel_id) {
|
|
159
|
+
yaml += `channel_id: "${item.channel_id}"\n`;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Add event_type if present (e.g., message_changed, reaction)
|
|
163
|
+
if (item.event_type) {
|
|
164
|
+
yaml += `event_type: "${item.event_type}"\n`;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Add attachments if present
|
|
168
|
+
if (item.attachments && item.attachments.length > 0) {
|
|
169
|
+
yaml += `attachments:\n`;
|
|
170
|
+
for (const att of item.attachments) {
|
|
171
|
+
yaml += ` - id: "${att.id}"\n`;
|
|
172
|
+
yaml += ` name: "${att.name}"\n`;
|
|
173
|
+
yaml += ` mimetype: "${att.mimetype}"\n`;
|
|
174
|
+
yaml += ` size: ${att.size}\n`;
|
|
175
|
+
if (att.local_path) {
|
|
176
|
+
yaml += ` local_path: "${att.local_path}"\n`;
|
|
177
|
+
}
|
|
178
|
+
if (att.url_private) {
|
|
179
|
+
yaml += ` url_private: "${att.url_private}"\n`;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Add voice metadata if present
|
|
185
|
+
if (item.is_voice_transcript && item.voice_meta) {
|
|
186
|
+
yaml += `voice_meta:
|
|
187
|
+
caller_phone: "${item.voice_meta.caller_phone || ""}"
|
|
188
|
+
duration_seconds: ${item.voice_meta.duration_seconds || 0}
|
|
189
|
+
turns: ${item.voice_meta.turns || 0}
|
|
190
|
+
topics: [${(item.voice_meta.topics || []).map((t) => `"${t}"`).join(", ")}]
|
|
191
|
+
action_priority: "${item.voice_meta.action_priority || ""}"
|
|
192
|
+
`;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return yaml;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// ── Attachment Handling ──────────────────────────────────────────────────────
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Download a Slack file attachment to local storage.
|
|
202
|
+
* Slack file URLs require the bot token for authentication.
|
|
203
|
+
* Returns the local file path on success, null on failure.
|
|
204
|
+
*/
|
|
205
|
+
export async function downloadSlackAttachment(file, token) {
|
|
206
|
+
if (!file || !token) return null;
|
|
207
|
+
|
|
208
|
+
const downloadUrl = file.url_private_download || file.url_private;
|
|
209
|
+
if (!downloadUrl) return null;
|
|
210
|
+
|
|
211
|
+
const ext = extname(file.name || "") || "";
|
|
212
|
+
const safeId = (file.id || "unknown").replace(/[^a-zA-Z0-9_-]/g, "");
|
|
213
|
+
const safeName = (file.name || "file").replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
214
|
+
const localName = `${safeId}-${safeName}`;
|
|
215
|
+
const localPath = join(ATTACHMENTS_DIR, localName);
|
|
216
|
+
|
|
217
|
+
// Skip if already downloaded
|
|
218
|
+
if (existsSync(localPath)) return localPath;
|
|
219
|
+
|
|
220
|
+
try {
|
|
221
|
+
mkdirSync(ATTACHMENTS_DIR, { recursive: true });
|
|
222
|
+
const res = await fetch(downloadUrl, {
|
|
223
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
224
|
+
});
|
|
225
|
+
if (!res.ok) {
|
|
226
|
+
console.error(`[attachment] Failed to download ${file.name}: HTTP ${res.status}`);
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
const buffer = Buffer.from(await res.arrayBuffer());
|
|
230
|
+
writeFileSync(localPath, buffer);
|
|
231
|
+
return localPath;
|
|
232
|
+
} catch (err) {
|
|
233
|
+
console.error(`[attachment] Download error for ${file.name}: ${err.message}`);
|
|
234
|
+
return null;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Extract attachment metadata from a Slack message's files array.
|
|
240
|
+
* Returns a structured array suitable for inbox YAML serialization.
|
|
241
|
+
*/
|
|
242
|
+
export function extractSlackAttachments(files) {
|
|
243
|
+
if (!files || !Array.isArray(files) || files.length === 0) return [];
|
|
244
|
+
return files.map((f) => ({
|
|
245
|
+
id: f.id || "",
|
|
246
|
+
name: f.name || "unnamed",
|
|
247
|
+
mimetype: f.mimetype || f.filetype || "unknown",
|
|
248
|
+
size: f.size || 0,
|
|
249
|
+
url_private: f.url_private || "",
|
|
250
|
+
url_private_download: f.url_private_download || "",
|
|
251
|
+
local_path: null, // Populated after download
|
|
252
|
+
}));
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// ── Slack Reactions ─────────────────────────────────────────────────────────
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Add an emoji reaction to a Slack message.
|
|
259
|
+
* Used by the agent to acknowledge, approve, or signal status on messages.
|
|
260
|
+
*
|
|
261
|
+
* @param {string} channel - Slack channel ID
|
|
262
|
+
* @param {string} timestamp - Message timestamp (ts)
|
|
263
|
+
* @param {string} emoji - Emoji name without colons (e.g., "eyes", "white_check_mark")
|
|
264
|
+
* @param {string} token - Slack bot token
|
|
265
|
+
* @returns {Promise<{ok: boolean, error?: string}>}
|
|
266
|
+
*/
|
|
267
|
+
export async function addSlackReaction(channel, timestamp, emoji, token) {
|
|
268
|
+
if (!token || !channel || !timestamp || !emoji) {
|
|
269
|
+
return { ok: false, error: "missing_parameters" };
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
try {
|
|
273
|
+
const res = await fetch("https://slack.com/api/reactions.add", {
|
|
274
|
+
method: "POST",
|
|
275
|
+
headers: {
|
|
276
|
+
Authorization: `Bearer ${token}`,
|
|
277
|
+
"Content-Type": "application/json",
|
|
278
|
+
},
|
|
279
|
+
body: JSON.stringify({
|
|
280
|
+
channel,
|
|
281
|
+
timestamp,
|
|
282
|
+
name: emoji,
|
|
283
|
+
}),
|
|
284
|
+
});
|
|
285
|
+
return await res.json();
|
|
286
|
+
} catch (err) {
|
|
287
|
+
return { ok: false, error: err.message };
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export function readCursor(service) {
|
|
292
|
+
const path = join(
|
|
293
|
+
AGENT_REPO_DIR,
|
|
294
|
+
"state",
|
|
295
|
+
"polling",
|
|
296
|
+
`${service}-cursor.yaml`,
|
|
297
|
+
);
|
|
298
|
+
try {
|
|
299
|
+
const content = readFileSync(path, "utf-8");
|
|
300
|
+
const match = content.match(/last_checked:\s*"?([^"\n]*)"?/);
|
|
301
|
+
const val = match ? match[1] : null;
|
|
302
|
+
return val === "null" ? null : val;
|
|
303
|
+
} catch {
|
|
304
|
+
return null;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
export function writeCursor(service, timestamp) {
|
|
309
|
+
const path = join(
|
|
310
|
+
AGENT_REPO_DIR,
|
|
311
|
+
"state",
|
|
312
|
+
"polling",
|
|
313
|
+
`${service}-cursor.yaml`,
|
|
314
|
+
);
|
|
315
|
+
const content = `service: ${service}
|
|
316
|
+
last_checked: "${timestamp}"
|
|
317
|
+
cursors: {}
|
|
318
|
+
stats:
|
|
319
|
+
total_polls_today: 0
|
|
320
|
+
items_found_today: 0
|
|
321
|
+
errors_today: 0
|
|
322
|
+
`;
|
|
323
|
+
writeFileSync(path, content);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
export function appendLog(service, entry) {
|
|
327
|
+
const dir = join(AGENT_REPO_DIR, "logs", "polling");
|
|
328
|
+
mkdirSync(dir, { recursive: true });
|
|
329
|
+
const today = new Date().toISOString().split("T")[0];
|
|
330
|
+
const path = join(dir, `${today}-${service}.jsonl`);
|
|
331
|
+
appendFileSync(path, JSON.stringify(entry) + "\n");
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
export function isPriorityItem(item) {
|
|
335
|
+
if (!item) return false;
|
|
336
|
+
return (
|
|
337
|
+
item.priority_signals?.from_ceo ||
|
|
338
|
+
item.priority_signals?.tagged_urgent ||
|
|
339
|
+
item.priority_signals?.mentions_agent
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Known user IDs for privilege resolution — derived from config/agent.json so
|
|
344
|
+
// the agent's own Slack ID gets "system" privilege and the principal's gets "ceo".
|
|
345
|
+
const KNOWN_USERS = (() => {
|
|
346
|
+
const map = {};
|
|
347
|
+
try {
|
|
348
|
+
const a = JSON.parse(readFileSync(join(AGENT_REPO_DIR, "config/agent.json"), "utf-8"));
|
|
349
|
+
if (a.principal?.slackMemberId) {
|
|
350
|
+
map[a.principal.slackMemberId] = {
|
|
351
|
+
name: (a.principal.fullName || "").toLowerCase().replace(/\s+/g, "-"),
|
|
352
|
+
privilege: "ceo",
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
if (a.slackMemberId) {
|
|
356
|
+
map[a.slackMemberId] = {
|
|
357
|
+
name: (a.fullName || "").toLowerCase().replace(/\s+/g, "-"),
|
|
358
|
+
privilege: "system",
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
} catch { /* no agent.json yet — empty registry */ }
|
|
362
|
+
return map;
|
|
363
|
+
})();
|
|
364
|
+
|
|
365
|
+
export function resolvePrivilege(userId) {
|
|
366
|
+
return KNOWN_USERS[userId]?.privilege || "unknown";
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
export function resolveName(userId) {
|
|
370
|
+
return KNOWN_USERS[userId]?.name || userId;
|
|
371
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* voice-cloud-relay-client.mjs — Polls the Railway-hosted voice relay for
|
|
4
|
+
* buffered `realtime.call.incoming` events from OpenAI, then forwards
|
|
5
|
+
* each event to the local voice-webhook-server's /voice/sip/incoming
|
|
6
|
+
* endpoint so all the existing context-loader / tool-bridge / brief
|
|
7
|
+
* machinery runs unchanged.
|
|
8
|
+
*
|
|
9
|
+
* Lifecycle:
|
|
10
|
+
* 1. Honour .emergency-stop before any I/O.
|
|
11
|
+
* 2. Loop forever:
|
|
12
|
+
* a. GET <VOICE_RELAY_URL>/openai/realtime/calls with bearer auth
|
|
13
|
+
* b. For each event in the response, POST it to the local
|
|
14
|
+
* voice-webhook-server with a fabricated openai-signature
|
|
15
|
+
* header so the local handler trusts it (we compute the
|
|
16
|
+
* HMAC locally using OPENAI_WEBHOOK_SECRET).
|
|
17
|
+
* c. Sleep VOICE_RELAY_POLL_INTERVAL_MS (default 2500).
|
|
18
|
+
*
|
|
19
|
+
* Env (from .env):
|
|
20
|
+
* VOICE_RELAY_URL — https://<your-railway-domain>
|
|
21
|
+
* VOICE_RELAY_POLL_TOKEN — bearer for the relay's poll endpoint
|
|
22
|
+
* OPENAI_WEBHOOK_SECRET — to re-sign forwarded events for the
|
|
23
|
+
* local webhook server
|
|
24
|
+
* VOICE_WEBHOOK_PORT — local server port (default 3200)
|
|
25
|
+
* VOICE_RELAY_POLL_INTERVAL_MS — poll cadence (default 2500)
|
|
26
|
+
*
|
|
27
|
+
* If VOICE_RELAY_URL isn't set, this exits cleanly so installations not
|
|
28
|
+
* using Railway aren't affected.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
32
|
+
import { join, dirname, resolve } from "node:path";
|
|
33
|
+
import { fileURLToPath } from "node:url";
|
|
34
|
+
import { createHmac } from "node:crypto";
|
|
35
|
+
|
|
36
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
37
|
+
const AGENT_DIR = process.env.AGENT_DIR || process.env.AGENT_ROOT || resolve(__dirname, "../..");
|
|
38
|
+
|
|
39
|
+
if (existsSync(join(AGENT_DIR, ".emergency-stop"))) {
|
|
40
|
+
console.error("[voice-relay-poller] .emergency-stop present — refusing to start");
|
|
41
|
+
await new Promise((r) => setTimeout(r, 30_000));
|
|
42
|
+
process.exit(0);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function loadDotEnv(p) {
|
|
46
|
+
if (!existsSync(p)) return;
|
|
47
|
+
for (const ln of readFileSync(p, "utf-8").split("\n")) {
|
|
48
|
+
const t = ln.trim();
|
|
49
|
+
if (!t || t.startsWith("#")) continue;
|
|
50
|
+
const eq = t.indexOf("=");
|
|
51
|
+
if (eq <= 0) continue;
|
|
52
|
+
const k = t.slice(0, eq).trim();
|
|
53
|
+
let v = t.slice(eq + 1).trim();
|
|
54
|
+
if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"))) v = v.slice(1, -1);
|
|
55
|
+
if (!process.env[k]) process.env[k] = v;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
loadDotEnv(join(AGENT_DIR, ".env"));
|
|
59
|
+
|
|
60
|
+
const RELAY_URL = (process.env.VOICE_RELAY_URL || "").replace(/\/$/, "");
|
|
61
|
+
const POLL_TOKEN = process.env.VOICE_RELAY_POLL_TOKEN || "";
|
|
62
|
+
const OPENAI_WEBHOOK_SECRET = process.env.OPENAI_WEBHOOK_SECRET || "";
|
|
63
|
+
const LOCAL_WEBHOOK_PORT = parseInt(process.env.VOICE_WEBHOOK_PORT || "3200", 10);
|
|
64
|
+
const POLL_INTERVAL_MS = parseInt(process.env.VOICE_RELAY_POLL_INTERVAL_MS || "750", 10);
|
|
65
|
+
const LOCAL_WEBHOOK_URL = `http://127.0.0.1:${LOCAL_WEBHOOK_PORT}/voice/sip/incoming`;
|
|
66
|
+
|
|
67
|
+
if (!RELAY_URL) {
|
|
68
|
+
console.log("[voice-relay-poller] VOICE_RELAY_URL not set — exiting (Railway relay not in use)");
|
|
69
|
+
process.exit(0);
|
|
70
|
+
}
|
|
71
|
+
if (!POLL_TOKEN) {
|
|
72
|
+
console.error("[voice-relay-poller] VOICE_RELAY_POLL_TOKEN not set — refusing to poll an unauthenticated relay");
|
|
73
|
+
process.exit(78);
|
|
74
|
+
}
|
|
75
|
+
if (!OPENAI_WEBHOOK_SECRET) {
|
|
76
|
+
console.warn("[voice-relay-poller] OPENAI_WEBHOOK_SECRET not set — local webhook server may reject forwarded events");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
const { acquireLock } = await import("../../lib/singleton.js");
|
|
81
|
+
if (!acquireLock("voice-relay-poller")) {
|
|
82
|
+
console.log("[voice-relay-poller] already running — exiting");
|
|
83
|
+
process.exit(0);
|
|
84
|
+
}
|
|
85
|
+
} catch { /* singleton unavailable; continue without lock */ }
|
|
86
|
+
|
|
87
|
+
console.log(`[voice-relay-poller] start: relay=${RELAY_URL} interval=${POLL_INTERVAL_MS}ms local=${LOCAL_WEBHOOK_URL}`);
|
|
88
|
+
|
|
89
|
+
let consecutiveErrors = 0;
|
|
90
|
+
const MAX_BACKOFF_MS = 60_000;
|
|
91
|
+
|
|
92
|
+
async function pollOnce() {
|
|
93
|
+
let resp;
|
|
94
|
+
try {
|
|
95
|
+
resp = await fetch(`${RELAY_URL}/openai/realtime/calls`, {
|
|
96
|
+
method: "GET",
|
|
97
|
+
headers: { Authorization: `Bearer ${POLL_TOKEN}` },
|
|
98
|
+
});
|
|
99
|
+
} catch (err) {
|
|
100
|
+
consecutiveErrors++;
|
|
101
|
+
console.warn(`[voice-relay-poller] poll fetch failed (${consecutiveErrors}): ${err.message}`);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
if (!resp.ok) {
|
|
105
|
+
consecutiveErrors++;
|
|
106
|
+
const body = await resp.text().catch(() => "");
|
|
107
|
+
console.warn(`[voice-relay-poller] poll HTTP ${resp.status}: ${body.slice(0, 200)}`);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
consecutiveErrors = 0;
|
|
111
|
+
let data;
|
|
112
|
+
try { data = await resp.json(); }
|
|
113
|
+
catch (err) { console.warn(`[voice-relay-poller] poll bad json: ${err.message}`); return; }
|
|
114
|
+
|
|
115
|
+
if (!data.events || data.events.length === 0) return;
|
|
116
|
+
console.log(`[voice-relay-poller] received ${data.events.length} buffered event(s)`);
|
|
117
|
+
|
|
118
|
+
for (const evt of data.events) {
|
|
119
|
+
await forwardToLocal(evt);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function forwardToLocal(evt) {
|
|
124
|
+
// Prefer evt.raw_body (the exact bytes OpenAI sent + signed) so HMAC
|
|
125
|
+
// verification on the local side matches. If absent (older relay
|
|
126
|
+
// version), fall back to re-stringifying — but warn the operator.
|
|
127
|
+
const payloadStr = typeof evt.raw_body === "string" && evt.raw_body.length
|
|
128
|
+
? evt.raw_body
|
|
129
|
+
: JSON.stringify(evt.payload || {});
|
|
130
|
+
if (!evt.raw_body) {
|
|
131
|
+
console.warn(`[voice-relay-poller] no raw_body in event — re-stringified payload may HMAC-mismatch local server`);
|
|
132
|
+
}
|
|
133
|
+
const headers = { "Content-Type": "application/json" };
|
|
134
|
+
if (OPENAI_WEBHOOK_SECRET) {
|
|
135
|
+
headers["openai-signature"] = createHmac("sha256", OPENAI_WEBHOOK_SECRET)
|
|
136
|
+
.update(payloadStr)
|
|
137
|
+
.digest("hex");
|
|
138
|
+
}
|
|
139
|
+
// If Railway already POSTed /accept on receipt (because it's the only path
|
|
140
|
+
// with low enough latency to beat OpenAI's accept window), tell the local
|
|
141
|
+
// handler to skip its own accept attempt — the call_id is already taken.
|
|
142
|
+
if (evt.accepted_by_relay) {
|
|
143
|
+
headers["x-maestro-relay-accepted"] = "1";
|
|
144
|
+
}
|
|
145
|
+
try {
|
|
146
|
+
const r = await fetch(LOCAL_WEBHOOK_URL, {
|
|
147
|
+
method: "POST",
|
|
148
|
+
headers,
|
|
149
|
+
body: payloadStr,
|
|
150
|
+
});
|
|
151
|
+
const body = await r.text().catch(() => "");
|
|
152
|
+
if (r.ok) {
|
|
153
|
+
console.log(`[voice-relay-poller] forwarded ${evt.event_type} call=${evt.call_id} → local OK`);
|
|
154
|
+
} else {
|
|
155
|
+
console.warn(`[voice-relay-poller] local webhook HTTP ${r.status}: ${body.slice(0, 200)}`);
|
|
156
|
+
}
|
|
157
|
+
} catch (err) {
|
|
158
|
+
console.warn(`[voice-relay-poller] forward failed: ${err.message}`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function loop() {
|
|
163
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
164
|
+
while (true) {
|
|
165
|
+
await pollOnce();
|
|
166
|
+
const backoff = consecutiveErrors > 0
|
|
167
|
+
? Math.min(MAX_BACKOFF_MS, POLL_INTERVAL_MS * Math.pow(2, consecutiveErrors))
|
|
168
|
+
: POLL_INTERVAL_MS;
|
|
169
|
+
await sleep(backoff);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
process.on("SIGTERM", () => { console.log("[voice-relay-poller] SIGTERM"); process.exit(0); });
|
|
174
|
+
process.on("SIGINT", () => { console.log("[voice-relay-poller] SIGINT"); process.exit(0); });
|
|
175
|
+
|
|
176
|
+
loop().catch((err) => {
|
|
177
|
+
console.error(`[voice-relay-poller] fatal: ${err.message}`);
|
|
178
|
+
process.exit(1);
|
|
179
|
+
});
|