@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,1229 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Agent Huddle — Main Server
|
|
4
|
+
*
|
|
5
|
+
* Orchestrates Slack huddle voice participation by coordinating:
|
|
6
|
+
* - HuddleController (CDP-based Slack UI automation)
|
|
7
|
+
* - AudioBridge (BlackHole capture → Deepgram STT → ElevenLabs TTS → BlackHole playback)
|
|
8
|
+
* - Claude API (conversation intelligence, reused from voice-ai)
|
|
9
|
+
*
|
|
10
|
+
* Lifecycle:
|
|
11
|
+
* 1. Connects to Slack desktop app via CDP
|
|
12
|
+
* 2. Listens for huddle invitations (Events API + DOM polling)
|
|
13
|
+
* 3. On invitation or initiate command → joins huddle
|
|
14
|
+
* 4. Captures audio → transcribes → Claude generates response → TTS → speaks
|
|
15
|
+
* 5. On huddle end → saves transcript, routes action items
|
|
16
|
+
*
|
|
17
|
+
* Usage:
|
|
18
|
+
* node huddle-server.mjs # Start and listen for huddles
|
|
19
|
+
* node huddle-server.mjs --join #general # Join huddle in #general
|
|
20
|
+
* node huddle-server.mjs --call principal # Initiate huddle with the agent's principal
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import dotenv from "dotenv";
|
|
24
|
+
import { fileURLToPath as _fu } from "node:url";
|
|
25
|
+
import { dirname as _dn, join as _jn } from "node:path";
|
|
26
|
+
|
|
27
|
+
// Load .env from agent repo root (where all API keys live), then huddle-specific .env
|
|
28
|
+
const _d = _dn(_fu(import.meta.url));
|
|
29
|
+
dotenv.config({ path: _jn(_d, "../..", ".env") });
|
|
30
|
+
dotenv.config({ path: _jn(_d, ".env"), override: true });
|
|
31
|
+
import { EventEmitter } from "node:events";
|
|
32
|
+
import { readFileSync, appendFileSync, mkdirSync, existsSync } from "node:fs";
|
|
33
|
+
import { join, dirname } from "node:path";
|
|
34
|
+
import { fileURLToPath } from "node:url";
|
|
35
|
+
import { createServer } from "node:http";
|
|
36
|
+
import { execFile as execFileCb } from "node:child_process";
|
|
37
|
+
import { promisify } from "node:util";
|
|
38
|
+
import Anthropic from "@anthropic-ai/sdk";
|
|
39
|
+
|
|
40
|
+
const execFileAsync = promisify(execFileCb);
|
|
41
|
+
import { HuddleController } from "./huddle-controller.mjs";
|
|
42
|
+
import { AudioBridge } from "./audio-bridge.mjs";
|
|
43
|
+
import { OpenAIRealtimeHuddleBridge } from "./openai-realtime-bridge.mjs";
|
|
44
|
+
|
|
45
|
+
// Brain selection. Default is "openai-realtime" — the same single-model
|
|
46
|
+
// speech-to-speech path that drives PSTN. The legacy Deepgram+Claude+11Labs
|
|
47
|
+
// pipeline is still selectable via HUDDLE_BRAIN=legacy for fallback.
|
|
48
|
+
const HUDDLE_BRAIN = (process.env.HUDDLE_BRAIN || "openai-realtime").toLowerCase();
|
|
49
|
+
|
|
50
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
51
|
+
const AGENT_REPO_DIR = process.env.AGENT_DIR || join(__dirname, "../..");
|
|
52
|
+
const VOICE_AI_ROOT = process.env.VOICE_AI_ROOT || join(process.env.HOME || "", "voice-ai");
|
|
53
|
+
|
|
54
|
+
// Load agent identity for transcript speaker tags + display names.
|
|
55
|
+
let _agent = null;
|
|
56
|
+
function loadAgent() {
|
|
57
|
+
if (_agent) return _agent;
|
|
58
|
+
try {
|
|
59
|
+
_agent = JSON.parse(readFileSync(join(AGENT_REPO_DIR, "config/agent.json"), "utf-8"));
|
|
60
|
+
} catch {
|
|
61
|
+
_agent = { firstName: "Agent" };
|
|
62
|
+
}
|
|
63
|
+
return _agent;
|
|
64
|
+
}
|
|
65
|
+
const AGENT_SLUG = (loadAgent().firstName || "agent").toLowerCase();
|
|
66
|
+
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
// Configuration
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
const MAX_TOKENS = 300;
|
|
72
|
+
const MAX_TOOL_ROUNDS = 3;
|
|
73
|
+
const HUDDLE_EVENTS_PORT = parseInt(process.env.HUDDLE_EVENTS_PORT || "3200", 10);
|
|
74
|
+
|
|
75
|
+
// Use Claude Code CLI (Max subscription) instead of API calls.
|
|
76
|
+
// Falls back to API if claude CLI is not available.
|
|
77
|
+
const USE_CLAUDE_CLI = process.env.HUDDLE_USE_API !== "1";
|
|
78
|
+
const anthropic = USE_CLAUDE_CLI ? null : new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });
|
|
79
|
+
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
// Maestro action executor + tool definitions (colocated under lib/ since
|
|
82
|
+
// 1.12.19). Falls back to the legacy ~/voice-ai install for old setups.
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
|
|
85
|
+
let executeAction = null;
|
|
86
|
+
let getToolsForAccessLevel = null;
|
|
87
|
+
let getToolNamesForAccessLevel = null;
|
|
88
|
+
|
|
89
|
+
const _maestroExecutorCandidates = [
|
|
90
|
+
join(__dirname, "../../lib/action-executor.js"),
|
|
91
|
+
join(__dirname, "../../lib/action-executor.mjs"),
|
|
92
|
+
join(VOICE_AI_ROOT, "lib/action-executor.js"),
|
|
93
|
+
];
|
|
94
|
+
const _maestroToolsCandidates = [
|
|
95
|
+
join(__dirname, "../../lib/tool-definitions.js"),
|
|
96
|
+
join(__dirname, "../../lib/tool-definitions.mjs"),
|
|
97
|
+
join(VOICE_AI_ROOT, "lib/tool-definitions.js"),
|
|
98
|
+
];
|
|
99
|
+
|
|
100
|
+
for (const candidate of _maestroExecutorCandidates) {
|
|
101
|
+
if (!existsSync(candidate)) continue;
|
|
102
|
+
try {
|
|
103
|
+
const mod = await import(candidate);
|
|
104
|
+
executeAction = mod.executeAction;
|
|
105
|
+
if (executeAction) break;
|
|
106
|
+
} catch { /* try next */ }
|
|
107
|
+
}
|
|
108
|
+
for (const candidate of _maestroToolsCandidates) {
|
|
109
|
+
if (!existsSync(candidate)) continue;
|
|
110
|
+
try {
|
|
111
|
+
const mod = await import(candidate);
|
|
112
|
+
getToolsForAccessLevel = mod.getToolsForAccessLevel;
|
|
113
|
+
getToolNamesForAccessLevel = mod.getToolNamesForAccessLevel;
|
|
114
|
+
if (getToolsForAccessLevel) break;
|
|
115
|
+
} catch { /* try next */ }
|
|
116
|
+
}
|
|
117
|
+
if (executeAction && getToolsForAccessLevel) {
|
|
118
|
+
console.log("[INIT] action executor + tool definitions loaded");
|
|
119
|
+
} else {
|
|
120
|
+
console.warn("[INIT] action executor / tool definitions not available — huddle will be read-only");
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ---------------------------------------------------------------------------
|
|
124
|
+
// Huddle Session State
|
|
125
|
+
// ---------------------------------------------------------------------------
|
|
126
|
+
|
|
127
|
+
/** @enum {string} */
|
|
128
|
+
const HuddleState = {
|
|
129
|
+
IDLE: "idle",
|
|
130
|
+
JOINING: "joining",
|
|
131
|
+
ACTIVE: "active",
|
|
132
|
+
LEAVING: "leaving",
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* HuddleServer is the top-level orchestrator for the agent's huddle participation.
|
|
137
|
+
*/
|
|
138
|
+
class HuddleServer extends EventEmitter {
|
|
139
|
+
constructor() {
|
|
140
|
+
super();
|
|
141
|
+
this.controller = new HuddleController();
|
|
142
|
+
// Brain selection: openai-realtime (default) is the single-model
|
|
143
|
+
// speech-to-speech path identical to PSTN inbound; legacy retains the
|
|
144
|
+
// older Deepgram+Claude+ElevenLabs three-stage pipeline.
|
|
145
|
+
if (HUDDLE_BRAIN === "openai-realtime") {
|
|
146
|
+
this.bridge = new OpenAIRealtimeHuddleBridge({
|
|
147
|
+
apiKey: process.env.OPENAI_API_KEY,
|
|
148
|
+
agentRoot: AGENT_REPO_DIR,
|
|
149
|
+
// tools + executor wired lazily in start() so we don't pull
|
|
150
|
+
// action-executor at import time.
|
|
151
|
+
});
|
|
152
|
+
this._brainKind = "openai-realtime";
|
|
153
|
+
} else {
|
|
154
|
+
// Pass API keys explicitly because ESM import hoisting evaluates
|
|
155
|
+
// audio-bridge.mjs before dotenv.config() runs in this file.
|
|
156
|
+
this.bridge = new AudioBridge({
|
|
157
|
+
deepgramKey: process.env.DEEPGRAM_API_KEY,
|
|
158
|
+
elevenLabsKey: process.env.ELEVENLABS_API_KEY,
|
|
159
|
+
});
|
|
160
|
+
this._brainKind = "legacy";
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
this.state = HuddleState.IDLE;
|
|
164
|
+
this.session = null; // Active huddle session
|
|
165
|
+
this.autoJoin = true; // Auto-join incoming huddle invitations
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// -------------------------------------------------------------------------
|
|
169
|
+
// Startup
|
|
170
|
+
// -------------------------------------------------------------------------
|
|
171
|
+
|
|
172
|
+
async start() {
|
|
173
|
+
console.log("\n==========================================");
|
|
174
|
+
console.log(` ${loadAgent().firstName || "Agent"} Huddle Server`);
|
|
175
|
+
console.log(" " + new Date().toISOString());
|
|
176
|
+
console.log("==========================================\n");
|
|
177
|
+
|
|
178
|
+
// Verify audio devices
|
|
179
|
+
const devices = await this.bridge.checkDevices();
|
|
180
|
+
if (!devices.capture || !devices.playback) {
|
|
181
|
+
console.error("[INIT] Audio devices missing:");
|
|
182
|
+
console.error(` Capture (BlackHole 2ch): ${devices.capture ? "OK" : "MISSING"}`);
|
|
183
|
+
console.error(` Playback (BlackHole 16ch): ${devices.playback ? "OK" : "MISSING"}`);
|
|
184
|
+
console.error("Run: ./setup-audio.sh to install");
|
|
185
|
+
process.exit(1);
|
|
186
|
+
}
|
|
187
|
+
console.log("[INIT] Audio devices verified");
|
|
188
|
+
console.log(`[INIT] LLM mode: ${USE_CLAUDE_CLI ? "Claude Code CLI (Max subscription)" : "Anthropic API (pay-per-token)"}`);
|
|
189
|
+
|
|
190
|
+
// Connect to Slack via CDP
|
|
191
|
+
this._wireControllerEvents();
|
|
192
|
+
const connected = await this.controller.connect();
|
|
193
|
+
if (!connected) {
|
|
194
|
+
console.error("[INIT] Failed to connect to Slack via CDP");
|
|
195
|
+
console.error("Run: ./launch-slack.sh to start Slack with CDP enabled");
|
|
196
|
+
process.exit(1);
|
|
197
|
+
}
|
|
198
|
+
console.log("[INIT] Connected to Slack via CDP");
|
|
199
|
+
|
|
200
|
+
// Wire audio bridge events
|
|
201
|
+
this._wireBridgeEvents();
|
|
202
|
+
|
|
203
|
+
// Start Events API listener for reliable huddle detection
|
|
204
|
+
this._startEventsListener();
|
|
205
|
+
|
|
206
|
+
console.log("[INIT] Huddle server ready — listening for invitations\n");
|
|
207
|
+
|
|
208
|
+
// Handle CLI arguments
|
|
209
|
+
const args = process.argv.slice(2);
|
|
210
|
+
if (args[0] === "--join" && args[1]) {
|
|
211
|
+
await this.joinHuddle(args[1]);
|
|
212
|
+
} else if (args[0] === "--call" && args[1]) {
|
|
213
|
+
await this.initiateHuddle(args[1]);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// -------------------------------------------------------------------------
|
|
218
|
+
// Huddle lifecycle
|
|
219
|
+
// -------------------------------------------------------------------------
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Join an existing huddle in a channel or DM.
|
|
223
|
+
* @param {string} channel - Channel name or person name
|
|
224
|
+
*/
|
|
225
|
+
async joinHuddle(channel) {
|
|
226
|
+
if (this.state !== HuddleState.IDLE) {
|
|
227
|
+
console.log(`[HUDDLE] Cannot join — currently ${this.state}`);
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
this.state = HuddleState.JOINING;
|
|
232
|
+
console.log(`[HUDDLE] Joining huddle in: ${channel}`);
|
|
233
|
+
|
|
234
|
+
try {
|
|
235
|
+
const joined = await this.controller.joinHuddle(channel);
|
|
236
|
+
if (!joined) {
|
|
237
|
+
console.error("[HUDDLE] Failed to join huddle");
|
|
238
|
+
this.state = HuddleState.IDLE;
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
await this._startSession(channel, "joined");
|
|
243
|
+
} catch (err) {
|
|
244
|
+
console.error(`[HUDDLE] Join error: ${err.message}`);
|
|
245
|
+
this.state = HuddleState.IDLE;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Initiate a new huddle with a person or in a channel.
|
|
251
|
+
* @param {string} target - Channel name or person name
|
|
252
|
+
*/
|
|
253
|
+
async initiateHuddle(target) {
|
|
254
|
+
if (this.state !== HuddleState.IDLE) {
|
|
255
|
+
console.log(`[HUDDLE] Cannot initiate — currently ${this.state}`);
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
this.state = HuddleState.JOINING;
|
|
260
|
+
console.log(`[HUDDLE] Initiating huddle with: ${target}`);
|
|
261
|
+
|
|
262
|
+
try {
|
|
263
|
+
const started = await this.controller.initiateHuddle(target);
|
|
264
|
+
if (!started) {
|
|
265
|
+
console.error("[HUDDLE] Failed to start huddle");
|
|
266
|
+
this.state = HuddleState.IDLE;
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
await this._startSession(target, "initiated");
|
|
271
|
+
} catch (err) {
|
|
272
|
+
console.error(`[HUDDLE] Initiate error: ${err.message}`);
|
|
273
|
+
this.state = HuddleState.IDLE;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Leave the current huddle.
|
|
279
|
+
*/
|
|
280
|
+
async leaveHuddle() {
|
|
281
|
+
if (this.state !== HuddleState.ACTIVE) return;
|
|
282
|
+
|
|
283
|
+
this.state = HuddleState.LEAVING;
|
|
284
|
+
console.log("[HUDDLE] Leaving huddle...");
|
|
285
|
+
|
|
286
|
+
try {
|
|
287
|
+
await this.controller.leaveHuddle();
|
|
288
|
+
} catch (err) {
|
|
289
|
+
console.error(`[HUDDLE] Leave error: ${err.message}`);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
await this._endSession();
|
|
293
|
+
this.state = HuddleState.IDLE;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// -------------------------------------------------------------------------
|
|
297
|
+
// Session management
|
|
298
|
+
// -------------------------------------------------------------------------
|
|
299
|
+
|
|
300
|
+
async _startSession(channel, mode, initiator = null) {
|
|
301
|
+
// Guard: don't start a duplicate session
|
|
302
|
+
if (this.state === HuddleState.ACTIVE && this.session) {
|
|
303
|
+
console.log(`[HUDDLE] Session already active — ignoring duplicate start`);
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
this.state = HuddleState.ACTIVE;
|
|
307
|
+
|
|
308
|
+
// Resolve who initiated the huddle for access control
|
|
309
|
+
const callerInfo = this._resolveHuddleParticipant(initiator || channel);
|
|
310
|
+
|
|
311
|
+
this.session = {
|
|
312
|
+
channel,
|
|
313
|
+
mode, // "joined" or "initiated"
|
|
314
|
+
startedAt: new Date().toISOString(),
|
|
315
|
+
messages: [],
|
|
316
|
+
transcript: [],
|
|
317
|
+
participants: [],
|
|
318
|
+
accessLevel: callerInfo.accessLevel,
|
|
319
|
+
callerSlug: callerInfo.slug,
|
|
320
|
+
callerName: callerInfo.name,
|
|
321
|
+
threadPollTimer: null, // Huddle thread message polling
|
|
322
|
+
lastThreadTs: null, // Last seen thread message timestamp
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
// Load live context from the agent repo for Claude
|
|
326
|
+
this.session.liveContext = this._loadLiveContext();
|
|
327
|
+
|
|
328
|
+
// In webaudio capture mode (legacy bridge only), set Slack's speaker to
|
|
329
|
+
// physical output (eliminates AEC echo). Audio is captured directly from
|
|
330
|
+
// the WebRTC stream via CDP, so BlackHole 2ch is not needed for the
|
|
331
|
+
// speaker path.
|
|
332
|
+
if (this.bridge.captureMode === "webaudio") {
|
|
333
|
+
await this._setSlackSpeakerToDefault();
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Wire the openai-realtime brain with tools + executor + per-call
|
|
337
|
+
// identity (channel, peer, brief). Done here rather than at bridge
|
|
338
|
+
// construction so each new huddle session can pull fresh context.
|
|
339
|
+
if (this._brainKind === "openai-realtime") {
|
|
340
|
+
await this._configureRealtimeBrain(channel, mode);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Start audio bridge
|
|
344
|
+
await this.bridge.start();
|
|
345
|
+
|
|
346
|
+
console.log(`[HUDDLE] Session active in ${channel} (${mode})`);
|
|
347
|
+
this._logEvent("session-started", { channel, mode });
|
|
348
|
+
|
|
349
|
+
// Speak a greeting
|
|
350
|
+
const greeting = mode === "initiated"
|
|
351
|
+
? `Hey, do you have a moment?`
|
|
352
|
+
: `Hey, what's up?`;
|
|
353
|
+
|
|
354
|
+
await this.bridge.speak(greeting);
|
|
355
|
+
if (this.session) {
|
|
356
|
+
this.session.messages.push({ role: "assistant", content: greeting });
|
|
357
|
+
this.session.transcript.push({ speaker: AGENT_SLUG, text: greeting, ts: Date.now() });
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Start polling the huddle thread for text messages
|
|
361
|
+
this._startThreadPolling();
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
async _endSession() {
|
|
365
|
+
if (!this.session) return;
|
|
366
|
+
|
|
367
|
+
// Stop thread polling
|
|
368
|
+
if (this.session.threadPollTimer) {
|
|
369
|
+
clearInterval(this.session.threadPollTimer);
|
|
370
|
+
this.session.threadPollTimer = null;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Stop audio bridge
|
|
374
|
+
await this.bridge.stop();
|
|
375
|
+
|
|
376
|
+
// Save transcript
|
|
377
|
+
this._saveTranscript();
|
|
378
|
+
|
|
379
|
+
// Log session end
|
|
380
|
+
const duration = Math.round((Date.now() - new Date(this.session.startedAt).getTime()) / 1000);
|
|
381
|
+
const turns = this.session.messages.length;
|
|
382
|
+
console.log(`[HUDDLE] Session ended — ${duration}s, ${turns} turns`);
|
|
383
|
+
this._logEvent("session-ended", {
|
|
384
|
+
channel: this.session.channel,
|
|
385
|
+
duration,
|
|
386
|
+
turns,
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
this.session = null;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// -------------------------------------------------------------------------
|
|
393
|
+
// Conversation loop (adapted from voice-ai/lib/claude.js)
|
|
394
|
+
// -------------------------------------------------------------------------
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Configure the OpenAI Realtime bridge before start() — loads tools
|
|
398
|
+
* appropriate for voice + wires the action executor so the model can
|
|
399
|
+
* place follow-ups, send Slack messages, query RAG, etc. mid-huddle.
|
|
400
|
+
* The bridge itself loads identity + queues + team + RAG via
|
|
401
|
+
* lib/voice/context-loader.mjs at start().
|
|
402
|
+
*/
|
|
403
|
+
async _configureRealtimeBrain(channel, mode) {
|
|
404
|
+
try {
|
|
405
|
+
const [{ actionsToRealtimeTools }, { executeAction }] = await Promise.all([
|
|
406
|
+
import("../../lib/voice/tool-bridge.mjs"),
|
|
407
|
+
import("../../lib/action-executor.js"),
|
|
408
|
+
]);
|
|
409
|
+
const tools = await actionsToRealtimeTools({
|
|
410
|
+
accessLevel: "voice",
|
|
411
|
+
agentRoot: AGENT_REPO_DIR,
|
|
412
|
+
});
|
|
413
|
+
this.bridge.tools = tools;
|
|
414
|
+
this.bridge.peer = channel || null;
|
|
415
|
+
this.bridge.brief = mode === "initiated"
|
|
416
|
+
? "You're initiating a casual huddle. Open with a quick check-in."
|
|
417
|
+
: `You've been invited to a huddle in ${channel}. Listen, then respond naturally.`;
|
|
418
|
+
this.bridge.executeTool = async ({ name, arguments: args, call_id }) => {
|
|
419
|
+
try {
|
|
420
|
+
const result = await executeAction(
|
|
421
|
+
name,
|
|
422
|
+
args,
|
|
423
|
+
{ accessLevel: "voice", source: "huddle", channelId: channel },
|
|
424
|
+
`huddle-${this.session?.id || Date.now()}`,
|
|
425
|
+
{ agentRoot: AGENT_REPO_DIR },
|
|
426
|
+
);
|
|
427
|
+
this._logEvent("tool-executed", { name, call_id, success: result?.success });
|
|
428
|
+
return typeof result === "string" ? result : JSON.stringify(result || {});
|
|
429
|
+
} catch (err) {
|
|
430
|
+
this._logEvent("tool-failed", { name, call_id, error: err.message });
|
|
431
|
+
return JSON.stringify({ ok: false, error: err.message });
|
|
432
|
+
}
|
|
433
|
+
};
|
|
434
|
+
} catch (err) {
|
|
435
|
+
console.warn(`[HUDDLE] failed to wire realtime tools: ${err.message}`);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Process a transcript from the huddle and generate the agent's response.
|
|
441
|
+
*
|
|
442
|
+
* Only used by the legacy Deepgram+Claude+11Labs brain. The openai-realtime
|
|
443
|
+
* brain handles speech-in → speech-out internally; this handler is a no-op
|
|
444
|
+
* for it (the bridge emits 'transcript' events for logging only).
|
|
445
|
+
*
|
|
446
|
+
* @param {string} userText - What someone said in the huddle
|
|
447
|
+
*/
|
|
448
|
+
async _handleTranscript(userText) {
|
|
449
|
+
if (!this.session || this.state !== HuddleState.ACTIVE) return;
|
|
450
|
+
if (!userText || !userText.trim()) return;
|
|
451
|
+
|
|
452
|
+
// openai-realtime: just append to transcript, no Claude round-trip
|
|
453
|
+
if (this._brainKind === "openai-realtime") {
|
|
454
|
+
this.session.transcript.push({ speaker: "participant", text: userText, ts: Date.now() });
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
console.log(`[SPEECH] "${userText}"`);
|
|
459
|
+
|
|
460
|
+
// Add to conversation history
|
|
461
|
+
this.session.messages.push({ role: "user", content: userText });
|
|
462
|
+
this.session.transcript.push({ speaker: "participant", text: userText, ts: Date.now() });
|
|
463
|
+
|
|
464
|
+
try {
|
|
465
|
+
// Get Claude's response
|
|
466
|
+
const responseText = await this._getClaudeResponse(this.session.messages);
|
|
467
|
+
|
|
468
|
+
if (!responseText || !responseText.trim()) return;
|
|
469
|
+
|
|
470
|
+
console.log(`[AGENT] "${responseText}"`);
|
|
471
|
+
|
|
472
|
+
// Check for end signal
|
|
473
|
+
const shouldEnd = responseText.includes("END_HUDDLE");
|
|
474
|
+
const cleanResponse = responseText.replace(/\s*END_HUDDLE\s*/g, "").trim();
|
|
475
|
+
|
|
476
|
+
// Add to history
|
|
477
|
+
this.session.messages.push({ role: "assistant", content: cleanResponse });
|
|
478
|
+
this.session.transcript.push({ speaker: AGENT_SLUG, text: cleanResponse, ts: Date.now() });
|
|
479
|
+
|
|
480
|
+
// Speak the response via TTS → BlackHole 16ch → Slack mic
|
|
481
|
+
await this.bridge.speak(cleanResponse);
|
|
482
|
+
|
|
483
|
+
if (shouldEnd) {
|
|
484
|
+
// Wait for TTS to finish, then leave
|
|
485
|
+
setTimeout(() => this.leaveHuddle(), 2000);
|
|
486
|
+
}
|
|
487
|
+
} catch (err) {
|
|
488
|
+
console.error(`[CLAUDE] Error: ${err.message}`);
|
|
489
|
+
|
|
490
|
+
const fallback = "Sorry, give me one second — I'm having a brief technical hiccup.";
|
|
491
|
+
await this.bridge.speak(fallback);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// -------------------------------------------------------------------------
|
|
496
|
+
// Huddle thread text message monitoring
|
|
497
|
+
// -------------------------------------------------------------------------
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* Start polling the huddle thread for text messages via the Slack API.
|
|
501
|
+
* Text messages in the huddle thread are injected into the conversation
|
|
502
|
+
* so the agent can respond to both voice and text.
|
|
503
|
+
*/
|
|
504
|
+
_startThreadPolling() {
|
|
505
|
+
const slackToken = process.env.SLACK_USER_TOKEN || process.env.SLACK_BOT_TOKEN;
|
|
506
|
+
if (!slackToken) {
|
|
507
|
+
console.log("[THREAD] No Slack token — thread monitoring disabled");
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// Determine the huddle channel ID from the current Slack page
|
|
512
|
+
this._resolveHuddleChannelId().then((channelId) => {
|
|
513
|
+
if (!channelId) {
|
|
514
|
+
console.log("[THREAD] Could not resolve huddle channel — thread monitoring disabled");
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
console.log(`[THREAD] Monitoring huddle thread in ${channelId}`);
|
|
519
|
+
this.session.lastThreadTs = String(Date.now() / 1000); // Start from now
|
|
520
|
+
|
|
521
|
+
this.session.threadPollTimer = setInterval(async () => {
|
|
522
|
+
try {
|
|
523
|
+
await this._pollThreadMessages(channelId, slackToken);
|
|
524
|
+
} catch (err) {
|
|
525
|
+
console.error(`[THREAD] Poll error: ${err.message}`);
|
|
526
|
+
}
|
|
527
|
+
}, 5000); // Poll every 5 seconds
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* Resolve the current huddle's channel ID from the Slack page via CDP.
|
|
533
|
+
* @returns {Promise<string|null>}
|
|
534
|
+
*/
|
|
535
|
+
async _resolveHuddleChannelId() {
|
|
536
|
+
try {
|
|
537
|
+
const targets = await fetch("http://localhost:9222/json").then((r) => r.json());
|
|
538
|
+
const mainTarget = targets.find(
|
|
539
|
+
(t) => t.type === "page" && t.url?.includes("app.slack.com/client")
|
|
540
|
+
);
|
|
541
|
+
if (!mainTarget) return null;
|
|
542
|
+
|
|
543
|
+
// Extract channel ID from the URL: /client/TEAM_ID/CHANNEL_ID
|
|
544
|
+
const match = mainTarget.url.match(/\/client\/[A-Z0-9]+\/([A-Z0-9]+)/);
|
|
545
|
+
return match ? match[1] : null;
|
|
546
|
+
} catch {
|
|
547
|
+
return null;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
* Poll the Slack channel for recent messages in the huddle thread.
|
|
553
|
+
* Only processes text messages from other users (not the agent's bot).
|
|
554
|
+
*/
|
|
555
|
+
async _pollThreadMessages(channelId, token) {
|
|
556
|
+
if (!this.session) return;
|
|
557
|
+
|
|
558
|
+
const oldest = this.session.lastThreadTs || "0";
|
|
559
|
+
const url = `https://slack.com/api/conversations.history?channel=${channelId}&oldest=${oldest}&limit=10`;
|
|
560
|
+
|
|
561
|
+
const resp = await fetch(url, {
|
|
562
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
563
|
+
});
|
|
564
|
+
const data = await resp.json();
|
|
565
|
+
if (!data.ok || !data.messages || data.messages.length === 0) return;
|
|
566
|
+
|
|
567
|
+
// The agent's Slack user ID (skip its own messages). Hardcoded fallback
|
|
568
|
+
// removed — agents must set SLACK_BOT_USER_ID in .env if they want this
|
|
569
|
+
// filter active, otherwise the agent's slackMemberId from agent.json is used.
|
|
570
|
+
const agentUserId = process.env.SLACK_BOT_USER_ID || loadAgent().slackMemberId || "";
|
|
571
|
+
|
|
572
|
+
for (const msg of data.messages.reverse()) { // oldest first
|
|
573
|
+
// Skip bot messages, the agent's own messages, and non-text messages
|
|
574
|
+
if (agentUserId && msg.user === agentUserId) continue;
|
|
575
|
+
if (msg.subtype) continue; // Skip system messages
|
|
576
|
+
if (!msg.text || !msg.text.trim()) continue;
|
|
577
|
+
|
|
578
|
+
// Update last seen timestamp
|
|
579
|
+
this.session.lastThreadTs = msg.ts;
|
|
580
|
+
|
|
581
|
+
// Handle the text message like a voice transcript
|
|
582
|
+
const senderName = msg.user_profile?.display_name || msg.user || "someone";
|
|
583
|
+
const textContent = `[Text message from ${senderName}]: ${msg.text}`;
|
|
584
|
+
|
|
585
|
+
console.log(`[THREAD] ${textContent}`);
|
|
586
|
+
this.session.transcript.push({ speaker: "text", text: textContent, ts: Date.now() });
|
|
587
|
+
|
|
588
|
+
// Add to conversation and generate response
|
|
589
|
+
await this._handleTranscript(textContent);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
/**
|
|
594
|
+
* Get a response from Claude given the conversation history.
|
|
595
|
+
* Uses Claude Code CLI (Max subscription) by default, falling back
|
|
596
|
+
* to the Anthropic API if CLI is unavailable or HUDDLE_USE_API=1.
|
|
597
|
+
*
|
|
598
|
+
* @param {Array} messages - Conversation history [{role, content}]
|
|
599
|
+
* @returns {Promise<string>} Response text
|
|
600
|
+
*/
|
|
601
|
+
async _getClaudeResponse(messages) {
|
|
602
|
+
if (USE_CLAUDE_CLI) {
|
|
603
|
+
return this._getClaudeResponseCLI(messages);
|
|
604
|
+
}
|
|
605
|
+
return this._getClaudeResponseAPI(messages);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
/**
|
|
609
|
+
* Get a response via Claude Code CLI (uses Max subscription).
|
|
610
|
+
* Constructs a prompt from system + conversation history and pipes
|
|
611
|
+
* it through `claude --print`.
|
|
612
|
+
*/
|
|
613
|
+
async _getClaudeResponseCLI(messages) {
|
|
614
|
+
const systemPrompt = this._buildSystemPrompt();
|
|
615
|
+
|
|
616
|
+
// Build a single prompt from system + conversation history
|
|
617
|
+
const parts = [`[System]\n${systemPrompt}\n`];
|
|
618
|
+
for (const msg of messages) {
|
|
619
|
+
const role = msg.role === "assistant" ? (loadAgent().firstName || "Agent") : "Participant";
|
|
620
|
+
const content = typeof msg.content === "string"
|
|
621
|
+
? msg.content
|
|
622
|
+
: msg.content?.map((b) => b.text || "").join("") || "";
|
|
623
|
+
if (content.trim()) {
|
|
624
|
+
parts.push(`[${role}]\n${content}`);
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
parts.push(`[${loadAgent().firstName || "Agent"}]`);
|
|
628
|
+
|
|
629
|
+
const prompt = parts.join("\n\n");
|
|
630
|
+
|
|
631
|
+
const { stdout } = await execFileAsync("claude", [
|
|
632
|
+
"--print",
|
|
633
|
+
"--model", "sonnet",
|
|
634
|
+
"-p", prompt,
|
|
635
|
+
], {
|
|
636
|
+
timeout: 30000,
|
|
637
|
+
maxBuffer: 1024 * 1024,
|
|
638
|
+
env: { ...process.env, TERM: "dumb" },
|
|
639
|
+
});
|
|
640
|
+
|
|
641
|
+
return (stdout || "").trim();
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
/**
|
|
645
|
+
* Get a response via Anthropic API (pay-per-token fallback).
|
|
646
|
+
* Supports tool use with multi-round execution.
|
|
647
|
+
*/
|
|
648
|
+
async _getClaudeResponseAPI(messages) {
|
|
649
|
+
const systemPrompt = this._buildSystemPrompt();
|
|
650
|
+
const accessLevel = this.session?.accessLevel || "default";
|
|
651
|
+
const tools = getToolsForAccessLevel ? getToolsForAccessLevel(accessLevel) : [];
|
|
652
|
+
|
|
653
|
+
const apiParams = {
|
|
654
|
+
model: "claude-sonnet-4-20250514",
|
|
655
|
+
max_tokens: MAX_TOKENS,
|
|
656
|
+
system: systemPrompt,
|
|
657
|
+
messages: [...messages],
|
|
658
|
+
};
|
|
659
|
+
|
|
660
|
+
if (tools.length > 0) {
|
|
661
|
+
apiParams.tools = tools;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
let fullResponse = "";
|
|
665
|
+
let round = 0;
|
|
666
|
+
|
|
667
|
+
while (round < MAX_TOOL_ROUNDS) {
|
|
668
|
+
round++;
|
|
669
|
+
const response = await anthropic.messages.create(apiParams);
|
|
670
|
+
|
|
671
|
+
const textParts = [];
|
|
672
|
+
const toolUseBlocks = [];
|
|
673
|
+
|
|
674
|
+
for (const block of response.content) {
|
|
675
|
+
if (block.type === "text") {
|
|
676
|
+
textParts.push(block.text);
|
|
677
|
+
} else if (block.type === "tool_use") {
|
|
678
|
+
toolUseBlocks.push(block);
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
const roundText = textParts.join("");
|
|
683
|
+
fullResponse += roundText;
|
|
684
|
+
|
|
685
|
+
if (toolUseBlocks.length === 0) break;
|
|
686
|
+
|
|
687
|
+
if (roundText.trim()) {
|
|
688
|
+
const filler = roundText.replace(/\s*END_HUDDLE\s*/g, "").trim();
|
|
689
|
+
if (filler) {
|
|
690
|
+
await this.bridge.speak(filler);
|
|
691
|
+
if (this.session) {
|
|
692
|
+
this.session.transcript.push({ speaker: AGENT_SLUG, text: filler, ts: Date.now() });
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
const assistantContent = [];
|
|
698
|
+
if (roundText.trim()) {
|
|
699
|
+
assistantContent.push({ type: "text", text: roundText });
|
|
700
|
+
}
|
|
701
|
+
for (const tb of toolUseBlocks) {
|
|
702
|
+
assistantContent.push({ type: "tool_use", id: tb.id, name: tb.name, input: tb.input });
|
|
703
|
+
}
|
|
704
|
+
apiParams.messages.push({ role: "assistant", content: assistantContent });
|
|
705
|
+
|
|
706
|
+
const toolResults = [];
|
|
707
|
+
for (const tb of toolUseBlocks) {
|
|
708
|
+
console.log(`[TOOL] Executing: ${tb.name}`);
|
|
709
|
+
this._logEvent("tool-call", { tool: tb.name, input: tb.input });
|
|
710
|
+
|
|
711
|
+
if (executeAction) {
|
|
712
|
+
const callerInfo = {
|
|
713
|
+
slug: this.session?.callerSlug || "huddle-participant",
|
|
714
|
+
name: this.session?.callerName || "Huddle Participant",
|
|
715
|
+
accessLevel,
|
|
716
|
+
};
|
|
717
|
+
const result = await executeAction(tb.name, tb.input, callerInfo, `huddle-${Date.now()}`);
|
|
718
|
+
toolResults.push({ type: "tool_result", tool_use_id: tb.id, content: result.result });
|
|
719
|
+
console.log(`[TOOL] ${tb.name}: ${result.success ? "OK" : "FAILED"}`);
|
|
720
|
+
} else {
|
|
721
|
+
toolResults.push({
|
|
722
|
+
type: "tool_result",
|
|
723
|
+
tool_use_id: tb.id,
|
|
724
|
+
content: "Tool execution not available in this session.",
|
|
725
|
+
});
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
apiParams.messages.push({ role: "user", content: toolResults });
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
return fullResponse.trim();
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
// -------------------------------------------------------------------------
|
|
736
|
+
// Participant resolution (for access control + personalization)
|
|
737
|
+
// -------------------------------------------------------------------------
|
|
738
|
+
|
|
739
|
+
/**
|
|
740
|
+
* Resolve a huddle participant's identity from the agent repo contacts/profiles.
|
|
741
|
+
* Used for access control (CEO gets write tools) and personalized greetings.
|
|
742
|
+
*/
|
|
743
|
+
_resolveHuddleParticipant(identifier) {
|
|
744
|
+
// Seed `knownParticipants` with the agent's PRINCIPAL from
|
|
745
|
+
// config/agent.json (SOT) — replaces the previously-hardcoded "alex"
|
|
746
|
+
// entry. Falls back to common operational handles for back-compat.
|
|
747
|
+
const knownParticipants = {
|
|
748
|
+
"#ceo-office": { slug: "ceo-office", name: "CEO Office Channel", accessLevel: "ceo" },
|
|
749
|
+
};
|
|
750
|
+
try {
|
|
751
|
+
const agentPath = join(AGENT_REPO_DIR, "config/agent.json");
|
|
752
|
+
const agentJson = JSON.parse(readFileSync(agentPath, "utf-8"));
|
|
753
|
+
const principal = agentJson?.principal;
|
|
754
|
+
if (principal?.firstName) {
|
|
755
|
+
const slug = `${principal.firstName}-${principal.lastName || ""}`.toLowerCase().replace(/\s+/g, "-").replace(/-+$/, "");
|
|
756
|
+
const entry = {
|
|
757
|
+
slug,
|
|
758
|
+
name: principal.fullName || `${principal.firstName} ${principal.lastName || ""}`.trim(),
|
|
759
|
+
accessLevel: "ceo",
|
|
760
|
+
};
|
|
761
|
+
knownParticipants[principal.firstName.toLowerCase()] = entry;
|
|
762
|
+
knownParticipants[slug] = entry;
|
|
763
|
+
knownParticipants["principal"] = entry;
|
|
764
|
+
}
|
|
765
|
+
} catch { /* agent.json unavailable — only #ceo-office remains */ }
|
|
766
|
+
|
|
767
|
+
const key = (identifier || "").toLowerCase().replace(/^#/, "");
|
|
768
|
+
const match = knownParticipants[key];
|
|
769
|
+
|
|
770
|
+
if (match) return match;
|
|
771
|
+
|
|
772
|
+
// Try loading from contacts.yaml
|
|
773
|
+
try {
|
|
774
|
+
const contactsPath = join(AGENT_REPO_DIR, "config/contacts.yaml");
|
|
775
|
+
const contacts = readFileSync(contactsPath, "utf8");
|
|
776
|
+
// Simple search — look for the identifier in the contacts file
|
|
777
|
+
if (contacts.toLowerCase().includes(key)) {
|
|
778
|
+
// If found in contacts, give default access
|
|
779
|
+
return { slug: key, name: identifier, accessLevel: "default" };
|
|
780
|
+
}
|
|
781
|
+
} catch { /* optional */ }
|
|
782
|
+
|
|
783
|
+
return { slug: "unknown", name: identifier || "Unknown", accessLevel: "default" };
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
// -------------------------------------------------------------------------
|
|
787
|
+
// Slack Events API listener (huddle detection)
|
|
788
|
+
// -------------------------------------------------------------------------
|
|
789
|
+
|
|
790
|
+
/**
|
|
791
|
+
* Start a lightweight HTTP server that receives forwarded huddle events
|
|
792
|
+
* from the main slack-events-server.mjs. This provides a more reliable
|
|
793
|
+
* huddle detection path than DOM polling alone.
|
|
794
|
+
*/
|
|
795
|
+
_startEventsListener() {
|
|
796
|
+
this.eventsServer = createServer((req, res) => {
|
|
797
|
+
if (req.method !== "POST" || req.url !== "/huddle-event") {
|
|
798
|
+
res.writeHead(404);
|
|
799
|
+
res.end();
|
|
800
|
+
return;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
let body = "";
|
|
804
|
+
req.on("data", (chunk) => { body += chunk; });
|
|
805
|
+
req.on("end", () => {
|
|
806
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
807
|
+
res.end('{"ok":true}');
|
|
808
|
+
|
|
809
|
+
try {
|
|
810
|
+
const event = JSON.parse(body);
|
|
811
|
+
this._handleHuddleEvent(event);
|
|
812
|
+
} catch (err) {
|
|
813
|
+
console.error(`[EVENTS] Parse error: ${err.message}`);
|
|
814
|
+
}
|
|
815
|
+
});
|
|
816
|
+
});
|
|
817
|
+
|
|
818
|
+
this.eventsServer.listen(HUDDLE_EVENTS_PORT, "127.0.0.1", () => {
|
|
819
|
+
console.log(`[EVENTS] Huddle event listener on port ${HUDDLE_EVENTS_PORT}`);
|
|
820
|
+
});
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
/**
|
|
824
|
+
* Handle a huddle event forwarded from the Slack Events API.
|
|
825
|
+
* Event shape: { user, huddle_state, channel, channel_type }
|
|
826
|
+
*/
|
|
827
|
+
_handleHuddleEvent(event) {
|
|
828
|
+
const { user, user_name, huddle_state, channel, channel_type } = event;
|
|
829
|
+
|
|
830
|
+
console.log(`[EVENTS] Huddle event: ${user_name || user} → ${huddle_state} in ${channel || "unknown"}`);
|
|
831
|
+
this._logEvent("events-api-huddle", event);
|
|
832
|
+
|
|
833
|
+
// Someone started a huddle and the agent isn't in one yet
|
|
834
|
+
if (huddle_state === "in_a_huddle" && this.state === HuddleState.IDLE && this.autoJoin) {
|
|
835
|
+
// Determine the channel to join
|
|
836
|
+
const targetChannel = channel || user_name;
|
|
837
|
+
if (targetChannel) {
|
|
838
|
+
console.log(`[EVENTS] Huddle detected — auto-joining ${targetChannel}`);
|
|
839
|
+
this.joinHuddle(targetChannel);
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
// -------------------------------------------------------------------------
|
|
845
|
+
// System prompt (adapted for huddle context)
|
|
846
|
+
// -------------------------------------------------------------------------
|
|
847
|
+
|
|
848
|
+
_buildSystemPrompt() {
|
|
849
|
+
const liveContextBlock = this.session?.liveContext
|
|
850
|
+
? `\n\nLIVE CONTEXT:\n${this.session.liveContext}`
|
|
851
|
+
: "";
|
|
852
|
+
|
|
853
|
+
return `${HUDDLE_SYSTEM_PROMPT}${liveContextBlock}`;
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
// -------------------------------------------------------------------------
|
|
857
|
+
// Live context loader (reads from the agent repo state files)
|
|
858
|
+
// -------------------------------------------------------------------------
|
|
859
|
+
|
|
860
|
+
_loadLiveContext() {
|
|
861
|
+
const contextParts = [];
|
|
862
|
+
|
|
863
|
+
// Load priorities
|
|
864
|
+
try {
|
|
865
|
+
const priorities = readFileSync(join(AGENT_REPO_DIR, "config/priorities.yaml"), "utf8");
|
|
866
|
+
contextParts.push(`PRIORITIES:\n${priorities.slice(0, 2000)}`);
|
|
867
|
+
} catch { /* optional */ }
|
|
868
|
+
|
|
869
|
+
// Load executive summary
|
|
870
|
+
try {
|
|
871
|
+
const summary = readFileSync(join(AGENT_REPO_DIR, "state/dashboards/executive-summary.yaml"), "utf8");
|
|
872
|
+
contextParts.push(`EXECUTIVE SUMMARY:\n${summary.slice(0, 3000)}`);
|
|
873
|
+
} catch { /* optional */ }
|
|
874
|
+
|
|
875
|
+
return contextParts.join("\n\n");
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
// -------------------------------------------------------------------------
|
|
879
|
+
// Event wiring
|
|
880
|
+
// -------------------------------------------------------------------------
|
|
881
|
+
|
|
882
|
+
_wireControllerEvents() {
|
|
883
|
+
this.controller.on("connected", () => {
|
|
884
|
+
console.log("[CDP] Connected to Slack");
|
|
885
|
+
});
|
|
886
|
+
|
|
887
|
+
this.controller.on("disconnected", () => {
|
|
888
|
+
console.log("[CDP] Disconnected from Slack — reconnecting...");
|
|
889
|
+
});
|
|
890
|
+
|
|
891
|
+
this.controller.on("huddle:invitation", async (data) => {
|
|
892
|
+
console.log(`[HUDDLE] Incoming invitation from ${data.from} in ${data.channel}`);
|
|
893
|
+
this._logEvent("invitation-received", data);
|
|
894
|
+
|
|
895
|
+
if (this.autoJoin && this.state === HuddleState.IDLE) {
|
|
896
|
+
console.log("[HUDDLE] Auto-joining...");
|
|
897
|
+
await this.controller.acceptInvitation();
|
|
898
|
+
await this._startSession(data.channel, "joined");
|
|
899
|
+
}
|
|
900
|
+
});
|
|
901
|
+
|
|
902
|
+
this.controller.on("huddle:joined", async (data) => {
|
|
903
|
+
console.log(`[HUDDLE] Active huddle detected: ${data.channel}`);
|
|
904
|
+
if (this.state === HuddleState.IDLE) {
|
|
905
|
+
await this._startSession(data.channel, "joined");
|
|
906
|
+
}
|
|
907
|
+
});
|
|
908
|
+
|
|
909
|
+
this.controller.on("huddle:left", async () => {
|
|
910
|
+
if (this.state === HuddleState.ACTIVE) {
|
|
911
|
+
// Debounce: the polling can falsely detect "left" when the huddle
|
|
912
|
+
// runs in a separate window and the main window DOM is temporarily
|
|
913
|
+
// empty. Wait 5 seconds and re-check before ending the session.
|
|
914
|
+
console.log("[HUDDLE] Huddle may have ended — verifying in 5s...");
|
|
915
|
+
await new Promise((r) => setTimeout(r, 5000));
|
|
916
|
+
const stillActive = await this.controller.isInHuddle();
|
|
917
|
+
if (!stillActive && this.state === HuddleState.ACTIVE) {
|
|
918
|
+
console.log("[HUDDLE] Confirmed: huddle ended");
|
|
919
|
+
await this._endSession();
|
|
920
|
+
this.state = HuddleState.IDLE;
|
|
921
|
+
} else {
|
|
922
|
+
console.log("[HUDDLE] False alarm — huddle is still active");
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
});
|
|
926
|
+
|
|
927
|
+
this.controller.on("error", (err) => {
|
|
928
|
+
console.error(`[CDP] Error: ${err.message} — ${err.detail || ""}`);
|
|
929
|
+
});
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
_wireBridgeEvents() {
|
|
933
|
+
this.bridge.on("transcript", (data) => {
|
|
934
|
+
// openai-realtime emits {role, text} — split user vs assistant
|
|
935
|
+
// legacy bridge emits {text} for user-side only
|
|
936
|
+
if (this._brainKind === "openai-realtime") {
|
|
937
|
+
if (!this.session) return;
|
|
938
|
+
const speaker = data.role === "assistant" ? AGENT_SLUG : "participant";
|
|
939
|
+
this.session.transcript.push({ speaker, text: data.text, ts: Date.now() });
|
|
940
|
+
if (data.role === "assistant") {
|
|
941
|
+
console.log(`[${AGENT_SLUG.toUpperCase()}] "${data.text}"`);
|
|
942
|
+
} else {
|
|
943
|
+
console.log(`[SPEECH] "${data.text}"`);
|
|
944
|
+
}
|
|
945
|
+
return;
|
|
946
|
+
}
|
|
947
|
+
// Legacy: drive the Claude conversation loop
|
|
948
|
+
this._handleTranscript(data.text);
|
|
949
|
+
});
|
|
950
|
+
|
|
951
|
+
this.bridge.on("transcript:interim", (data) => {
|
|
952
|
+
// Interim transcript — show for debugging
|
|
953
|
+
process.stdout.write(`\r[INTERIM] ${data.text} `);
|
|
954
|
+
});
|
|
955
|
+
|
|
956
|
+
this.bridge.on("tool:call", (data) => {
|
|
957
|
+
console.log(`[TOOL] ${data.name}(${(data.arguments || "").slice(0, 80)})`);
|
|
958
|
+
});
|
|
959
|
+
|
|
960
|
+
this.bridge.on("speech:start", () => {
|
|
961
|
+
// Someone started talking
|
|
962
|
+
});
|
|
963
|
+
|
|
964
|
+
this.bridge.on("speech:end", () => {
|
|
965
|
+
process.stdout.write("\r" + " ".repeat(80) + "\r"); // Clear interim line
|
|
966
|
+
});
|
|
967
|
+
|
|
968
|
+
this.bridge.on("tts:start", async (data) => {
|
|
969
|
+
// Legacy bridge emits {text: ...} (a discrete utterance about to play);
|
|
970
|
+
// openai-realtime emits empty (continuous audio stream just opened).
|
|
971
|
+
const preview = data?.text ? `"${data.text.substring(0, 60)}…"` : "(audio stream live)";
|
|
972
|
+
console.log(`[TTS] ${preview}`);
|
|
973
|
+
// Unmute Slack mic so TTS audio reaches the huddle
|
|
974
|
+
await this._setSlackMicMute(false);
|
|
975
|
+
});
|
|
976
|
+
|
|
977
|
+
this.bridge.on("tts:end", async () => {
|
|
978
|
+
// Re-mute Slack mic after TTS to prevent echo
|
|
979
|
+
// Small delay to ensure the last audio samples are transmitted
|
|
980
|
+
setTimeout(() => this._setSlackMicMute(true), 300);
|
|
981
|
+
});
|
|
982
|
+
|
|
983
|
+
this.bridge.on("error", (err) => {
|
|
984
|
+
console.error(`[BRIDGE] ${err.component} error: ${err.message}`);
|
|
985
|
+
});
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
// -------------------------------------------------------------------------
|
|
989
|
+
// Slack mic mute control (half-duplex echo prevention)
|
|
990
|
+
// -------------------------------------------------------------------------
|
|
991
|
+
|
|
992
|
+
/**
|
|
993
|
+
* Toggle the agent's Slack mic mute via CDP on the huddle window.
|
|
994
|
+
* Keeps mic muted between TTS outputs to prevent echo from the
|
|
995
|
+
* virtual audio loopback.
|
|
996
|
+
* @param {boolean} muted - true to mute, false to unmute
|
|
997
|
+
*/
|
|
998
|
+
async _setSlackMicMute(muted) {
|
|
999
|
+
// In webaudio mode, mic muting for echo prevention is unnecessary —
|
|
1000
|
+
// the echo is eliminated at the capture level, not the mic level.
|
|
1001
|
+
if (this.bridge.captureMode === "webaudio") return;
|
|
1002
|
+
try {
|
|
1003
|
+
const targets = await fetch("http://localhost:9222/json").then((r) => r.json());
|
|
1004
|
+
const huddleTarget = targets.find(
|
|
1005
|
+
(t) => t.type === "page" && (t.title?.includes("Huddle:") || t.title?.includes("🎤"))
|
|
1006
|
+
);
|
|
1007
|
+
if (!huddleTarget) return;
|
|
1008
|
+
|
|
1009
|
+
const ws = new (await import("ws")).WebSocket(huddleTarget.webSocketDebuggerUrl);
|
|
1010
|
+
await new Promise((resolve, reject) => {
|
|
1011
|
+
ws.on("error", reject);
|
|
1012
|
+
ws.on("open", () => {
|
|
1013
|
+
ws.send(JSON.stringify({
|
|
1014
|
+
id: 1,
|
|
1015
|
+
method: "Runtime.evaluate",
|
|
1016
|
+
params: {
|
|
1017
|
+
expression: `(() => {
|
|
1018
|
+
const muteBtn = document.querySelector('[data-qa="segmented-mute-button-main"]');
|
|
1019
|
+
if (!muteBtn) return "no-mute-btn";
|
|
1020
|
+
const label = (muteBtn.getAttribute("aria-label") || "").toLowerCase();
|
|
1021
|
+
const currentlyMuted = label.includes("unmute");
|
|
1022
|
+
if (currentlyMuted === ${muted}) return "already-${muted ? "muted" : "unmuted"}";
|
|
1023
|
+
muteBtn.click();
|
|
1024
|
+
return "${muted ? "muted" : "unmuted"}";
|
|
1025
|
+
})()`,
|
|
1026
|
+
returnByValue: true,
|
|
1027
|
+
},
|
|
1028
|
+
}));
|
|
1029
|
+
});
|
|
1030
|
+
ws.on("message", (data) => {
|
|
1031
|
+
const msg = JSON.parse(data.toString());
|
|
1032
|
+
if (msg.id === 1) {
|
|
1033
|
+
const result = msg.result?.result?.value || "unknown";
|
|
1034
|
+
if (result !== "already-muted" && result !== "already-unmuted") {
|
|
1035
|
+
console.log(`[MIC] ${result}`);
|
|
1036
|
+
}
|
|
1037
|
+
ws.close();
|
|
1038
|
+
resolve();
|
|
1039
|
+
}
|
|
1040
|
+
});
|
|
1041
|
+
});
|
|
1042
|
+
} catch {
|
|
1043
|
+
// Non-fatal — mute toggle failed silently
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
/**
|
|
1048
|
+
* Set Slack's speaker output to "default" (physical speakers) via CDP.
|
|
1049
|
+
* Used in webaudio capture mode to eliminate AEC echo — audio is captured
|
|
1050
|
+
* directly from the WebRTC stream, so BlackHole 2ch isn't needed.
|
|
1051
|
+
*/
|
|
1052
|
+
async _setSlackSpeakerToDefault() {
|
|
1053
|
+
try {
|
|
1054
|
+
const targets = await fetch("http://localhost:9222/json").then((r) => r.json());
|
|
1055
|
+
const mainTarget = targets.find(
|
|
1056
|
+
(t) => t.type === "page" && t.url?.includes("app.slack.com/client")
|
|
1057
|
+
);
|
|
1058
|
+
if (!mainTarget) return;
|
|
1059
|
+
|
|
1060
|
+
const ws = new (await import("ws")).WebSocket(mainTarget.webSocketDebuggerUrl);
|
|
1061
|
+
await new Promise((resolve) => {
|
|
1062
|
+
ws.on("error", () => resolve());
|
|
1063
|
+
ws.on("open", () => {
|
|
1064
|
+
ws.send(JSON.stringify({
|
|
1065
|
+
id: 1,
|
|
1066
|
+
method: "Runtime.evaluate",
|
|
1067
|
+
params: {
|
|
1068
|
+
expression: `(async () => {
|
|
1069
|
+
// Set speaker preference to default (physical speakers)
|
|
1070
|
+
const inputId = "9f391a3f2efb9f50f9dc8dea1534700bcc5d4e0478b56438930a2071401b25bb";
|
|
1071
|
+
localStorage.setItem("preferred-media-devices",
|
|
1072
|
+
JSON.stringify({ audioinput: inputId, audiooutput: "default" }));
|
|
1073
|
+
|
|
1074
|
+
// Also change the active audio element's output immediately
|
|
1075
|
+
for (const audio of document.querySelectorAll("audio")) {
|
|
1076
|
+
try { await audio.setSinkId("default"); } catch {}
|
|
1077
|
+
}
|
|
1078
|
+
return "speaker set to default";
|
|
1079
|
+
})()`,
|
|
1080
|
+
returnByValue: true,
|
|
1081
|
+
awaitPromise: true,
|
|
1082
|
+
},
|
|
1083
|
+
}));
|
|
1084
|
+
});
|
|
1085
|
+
ws.on("message", (data) => {
|
|
1086
|
+
const msg = JSON.parse(data.toString());
|
|
1087
|
+
if (msg.id === 1) {
|
|
1088
|
+
console.log(`[AUDIO] Slack speaker → physical (${msg.result?.result?.value})`);
|
|
1089
|
+
ws.close();
|
|
1090
|
+
resolve();
|
|
1091
|
+
}
|
|
1092
|
+
});
|
|
1093
|
+
});
|
|
1094
|
+
} catch {
|
|
1095
|
+
// Non-fatal
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
// -------------------------------------------------------------------------
|
|
1100
|
+
// Logging & transcript persistence
|
|
1101
|
+
// -------------------------------------------------------------------------
|
|
1102
|
+
|
|
1103
|
+
_logEvent(event, data = {}) {
|
|
1104
|
+
const logDir = join(AGENT_REPO_DIR, "logs/huddle");
|
|
1105
|
+
mkdirSync(logDir, { recursive: true });
|
|
1106
|
+
|
|
1107
|
+
const date = new Date().toISOString().split("T")[0];
|
|
1108
|
+
const logFile = join(logDir, `${date}-huddle-events.jsonl`);
|
|
1109
|
+
|
|
1110
|
+
const entry = {
|
|
1111
|
+
timestamp: new Date().toISOString(),
|
|
1112
|
+
event,
|
|
1113
|
+
...data,
|
|
1114
|
+
};
|
|
1115
|
+
|
|
1116
|
+
appendFileSync(logFile, JSON.stringify(entry) + "\n");
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
_saveTranscript() {
|
|
1120
|
+
if (!this.session || this.session.transcript.length === 0) return;
|
|
1121
|
+
|
|
1122
|
+
const logDir = join(AGENT_REPO_DIR, "logs/huddle");
|
|
1123
|
+
mkdirSync(logDir, { recursive: true });
|
|
1124
|
+
|
|
1125
|
+
const date = new Date().toISOString().split("T")[0];
|
|
1126
|
+
const ts = Date.now();
|
|
1127
|
+
const channel = (this.session.channel || "unknown").replace(/[^a-zA-Z0-9-]/g, "_");
|
|
1128
|
+
const transcriptFile = join(logDir, `${date}-transcript-${channel}-${ts}.jsonl`);
|
|
1129
|
+
|
|
1130
|
+
for (const entry of this.session.transcript) {
|
|
1131
|
+
appendFileSync(transcriptFile, JSON.stringify(entry) + "\n");
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
console.log(`[TRANSCRIPT] Saved to ${transcriptFile}`);
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
// ---------------------------------------------------------------------------
|
|
1139
|
+
// Huddle-specific system prompt
|
|
1140
|
+
// ---------------------------------------------------------------------------
|
|
1141
|
+
|
|
1142
|
+
function buildHuddleSystemPrompt() {
|
|
1143
|
+
const a = loadAgent();
|
|
1144
|
+
const principal = a.principal || {};
|
|
1145
|
+
return `You are ${a.fullName}, ${a.title} to ${principal.fullName || "the principal"}, ${principal.title || "the principal"} of ${a.company}.
|
|
1146
|
+
You are participating in a Slack huddle — a real-time voice conversation.
|
|
1147
|
+
|
|
1148
|
+
PERSONALITY:
|
|
1149
|
+
- Sound like a thoughtful colleague, not an assistant. Full guide: policies/communication-style.md
|
|
1150
|
+
- Natural, conversational, warm — this is a casual voice huddle, not a formal call
|
|
1151
|
+
- Concise responses — one or two sentences per turn, then pause
|
|
1152
|
+
- Use contractions, casual phrasing, natural filler words ("yeah", "right", "got it")
|
|
1153
|
+
- Match the energy of the other participants — terse if they're terse, warm if they're warm
|
|
1154
|
+
- If they are discussing something serious, be appropriately focused
|
|
1155
|
+
|
|
1156
|
+
BANNED OPENERS AND PHRASES (do not use):
|
|
1157
|
+
- "Certainly!" / "Absolutely!" / "Great question!" as openers
|
|
1158
|
+
- "I'd be happy to…" / "I'm here to help" / "Let me know if you need anything else"
|
|
1159
|
+
- "As an AI" / "As a language model" / "I should clarify that…"
|
|
1160
|
+
- "I hope this helps" / "Feel free to ask if you have any questions"
|
|
1161
|
+
- "Here's a comprehensive overview of…"
|
|
1162
|
+
- Don't re-introduce yourself by listing your role at length. "Hi, this is ${a.firstName || "the agent"}" is enough.
|
|
1163
|
+
|
|
1164
|
+
ABOUT ADAPTIC:
|
|
1165
|
+
- Northwind is a global AI-native institutional asset management group
|
|
1166
|
+
- Headquartered in DIFC, Dubai with entities across seven plus jurisdictions
|
|
1167
|
+
- CEO and founder: the principal (resolved from config/agent.json)
|
|
1168
|
+
- Building Northwind OS, an algorithmic trading platform
|
|
1169
|
+
- Currently in regulatory licensing phase
|
|
1170
|
+
|
|
1171
|
+
HUDDLE RULES:
|
|
1172
|
+
- Huddles are informal — keep responses short and natural
|
|
1173
|
+
- Listen more than you talk — do not dominate the conversation
|
|
1174
|
+
- If asked a direct question, answer it concisely
|
|
1175
|
+
- If you hear a request or action item, confirm it: "Got it, I will take care of that"
|
|
1176
|
+
- If you cannot help with something, say so briefly
|
|
1177
|
+
- If the conversation naturally ends or someone says goodbye, respond with a brief farewell and end your response with END_HUDDLE on its own line
|
|
1178
|
+
|
|
1179
|
+
MULTI-SPEAKER AWARENESS:
|
|
1180
|
+
- Multiple people may be in the huddle
|
|
1181
|
+
- You may not always know who is speaking — that is okay
|
|
1182
|
+
- If someone introduces themselves, use their name
|
|
1183
|
+
- Do not assume all speech is from one person
|
|
1184
|
+
|
|
1185
|
+
SPEECH RULES:
|
|
1186
|
+
- Never use special characters, markdown, bullet points, or formatting
|
|
1187
|
+
- Spell out numbers in words
|
|
1188
|
+
- Keep it conversational — say "yeah" not "yes indeed"
|
|
1189
|
+
- Do not use emojis
|
|
1190
|
+
- When saying email addresses, use "at" and "dot" instead of symbols
|
|
1191
|
+
|
|
1192
|
+
CRITICAL — DO NOT REPEAT YOURSELF:
|
|
1193
|
+
- Never re-introduce yourself after the initial greeting
|
|
1194
|
+
- Each response should add new information or move the conversation forward
|
|
1195
|
+
- If you already answered a question, do not repeat the answer`;
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
const HUDDLE_SYSTEM_PROMPT = buildHuddleSystemPrompt();
|
|
1199
|
+
|
|
1200
|
+
// ---------------------------------------------------------------------------
|
|
1201
|
+
// Main
|
|
1202
|
+
// ---------------------------------------------------------------------------
|
|
1203
|
+
|
|
1204
|
+
const server = new HuddleServer();
|
|
1205
|
+
|
|
1206
|
+
// Graceful shutdown
|
|
1207
|
+
process.on("SIGINT", async () => {
|
|
1208
|
+
console.log("\n[SHUTDOWN] Shutting down huddle server...");
|
|
1209
|
+
if (server.state === HuddleState.ACTIVE) {
|
|
1210
|
+
await server.bridge.speak("I need to go — talk soon.");
|
|
1211
|
+
await server.leaveHuddle();
|
|
1212
|
+
}
|
|
1213
|
+
await server.controller.disconnect();
|
|
1214
|
+
process.exit(0);
|
|
1215
|
+
});
|
|
1216
|
+
|
|
1217
|
+
process.on("SIGTERM", async () => {
|
|
1218
|
+
if (server.state === HuddleState.ACTIVE) {
|
|
1219
|
+
await server.leaveHuddle();
|
|
1220
|
+
}
|
|
1221
|
+
await server.controller.disconnect();
|
|
1222
|
+
process.exit(0);
|
|
1223
|
+
});
|
|
1224
|
+
|
|
1225
|
+
// Start
|
|
1226
|
+
server.start().catch((err) => {
|
|
1227
|
+
console.error(`[FATAL] ${err.message}`);
|
|
1228
|
+
process.exit(1);
|
|
1229
|
+
});
|