@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,257 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Maestro — Voice session rotation
|
|
3
|
+
*
|
|
4
|
+
* OpenAI Realtime has a documented latency-drift bug on long sessions:
|
|
5
|
+
* median turn latency climbs from ~800 ms to >2 s after ~20 turns. The
|
|
6
|
+
* supported mitigation is to **rotate the session** every N turns or M
|
|
7
|
+
* minutes — summarise the conversation, close the WebSocket, open a new
|
|
8
|
+
* one, and re-seed the new session with a system message containing the
|
|
9
|
+
* running summary.
|
|
10
|
+
*
|
|
11
|
+
* This module provides:
|
|
12
|
+
* shouldRotate(state, opts) → boolean
|
|
13
|
+
* rotateSessionIfStale(provider, state, opts) → { rotated, newProvider }
|
|
14
|
+
* makeSummary(transcript) → short text summary for re-seeding
|
|
15
|
+
*
|
|
16
|
+
* The voice worker calls rotateSessionIfStale() after each TURN_DONE.
|
|
17
|
+
* If rotation is needed it spins up a fresh provider with the same
|
|
18
|
+
* sessionConfig, plays the summary into it via sendText(), and the
|
|
19
|
+
* worker swaps providers transparently. The transcript + audit log
|
|
20
|
+
* persist across the rotation in state/voice/sessions/.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { VOICE_EVENTS } from "./provider.mjs";
|
|
24
|
+
|
|
25
|
+
export const SESSION_ROTATION_DEFAULTS = Object.freeze({
|
|
26
|
+
max_turns: 12, // rotate after this many user→agent turn-pairs
|
|
27
|
+
max_duration_ms: 4 * 60 * 1000, // rotate after this much wall-clock
|
|
28
|
+
max_p95_latency_ms: 2000, // rotate if last 5 turns averaged > this
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Decide whether the session is stale enough to rotate.
|
|
33
|
+
*
|
|
34
|
+
* @param {object} state Tracking state from the voice worker:
|
|
35
|
+
* {
|
|
36
|
+
* turns: number,
|
|
37
|
+
* started_at: number (ms epoch),
|
|
38
|
+
* recent_latencies_ms: number[], // last N turn latencies
|
|
39
|
+
* }
|
|
40
|
+
* @param {object} opts Optional overrides for the defaults.
|
|
41
|
+
* @returns {{ rotate: boolean, reason?: string }}
|
|
42
|
+
*/
|
|
43
|
+
export function shouldRotate(state, opts = {}) {
|
|
44
|
+
const cfg = { ...SESSION_ROTATION_DEFAULTS, ...opts };
|
|
45
|
+
if (!state) return { rotate: false };
|
|
46
|
+
|
|
47
|
+
if ((state.turns || 0) >= cfg.max_turns) {
|
|
48
|
+
return { rotate: true, reason: "max_turns" };
|
|
49
|
+
}
|
|
50
|
+
const ageMs = Date.now() - (state.started_at || Date.now());
|
|
51
|
+
if (ageMs >= cfg.max_duration_ms) {
|
|
52
|
+
return { rotate: true, reason: "max_duration" };
|
|
53
|
+
}
|
|
54
|
+
const recent = state.recent_latencies_ms || [];
|
|
55
|
+
if (recent.length >= 5) {
|
|
56
|
+
const sorted = [...recent].sort((a, b) => a - b);
|
|
57
|
+
const p95 = sorted[Math.max(0, Math.floor(sorted.length * 0.95) - 1)];
|
|
58
|
+
if (p95 >= cfg.max_p95_latency_ms) {
|
|
59
|
+
return { rotate: true, reason: "latency_drift" };
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return { rotate: false };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Build a compact summary of a transcript for re-seeding into a fresh
|
|
67
|
+
* session. We keep this dumb-on-purpose: deterministic, no LLM round-trip,
|
|
68
|
+
* preserves the most recent N exchanges and the most recent tool calls.
|
|
69
|
+
*
|
|
70
|
+
* @param {Array<{role: string, text?: string, tool?: object}>} transcript
|
|
71
|
+
* @param {object} [opts]
|
|
72
|
+
* @returns {string}
|
|
73
|
+
*/
|
|
74
|
+
export function makeSummary(transcript, opts = {}) {
|
|
75
|
+
const keepExchanges = opts.keepExchanges ?? 4;
|
|
76
|
+
const keepTools = opts.keepTools ?? 3;
|
|
77
|
+
|
|
78
|
+
if (!Array.isArray(transcript) || !transcript.length) {
|
|
79
|
+
return "Conversation continues. No prior context available.";
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const exchanges = transcript.filter((t) => t.role === "user" || t.role === "assistant");
|
|
83
|
+
const recentExchanges = exchanges.slice(-keepExchanges * 2);
|
|
84
|
+
const recentTools = transcript
|
|
85
|
+
.filter((t) => t.tool)
|
|
86
|
+
.slice(-keepTools)
|
|
87
|
+
.map((t) => ({
|
|
88
|
+
name: t.tool.name,
|
|
89
|
+
ok: t.tool.error ? false : true,
|
|
90
|
+
summary: summariseToolCall(t.tool),
|
|
91
|
+
}));
|
|
92
|
+
|
|
93
|
+
const lines = [
|
|
94
|
+
"[Session rotated for latency reasons — running summary follows.]",
|
|
95
|
+
"",
|
|
96
|
+
"Recent dialogue:",
|
|
97
|
+
...recentExchanges.map((t) => `- ${t.role}: ${truncate(t.text || "", 240)}`),
|
|
98
|
+
];
|
|
99
|
+
if (recentTools.length) {
|
|
100
|
+
lines.push("", "Recent tool activity:");
|
|
101
|
+
for (const r of recentTools) {
|
|
102
|
+
lines.push(`- ${r.name} → ${r.ok ? "ok" : "error"}${r.summary ? ` (${r.summary})` : ""}`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
lines.push("", "Continue the conversation from this state. Do not re-introduce yourself.");
|
|
106
|
+
return lines.join("\n");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function summariseToolCall(tool) {
|
|
110
|
+
if (!tool) return "";
|
|
111
|
+
if (tool.error) return `error: ${truncate(String(tool.error), 80)}`;
|
|
112
|
+
if (tool.args?.channel) return `to ${tool.args.channel}`;
|
|
113
|
+
if (tool.args?.to) return `to ${tool.args.to}`;
|
|
114
|
+
if (tool.args?.query) return `query="${truncate(tool.args.query, 60)}"`;
|
|
115
|
+
return "";
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function truncate(s, n) {
|
|
119
|
+
if (!s || s.length <= n) return s;
|
|
120
|
+
return s.slice(0, n - 1) + "…";
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Default time (ms) to wait for a freshly-spun provider to confirm it is
|
|
125
|
+
* live before we treat the rotation as failed and keep the old session.
|
|
126
|
+
*/
|
|
127
|
+
export const ROTATION_CONFIRM_TIMEOUT_MS = 5000;
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Wait until a freshly-connected provider is observably live, or fail.
|
|
131
|
+
*
|
|
132
|
+
* "Live" means either:
|
|
133
|
+
* - its connect() promise resolved (already awaited by the caller), AND
|
|
134
|
+
* - it has reached a non-idle/non-closed state, OR emitted CONNECTED /
|
|
135
|
+
* SESSION_CREATED within `timeoutMs`.
|
|
136
|
+
*
|
|
137
|
+
* connect() resolving is necessary but not sufficient: a provider can
|
|
138
|
+
* resolve connect() and then immediately drop the upstream socket. We
|
|
139
|
+
* therefore confirm against the observable signals the provider exposes.
|
|
140
|
+
*
|
|
141
|
+
* @param {VoiceProvider} provider The newly-connected provider.
|
|
142
|
+
* @param {number} timeoutMs
|
|
143
|
+
* @returns {Promise<void>} Resolves when live; rejects on timeout/error.
|
|
144
|
+
*/
|
|
145
|
+
function confirmProviderLive(provider, timeoutMs) {
|
|
146
|
+
const healthy = (s) => s && s !== "idle" && s !== "closed";
|
|
147
|
+
|
|
148
|
+
return new Promise((resolve, reject) => {
|
|
149
|
+
// Fast path: if the provider already reports a healthy state (e.g.
|
|
150
|
+
// connect() resolved and set state="connected"), trust it. Evaluated
|
|
151
|
+
// inside the executor so it reflects state AFTER connect() resolved,
|
|
152
|
+
// not the state captured when this promise was constructed.
|
|
153
|
+
if (healthy(provider?.state)) { resolve(); return; }
|
|
154
|
+
|
|
155
|
+
// No event surface to observe and not already healthy → cannot confirm.
|
|
156
|
+
if (typeof provider?.once !== "function") {
|
|
157
|
+
reject(new Error("rotation: new provider is not observable and not in a live state"));
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
let settled = false;
|
|
161
|
+
const finish = (fn, arg) => {
|
|
162
|
+
if (settled) return;
|
|
163
|
+
settled = true;
|
|
164
|
+
clearTimeout(timer);
|
|
165
|
+
try { provider.removeListener?.(VOICE_EVENTS.CONNECTED, onUp); } catch { /* */ }
|
|
166
|
+
try { provider.removeListener?.(VOICE_EVENTS.SESSION_CREATED, onUp); } catch { /* */ }
|
|
167
|
+
try { provider.removeListener?.(VOICE_EVENTS.CLOSED, onDown); } catch { /* */ }
|
|
168
|
+
try { provider.removeListener?.(VOICE_EVENTS.ERROR, onErr); } catch { /* */ }
|
|
169
|
+
fn(arg);
|
|
170
|
+
};
|
|
171
|
+
const onUp = () => finish(resolve);
|
|
172
|
+
const onDown = (info) => finish(reject, new Error(`rotation: new provider closed before confirmed live${info?.reason ? ` (${info.reason})` : ""}`));
|
|
173
|
+
const onErr = (err) => finish(reject, err instanceof Error ? err : new Error(String(err?.message || err)));
|
|
174
|
+
const timer = setTimeout(
|
|
175
|
+
() => finish(reject, new Error(`rotation: new provider did not confirm live within ${timeoutMs}ms`)),
|
|
176
|
+
timeoutMs,
|
|
177
|
+
);
|
|
178
|
+
if (typeof timer.unref === "function") timer.unref();
|
|
179
|
+
|
|
180
|
+
provider.once(VOICE_EVENTS.CONNECTED, onUp);
|
|
181
|
+
provider.once(VOICE_EVENTS.SESSION_CREATED, onUp);
|
|
182
|
+
provider.once(VOICE_EVENTS.CLOSED, onDown);
|
|
183
|
+
provider.once(VOICE_EVENTS.ERROR, onErr);
|
|
184
|
+
|
|
185
|
+
// Re-check state after wiring listeners to avoid a race where the
|
|
186
|
+
// provider became healthy between the fast-path check and now.
|
|
187
|
+
if (healthy(provider.state)) finish(resolve);
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Rotate the session if stale. Returns the (possibly-new) provider plus
|
|
193
|
+
* a flag indicating whether rotation actually happened.
|
|
194
|
+
*
|
|
195
|
+
* The new session is brought up and CONFIRMED LIVE before we touch the
|
|
196
|
+
* old one. If the bring-up rejects (or never confirms live), we keep the
|
|
197
|
+
* old provider running, best-effort close the half-open new provider, and
|
|
198
|
+
* return { rotated: false, provider: <old> } so the caller keeps serving
|
|
199
|
+
* the call instead of cutting over to a dead session. The old provider is
|
|
200
|
+
* only closed AFTER the new one is confirmed live (M10).
|
|
201
|
+
*
|
|
202
|
+
* Caller must:
|
|
203
|
+
* - Provide `createNewProvider`: a factory the worker uses to spin up
|
|
204
|
+
* a fresh provider with identical session config.
|
|
205
|
+
* - Provide `state.transcript` so the summary can be built.
|
|
206
|
+
*
|
|
207
|
+
* @param {VoiceProvider} provider Current live provider
|
|
208
|
+
* @param {object} state
|
|
209
|
+
* @param {object} opts
|
|
210
|
+
* @param {number} [opts.confirmTimeoutMs] Override the liveness timeout.
|
|
211
|
+
*/
|
|
212
|
+
export async function rotateSessionIfStale(provider, state, opts = {}) {
|
|
213
|
+
const decision = shouldRotate(state, opts.thresholds);
|
|
214
|
+
if (!decision.rotate) return { rotated: false, provider };
|
|
215
|
+
|
|
216
|
+
if (typeof opts.createNewProvider !== "function") {
|
|
217
|
+
throw new Error("rotateSessionIfStale requires opts.createNewProvider");
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const summary = makeSummary(state.transcript || [], opts.summary);
|
|
221
|
+
const oldProvider = provider;
|
|
222
|
+
const sessionConfig = state.sessionConfig || provider.sessionConfig;
|
|
223
|
+
const confirmTimeoutMs = opts.confirmTimeoutMs ?? ROTATION_CONFIRM_TIMEOUT_MS;
|
|
224
|
+
|
|
225
|
+
let next;
|
|
226
|
+
try {
|
|
227
|
+
next = opts.createNewProvider({ reason: decision.reason });
|
|
228
|
+
// Bring up the new session, then confirm it is actually live before we
|
|
229
|
+
// touch the old one. confirmProviderLive checks the post-connect state
|
|
230
|
+
// (a well-behaved provider sets state="connected"/emits CONNECTED from
|
|
231
|
+
// connect()) and, if not yet healthy, waits for CONNECTED/SESSION_CREATED
|
|
232
|
+
// with a timeout — rejecting on CLOSED/ERROR. A provider that resolves
|
|
233
|
+
// connect() but drops (or never reaches a live state) fails confirmation
|
|
234
|
+
// and we keep the old session running rather than cutting over.
|
|
235
|
+
await Promise.resolve(next.connect(sessionConfig));
|
|
236
|
+
await confirmProviderLive(next, confirmTimeoutMs);
|
|
237
|
+
// New session is live — seed it with the running summary.
|
|
238
|
+
next.sendText(`(System) ${summary}`);
|
|
239
|
+
} catch (err) {
|
|
240
|
+
// Bring-up failed: do NOT cut over. Tear down the half-open new
|
|
241
|
+
// provider so it can't leak, keep the old provider serving the call.
|
|
242
|
+
if (next) { try { next.close?.(`rotation_failed: ${decision.reason}`); } catch { /* */ } }
|
|
243
|
+
return {
|
|
244
|
+
rotated: false,
|
|
245
|
+
provider: oldProvider,
|
|
246
|
+
reason: decision.reason,
|
|
247
|
+
error: err instanceof Error ? err.message : String(err),
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Only now that the new session is confirmed live do we close the old.
|
|
252
|
+
try { oldProvider.close(`rotated: ${decision.reason}`); } catch { /* */ }
|
|
253
|
+
|
|
254
|
+
return { rotated: true, provider: next, reason: decision.reason };
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export default { shouldRotate, rotateSessionIfStale, makeSummary, SESSION_ROTATION_DEFAULTS };
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Maestro — Speech-to-text helper for inbound audio files.
|
|
3
|
+
*
|
|
4
|
+
* Wraps OpenAI's (and any OpenAI-compatible) `/v1/audio/transcriptions`
|
|
5
|
+
* endpoint. Used by the voice channel adapter's receive() path to turn an
|
|
6
|
+
* inbound voice note (WhatsApp PTT, Slack audio clip, a recorded Twilio
|
|
7
|
+
* call leg, an uploaded recording) into text the agent can reason over.
|
|
8
|
+
*
|
|
9
|
+
* Counterpart to lib/voice/tts.mjs (which goes text → audio). This goes
|
|
10
|
+
* audio file → text and mirrors its conventions: module-level defaults,
|
|
11
|
+
* apiKey from arg or OPENAI_API_KEY, clear HTTP errors, a default export.
|
|
12
|
+
*
|
|
13
|
+
* Why a separate helper from the Realtime API:
|
|
14
|
+
* - Asynchronous transcription of a *file* is much cheaper than a live
|
|
15
|
+
* Realtime session and is the right primitive for store-and-forward
|
|
16
|
+
* channels (WhatsApp / Slack / SMS) that hand you a file, not a stream.
|
|
17
|
+
* - whisper-1 / gpt-4o-transcribe accept multipart/form-data uploads;
|
|
18
|
+
* no streaming session required.
|
|
19
|
+
*
|
|
20
|
+
* Ported and adapted from OpenClaw
|
|
21
|
+
* (src/media-understanding/openai-compatible-audio.ts —
|
|
22
|
+
* transcribeOpenAiCompatibleAudio) for the maestro autonomy model. The
|
|
23
|
+
* OpenClaw-specific provider-config / DNS-pinning / dispatcher plumbing is
|
|
24
|
+
* dropped in favour of plain global fetch + FormData/Blob (Node 18+) with
|
|
25
|
+
* an AbortController timeout.
|
|
26
|
+
*
|
|
27
|
+
* Public API:
|
|
28
|
+
* transcribeAudioFile({ filePath | buffer, apiKey, model, baseUrl,
|
|
29
|
+
* language, prompt, fileName, mime, timeoutMs })
|
|
30
|
+
* → Promise<{ text, raw }>
|
|
31
|
+
*
|
|
32
|
+
* Node builtins only (node:fs/promises, node:path); global fetch / FormData
|
|
33
|
+
* / Blob / AbortController are Node 18+ globals.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
import { readFile } from "node:fs/promises";
|
|
37
|
+
import { basename } from "node:path";
|
|
38
|
+
|
|
39
|
+
const DEFAULT_BASE_URL = "https://api.openai.com/v1";
|
|
40
|
+
const DEFAULT_MODEL = "whisper-1";
|
|
41
|
+
const DEFAULT_TIMEOUT_MS = 120000; // 2 min — large files / cold models
|
|
42
|
+
const DEFAULT_FILE_NAME = "audio";
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Strip a single trailing slash so `${baseUrl}/audio/transcriptions` never
|
|
46
|
+
* doubles up. Mirrors OpenClaw's resolveProviderHttpRequestConfig behaviour
|
|
47
|
+
* of treating baseUrl as a root without a trailing slash.
|
|
48
|
+
* @param {string} url
|
|
49
|
+
* @returns {string}
|
|
50
|
+
*/
|
|
51
|
+
function normalizeBaseUrl(url) {
|
|
52
|
+
return String(url || "").replace(/\/+$/, "");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* @typedef {Object} TranscribeAudioFileOptions
|
|
57
|
+
* @property {string} [filePath] Path to the audio file to transcribe. Mutually
|
|
58
|
+
* sufficient with `buffer`; one of the two is required.
|
|
59
|
+
* @property {Buffer|Uint8Array|ArrayBuffer} [buffer] Raw audio bytes. Used instead of
|
|
60
|
+
* reading from disk when supplied.
|
|
61
|
+
* @property {string} [apiKey] Override OPENAI_API_KEY.
|
|
62
|
+
* @property {string} [model] Transcription model id (default: whisper-1).
|
|
63
|
+
* @property {string} [baseUrl] OpenAI-compatible API root (default:
|
|
64
|
+
* https://api.openai.com/v1). No trailing "/audio/...".
|
|
65
|
+
* @property {string} [language] ISO-639-1 language hint (e.g. "en"); improves accuracy.
|
|
66
|
+
* @property {string} [prompt] Optional prompt to steer style / spelling.
|
|
67
|
+
* @property {string} [fileName] Filename to send in the multipart part. Defaults to the
|
|
68
|
+
* basename of `filePath`, else "audio".
|
|
69
|
+
* @property {string} [mime] Content-Type for the audio part (e.g. "audio/mpeg").
|
|
70
|
+
* @property {number} [timeoutMs] Request timeout in ms (default: 120000).
|
|
71
|
+
*/
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Transcribe a single audio file via an OpenAI-compatible
|
|
75
|
+
* `/audio/transcriptions` endpoint.
|
|
76
|
+
*
|
|
77
|
+
* @param {TranscribeAudioFileOptions} [opts]
|
|
78
|
+
* @returns {Promise<{ text: string, raw: any }>} The transcript text plus the
|
|
79
|
+
* raw parsed JSON payload from the provider.
|
|
80
|
+
*/
|
|
81
|
+
export async function transcribeAudioFile(opts = {}) {
|
|
82
|
+
const apiKey = opts.apiKey || process.env.OPENAI_API_KEY;
|
|
83
|
+
if (!apiKey) throw new Error("OPENAI_API_KEY not set");
|
|
84
|
+
|
|
85
|
+
// Resolve the audio bytes: explicit buffer wins, else read from disk.
|
|
86
|
+
let bytes;
|
|
87
|
+
let fileName = opts.fileName;
|
|
88
|
+
if (opts.buffer != null) {
|
|
89
|
+
bytes =
|
|
90
|
+
opts.buffer instanceof ArrayBuffer ? Buffer.from(opts.buffer) : Buffer.from(opts.buffer);
|
|
91
|
+
} else if (opts.filePath) {
|
|
92
|
+
bytes = await readFile(opts.filePath);
|
|
93
|
+
if (!fileName) fileName = basename(opts.filePath);
|
|
94
|
+
} else {
|
|
95
|
+
throw new Error("transcribeAudioFile: provide either filePath or buffer");
|
|
96
|
+
}
|
|
97
|
+
if (!fileName) fileName = DEFAULT_FILE_NAME;
|
|
98
|
+
|
|
99
|
+
const model = (opts.model && String(opts.model).trim()) || DEFAULT_MODEL;
|
|
100
|
+
const baseUrl = normalizeBaseUrl(opts.baseUrl || DEFAULT_BASE_URL);
|
|
101
|
+
const url = `${baseUrl}/audio/transcriptions`;
|
|
102
|
+
const timeoutMs =
|
|
103
|
+
typeof opts.timeoutMs === "number" && opts.timeoutMs > 0 ? opts.timeoutMs : DEFAULT_TIMEOUT_MS;
|
|
104
|
+
|
|
105
|
+
// Build multipart/form-data. fetch sets the Content-Type (with boundary)
|
|
106
|
+
// automatically when the body is a FormData instance — do NOT set it by hand.
|
|
107
|
+
const form = new FormData();
|
|
108
|
+
const blob = opts.mime ? new Blob([bytes], { type: opts.mime }) : new Blob([bytes]);
|
|
109
|
+
form.append("file", blob, fileName);
|
|
110
|
+
form.append("model", model);
|
|
111
|
+
if (opts.language) form.append("language", String(opts.language));
|
|
112
|
+
if (opts.prompt) form.append("prompt", String(opts.prompt));
|
|
113
|
+
|
|
114
|
+
const controller = new AbortController();
|
|
115
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
116
|
+
|
|
117
|
+
let r;
|
|
118
|
+
try {
|
|
119
|
+
r = await fetch(url, {
|
|
120
|
+
method: "POST",
|
|
121
|
+
headers: {
|
|
122
|
+
Authorization: `Bearer ${apiKey}`,
|
|
123
|
+
},
|
|
124
|
+
body: form,
|
|
125
|
+
signal: controller.signal,
|
|
126
|
+
});
|
|
127
|
+
} catch (err) {
|
|
128
|
+
if (err && err.name === "AbortError") {
|
|
129
|
+
throw new Error(`stt failed: request timed out after ${timeoutMs}ms`);
|
|
130
|
+
}
|
|
131
|
+
throw err;
|
|
132
|
+
} finally {
|
|
133
|
+
clearTimeout(timer);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (!r.ok) {
|
|
137
|
+
let errText = "";
|
|
138
|
+
try {
|
|
139
|
+
errText = await r.text();
|
|
140
|
+
} catch {
|
|
141
|
+
errText = "";
|
|
142
|
+
}
|
|
143
|
+
throw new Error(`stt failed: HTTP ${r.status} ${errText.slice(0, 300)}`);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
let payload;
|
|
147
|
+
try {
|
|
148
|
+
payload = await r.json();
|
|
149
|
+
} catch (err) {
|
|
150
|
+
throw new Error(`stt failed: invalid JSON response (${err && err.message})`);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const text = payload && typeof payload.text === "string" ? payload.text : undefined;
|
|
154
|
+
if (typeof text !== "string") {
|
|
155
|
+
throw new Error("stt failed: response missing text");
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return { text, raw: payload };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export default { transcribeAudioFile };
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for lib/voice/stt.mjs — speech-to-text over an OpenAI-compatible
|
|
3
|
+
* /audio/transcriptions endpoint.
|
|
4
|
+
*
|
|
5
|
+
* Network is fully stubbed: global.fetch is replaced per-test so no real
|
|
6
|
+
* HTTP call is ever made. We assert the request URL, the Authorization
|
|
7
|
+
* header, the multipart field names/values (model, language, file), the
|
|
8
|
+
* parsed result shape, default model/baseUrl, the filePath read path, and
|
|
9
|
+
* error handling on a 401 stub.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { test } from "node:test";
|
|
13
|
+
import assert from "node:assert/strict";
|
|
14
|
+
import { mkdtemp, writeFile, rm } from "node:fs/promises";
|
|
15
|
+
import { tmpdir } from "node:os";
|
|
16
|
+
import { join } from "node:path";
|
|
17
|
+
|
|
18
|
+
import { transcribeAudioFile } from "./stt.mjs";
|
|
19
|
+
|
|
20
|
+
const realFetch = global.fetch;
|
|
21
|
+
|
|
22
|
+
function jsonResponse(obj, { ok = true, status = 200 } = {}) {
|
|
23
|
+
return {
|
|
24
|
+
ok,
|
|
25
|
+
status,
|
|
26
|
+
async json() {
|
|
27
|
+
return obj;
|
|
28
|
+
},
|
|
29
|
+
async text() {
|
|
30
|
+
return JSON.stringify(obj);
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function installFetchStub(handler) {
|
|
36
|
+
const calls = [];
|
|
37
|
+
global.fetch = async (url, init) => {
|
|
38
|
+
calls.push({ url, init });
|
|
39
|
+
return handler(url, init, calls);
|
|
40
|
+
};
|
|
41
|
+
return calls;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function restoreFetch() {
|
|
45
|
+
global.fetch = realFetch;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Read multipart fields from a FormData body without making a network call.
|
|
49
|
+
async function readForm(init) {
|
|
50
|
+
assert.ok(init && init.body instanceof FormData, "body must be a FormData instance");
|
|
51
|
+
const form = init.body;
|
|
52
|
+
return {
|
|
53
|
+
model: form.get("model"),
|
|
54
|
+
language: form.get("language"),
|
|
55
|
+
prompt: form.get("prompt"),
|
|
56
|
+
file: form.get("file"),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
test("buffer path: posts multipart to default URL with auth + field names, parses result", async () => {
|
|
61
|
+
const calls = installFetchStub(() => jsonResponse({ text: "hello world", language: "en" }));
|
|
62
|
+
try {
|
|
63
|
+
const out = await transcribeAudioFile({
|
|
64
|
+
buffer: Buffer.from("FAKEAUDIOBYTES"),
|
|
65
|
+
apiKey: "sk-test-123",
|
|
66
|
+
language: "en",
|
|
67
|
+
fileName: "note.mp3",
|
|
68
|
+
mime: "audio/mpeg",
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
assert.equal(calls.length, 1, "exactly one fetch call");
|
|
72
|
+
const { url, init } = calls[0];
|
|
73
|
+
|
|
74
|
+
// URL + method
|
|
75
|
+
assert.equal(url, "https://api.openai.com/v1/audio/transcriptions");
|
|
76
|
+
assert.equal(init.method, "POST");
|
|
77
|
+
|
|
78
|
+
// Auth header
|
|
79
|
+
assert.equal(init.headers.Authorization, "Bearer sk-test-123");
|
|
80
|
+
// Caller must NOT hand-set Content-Type for FormData (fetch adds boundary).
|
|
81
|
+
assert.ok(
|
|
82
|
+
!init.headers["Content-Type"] && !init.headers["content-type"],
|
|
83
|
+
"Content-Type must not be set manually for multipart",
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
// Multipart fields
|
|
87
|
+
const fields = await readForm(init);
|
|
88
|
+
assert.equal(fields.model, "whisper-1", "default model");
|
|
89
|
+
assert.equal(fields.language, "en");
|
|
90
|
+
assert.equal(fields.prompt, null, "no prompt field when not provided");
|
|
91
|
+
assert.ok(fields.file instanceof Blob, "file part is a Blob");
|
|
92
|
+
assert.equal(fields.file.type, "audio/mpeg", "blob carries supplied mime");
|
|
93
|
+
|
|
94
|
+
// Parsed result
|
|
95
|
+
assert.equal(out.text, "hello world");
|
|
96
|
+
assert.deepEqual(out.raw, { text: "hello world", language: "en" });
|
|
97
|
+
} finally {
|
|
98
|
+
restoreFetch();
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("filePath path: reads file, derives fileName, honours baseUrl + model overrides", async () => {
|
|
103
|
+
const calls = installFetchStub(() => jsonResponse({ text: "from disk" }));
|
|
104
|
+
const dir = await mkdtemp(join(tmpdir(), "stt-test-"));
|
|
105
|
+
const fp = join(dir, "voicemail.wav");
|
|
106
|
+
await writeFile(fp, Buffer.from("RIFFfakewav"));
|
|
107
|
+
try {
|
|
108
|
+
const out = await transcribeAudioFile({
|
|
109
|
+
filePath: fp,
|
|
110
|
+
apiKey: "sk-abc",
|
|
111
|
+
model: "gpt-4o-transcribe",
|
|
112
|
+
baseUrl: "https://compat.example.com/v1/", // trailing slash should be normalized
|
|
113
|
+
prompt: "Northwind",
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const { url, init } = calls[0];
|
|
117
|
+
assert.equal(url, "https://compat.example.com/v1/audio/transcriptions", "trailing slash trimmed");
|
|
118
|
+
|
|
119
|
+
const fields = await readForm(init);
|
|
120
|
+
assert.equal(fields.model, "gpt-4o-transcribe");
|
|
121
|
+
assert.equal(fields.prompt, "Northwind");
|
|
122
|
+
assert.ok(fields.file instanceof Blob);
|
|
123
|
+
// fileName defaults to the basename of filePath; assert via the part name.
|
|
124
|
+
assert.equal(fields.file.name, "voicemail.wav", "fileName derived from filePath basename");
|
|
125
|
+
|
|
126
|
+
assert.equal(out.text, "from disk");
|
|
127
|
+
} finally {
|
|
128
|
+
restoreFetch();
|
|
129
|
+
await rm(dir, { recursive: true, force: true });
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("apiKey falls back to process.env.OPENAI_API_KEY", async () => {
|
|
134
|
+
const calls = installFetchStub(() => jsonResponse({ text: "env key works" }));
|
|
135
|
+
const prev = process.env.OPENAI_API_KEY;
|
|
136
|
+
process.env.OPENAI_API_KEY = "sk-from-env";
|
|
137
|
+
try {
|
|
138
|
+
const out = await transcribeAudioFile({ buffer: Buffer.from("x") });
|
|
139
|
+
assert.equal(calls[0].init.headers.Authorization, "Bearer sk-from-env");
|
|
140
|
+
assert.equal(out.text, "env key works");
|
|
141
|
+
} finally {
|
|
142
|
+
restoreFetch();
|
|
143
|
+
if (prev === undefined) delete process.env.OPENAI_API_KEY;
|
|
144
|
+
else process.env.OPENAI_API_KEY = prev;
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test("throws clear error when no apiKey available", async () => {
|
|
149
|
+
const prev = process.env.OPENAI_API_KEY;
|
|
150
|
+
delete process.env.OPENAI_API_KEY;
|
|
151
|
+
// fetch must never be called.
|
|
152
|
+
const calls = installFetchStub(() => {
|
|
153
|
+
throw new Error("fetch should not be called");
|
|
154
|
+
});
|
|
155
|
+
try {
|
|
156
|
+
await assert.rejects(
|
|
157
|
+
() => transcribeAudioFile({ buffer: Buffer.from("x") }),
|
|
158
|
+
/OPENAI_API_KEY not set/,
|
|
159
|
+
);
|
|
160
|
+
assert.equal(calls.length, 0, "no network call without a key");
|
|
161
|
+
} finally {
|
|
162
|
+
restoreFetch();
|
|
163
|
+
if (prev !== undefined) process.env.OPENAI_API_KEY = prev;
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test("throws when neither filePath nor buffer is provided", async () => {
|
|
168
|
+
const calls = installFetchStub(() => jsonResponse({ text: "nope" }));
|
|
169
|
+
try {
|
|
170
|
+
await assert.rejects(
|
|
171
|
+
() => transcribeAudioFile({ apiKey: "sk-x" }),
|
|
172
|
+
/provide either filePath or buffer/,
|
|
173
|
+
);
|
|
174
|
+
assert.equal(calls.length, 0);
|
|
175
|
+
} finally {
|
|
176
|
+
restoreFetch();
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test("error handling: non-2xx (401) surfaces status + body", async () => {
|
|
181
|
+
installFetchStub(() =>
|
|
182
|
+
jsonResponse({ error: { message: "Invalid API key" } }, { ok: false, status: 401 }),
|
|
183
|
+
);
|
|
184
|
+
try {
|
|
185
|
+
await assert.rejects(
|
|
186
|
+
() => transcribeAudioFile({ buffer: Buffer.from("x"), apiKey: "sk-bad" }),
|
|
187
|
+
(err) => {
|
|
188
|
+
assert.match(err.message, /stt failed: HTTP 401/);
|
|
189
|
+
assert.match(err.message, /Invalid API key/);
|
|
190
|
+
return true;
|
|
191
|
+
},
|
|
192
|
+
);
|
|
193
|
+
} finally {
|
|
194
|
+
restoreFetch();
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test("error handling: 2xx but missing text field", async () => {
|
|
199
|
+
installFetchStub(() => jsonResponse({ language: "en" }));
|
|
200
|
+
try {
|
|
201
|
+
await assert.rejects(
|
|
202
|
+
() => transcribeAudioFile({ buffer: Buffer.from("x"), apiKey: "sk-x" }),
|
|
203
|
+
/response missing text/,
|
|
204
|
+
);
|
|
205
|
+
} finally {
|
|
206
|
+
restoreFetch();
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test("timeout: AbortError is reported as a timeout", async () => {
|
|
211
|
+
global.fetch = async (_url, init) => {
|
|
212
|
+
// Simulate the runtime aborting the request.
|
|
213
|
+
const err = new Error("aborted");
|
|
214
|
+
err.name = "AbortError";
|
|
215
|
+
void init;
|
|
216
|
+
throw err;
|
|
217
|
+
};
|
|
218
|
+
try {
|
|
219
|
+
await assert.rejects(
|
|
220
|
+
() => transcribeAudioFile({ buffer: Buffer.from("x"), apiKey: "sk-x", timeoutMs: 5 }),
|
|
221
|
+
/request timed out after 5ms/,
|
|
222
|
+
);
|
|
223
|
+
} finally {
|
|
224
|
+
restoreFetch();
|
|
225
|
+
}
|
|
226
|
+
});
|