@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,942 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Maestro Huddle — Controller
|
|
4
|
+
*
|
|
5
|
+
* Controls the Slack desktop app via Chrome DevTools Protocol for huddle
|
|
6
|
+
* participation. Connects to Slack launched with --remote-debugging-port
|
|
7
|
+
* and automates join/initiate/leave via keyboard shortcuts and DOM observation.
|
|
8
|
+
*
|
|
9
|
+
* Architecture:
|
|
10
|
+
* Slack Desktop (Electron) ←→ CDP (port 9222) ←→ This controller
|
|
11
|
+
*
|
|
12
|
+
* Primary automation method: keyboard shortcuts (most stable across updates)
|
|
13
|
+
* - Cmd+K → Quick switcher (navigate to channel/DM)
|
|
14
|
+
* - Cmd+Shift+H → Toggle huddle (start/join/leave)
|
|
15
|
+
*
|
|
16
|
+
* Fallback: DOM queries via CDP for state verification and invitation detection.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { EventEmitter } from "node:events";
|
|
20
|
+
import { execFile } from "node:child_process";
|
|
21
|
+
import { promisify } from "node:util";
|
|
22
|
+
import { WebSocket } from "ws";
|
|
23
|
+
|
|
24
|
+
const execFileAsync = promisify(execFile);
|
|
25
|
+
|
|
26
|
+
const CDP_URL = process.env.CDP_URL || "http://localhost:9222";
|
|
27
|
+
const RECONNECT_INTERVAL_MS = 5000;
|
|
28
|
+
const POLL_INTERVAL_MS = 2000;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* HuddleController manages Slack huddle interactions via CDP.
|
|
32
|
+
*
|
|
33
|
+
* Events emitted:
|
|
34
|
+
* - 'connected' — CDP connection established
|
|
35
|
+
* - 'disconnected' — CDP connection lost
|
|
36
|
+
* - 'huddle:invitation' — Incoming huddle invitation detected { channel, from }
|
|
37
|
+
* - 'huddle:joined' — Successfully joined a huddle { channel }
|
|
38
|
+
* - 'huddle:left' — Left a huddle
|
|
39
|
+
* - 'huddle:started' — Initiated a new huddle { channel }
|
|
40
|
+
* - 'huddle:participant_joined' — Someone joined the huddle { name }
|
|
41
|
+
* - 'huddle:participant_left' — Someone left the huddle { name }
|
|
42
|
+
* - 'error' — An error occurred { message, detail }
|
|
43
|
+
*/
|
|
44
|
+
export class HuddleController extends EventEmitter {
|
|
45
|
+
constructor(options = {}) {
|
|
46
|
+
super();
|
|
47
|
+
this.cdpUrl = options.cdpUrl || CDP_URL;
|
|
48
|
+
this.ws = null;
|
|
49
|
+
this.messageId = 0;
|
|
50
|
+
this.pendingMessages = new Map(); // id → { resolve, reject, timer }
|
|
51
|
+
this.targetId = null;
|
|
52
|
+
this.sessionId = null;
|
|
53
|
+
this.connected = false;
|
|
54
|
+
this.huddleActive = false;
|
|
55
|
+
this.currentChannel = null;
|
|
56
|
+
this.pollTimer = null;
|
|
57
|
+
this.reconnectTimer = null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// -------------------------------------------------------------------------
|
|
61
|
+
// Connection lifecycle
|
|
62
|
+
// -------------------------------------------------------------------------
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Connect to Slack's CDP endpoint. Finds the main Slack window target
|
|
66
|
+
* and establishes a debugging session.
|
|
67
|
+
*/
|
|
68
|
+
async connect() {
|
|
69
|
+
try {
|
|
70
|
+
// Discover debuggable targets
|
|
71
|
+
const targets = await this._fetchTargets();
|
|
72
|
+
if (!targets || targets.length === 0) {
|
|
73
|
+
throw new Error("No CDP targets found — is Slack running with --remote-debugging-port?");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Find the main Slack client page (app.slack.com), not the blank huddle preview
|
|
77
|
+
const slackTarget =
|
|
78
|
+
targets.find(
|
|
79
|
+
(t) => t.type === "page" && t.url && t.url.includes("app.slack.com")
|
|
80
|
+
) ||
|
|
81
|
+
targets.find(
|
|
82
|
+
(t) =>
|
|
83
|
+
t.type === "page" &&
|
|
84
|
+
t.url &&
|
|
85
|
+
!t.url.startsWith("devtools://") &&
|
|
86
|
+
t.url !== "about:blank"
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
if (!slackTarget) {
|
|
90
|
+
throw new Error("No Slack page target found among CDP targets");
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
this.targetId = slackTarget.id;
|
|
94
|
+
|
|
95
|
+
// Connect WebSocket to the target
|
|
96
|
+
const wsUrl = slackTarget.webSocketDebuggerUrl;
|
|
97
|
+
if (!wsUrl) {
|
|
98
|
+
throw new Error("Target has no webSocketDebuggerUrl");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
await this._connectWebSocket(wsUrl);
|
|
102
|
+
this.connected = true;
|
|
103
|
+
this.emit("connected");
|
|
104
|
+
|
|
105
|
+
// Start polling for huddle state
|
|
106
|
+
this._startPolling();
|
|
107
|
+
|
|
108
|
+
return true;
|
|
109
|
+
} catch (err) {
|
|
110
|
+
this.emit("error", { message: "Connection failed", detail: err.message });
|
|
111
|
+
this._scheduleReconnect();
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Disconnect from CDP and clean up.
|
|
118
|
+
*/
|
|
119
|
+
async disconnect() {
|
|
120
|
+
this._stopPolling();
|
|
121
|
+
if (this.reconnectTimer) {
|
|
122
|
+
clearTimeout(this.reconnectTimer);
|
|
123
|
+
this.reconnectTimer = null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (this.ws) {
|
|
127
|
+
this.ws.close();
|
|
128
|
+
this.ws = null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
this.connected = false;
|
|
132
|
+
this.pendingMessages.clear();
|
|
133
|
+
this.emit("disconnected");
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// -------------------------------------------------------------------------
|
|
137
|
+
// Huddle actions
|
|
138
|
+
// -------------------------------------------------------------------------
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Navigate to a channel or DM by name using Cmd+K quick switcher.
|
|
142
|
+
* @param {string} name - Channel name (e.g., "#general") or person name
|
|
143
|
+
* @returns {Promise<boolean>} Whether navigation succeeded
|
|
144
|
+
*/
|
|
145
|
+
async navigateToChannel(name) {
|
|
146
|
+
if (!this.connected) throw new Error("Not connected to Slack");
|
|
147
|
+
|
|
148
|
+
// Use system-level keystrokes — Cmd+K is an Electron accelerator,
|
|
149
|
+
// not a DOM event, so CDP Input.dispatchKeyEvent doesn't trigger it.
|
|
150
|
+
await this._systemKeystroke("k");
|
|
151
|
+
await this._sleep(800);
|
|
152
|
+
|
|
153
|
+
// Type the person/channel name into the quick switcher
|
|
154
|
+
await this._systemType(name);
|
|
155
|
+
await this._sleep(1200);
|
|
156
|
+
|
|
157
|
+
// Press Enter to navigate to the selected result
|
|
158
|
+
await this._systemKeyPress("return");
|
|
159
|
+
await this._sleep(1500);
|
|
160
|
+
|
|
161
|
+
this.currentChannel = name;
|
|
162
|
+
return true;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Join an active huddle or start a new one in the current channel.
|
|
167
|
+
* Uses Cmd+Shift+H keyboard shortcut.
|
|
168
|
+
* @param {string} [channel] - Optional channel to navigate to first
|
|
169
|
+
* @returns {Promise<boolean>}
|
|
170
|
+
*/
|
|
171
|
+
async joinHuddle(channel) {
|
|
172
|
+
if (!this.connected) throw new Error("Not connected to Slack");
|
|
173
|
+
|
|
174
|
+
if (channel) {
|
|
175
|
+
await this.navigateToChannel(channel);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Step 1: Click huddle button (opens preview window)
|
|
179
|
+
const previewOpened = await this._clickHuddleButton();
|
|
180
|
+
if (!previewOpened) {
|
|
181
|
+
await this._systemKeystroke("h", ["command", "shift"]);
|
|
182
|
+
}
|
|
183
|
+
await this._sleep(2000);
|
|
184
|
+
|
|
185
|
+
// Step 2: Click "Start huddle" in preview window
|
|
186
|
+
await this._clickPreviewStartButton();
|
|
187
|
+
await this._sleep(4000);
|
|
188
|
+
|
|
189
|
+
// Verify huddle state
|
|
190
|
+
const inHuddle = await this._checkHuddleActive();
|
|
191
|
+
if (inHuddle) {
|
|
192
|
+
this.huddleActive = true;
|
|
193
|
+
this.emit("huddle:joined", { channel: channel || this.currentChannel });
|
|
194
|
+
return true;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Initiate a new huddle in a specific channel or DM.
|
|
202
|
+
* @param {string} channel - Channel name or person name
|
|
203
|
+
* @returns {Promise<boolean>}
|
|
204
|
+
*/
|
|
205
|
+
async initiateHuddle(channel) {
|
|
206
|
+
if (!this.connected) throw new Error("Not connected to Slack");
|
|
207
|
+
|
|
208
|
+
await this.navigateToChannel(channel);
|
|
209
|
+
await this._sleep(500);
|
|
210
|
+
|
|
211
|
+
// Step 1: Click the huddle start button in the main window via React props.
|
|
212
|
+
// Slack opens a "Huddle preview" window where the user confirms mic/video
|
|
213
|
+
// before actually joining. We invoke the React onClick handler because
|
|
214
|
+
// CDP mouse events and DOM .click() are silently ignored by Slack's
|
|
215
|
+
// Electron security model.
|
|
216
|
+
const previewOpened = await this._clickHuddleButton();
|
|
217
|
+
if (!previewOpened) {
|
|
218
|
+
// Fallback: try Cmd+Shift+H keyboard shortcut
|
|
219
|
+
await this._systemKeystroke("h", ["command", "shift"]);
|
|
220
|
+
}
|
|
221
|
+
await this._sleep(2000);
|
|
222
|
+
|
|
223
|
+
// Step 2: Click "Start huddle" in the preview window using cliclick
|
|
224
|
+
// (system-level mouse click). The preview window is the first Slack
|
|
225
|
+
// window in the process list. We use the preview window's screen
|
|
226
|
+
// position to calculate absolute coordinates.
|
|
227
|
+
const started = await this._clickPreviewStartButton();
|
|
228
|
+
if (!started) {
|
|
229
|
+
console.warn("[HUDDLE] Failed to click Start in preview window");
|
|
230
|
+
}
|
|
231
|
+
await this._sleep(4000);
|
|
232
|
+
|
|
233
|
+
// Verify huddle is active
|
|
234
|
+
const inHuddle = await this._checkHuddleActive();
|
|
235
|
+
if (inHuddle) {
|
|
236
|
+
this.huddleActive = true;
|
|
237
|
+
this.emit("huddle:started", { channel });
|
|
238
|
+
return true;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return false;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Click the huddle button in the main window via React onClick handler.
|
|
246
|
+
* This opens the huddle preview window.
|
|
247
|
+
* @returns {Promise<boolean>}
|
|
248
|
+
*/
|
|
249
|
+
async _clickHuddleButton() {
|
|
250
|
+
try {
|
|
251
|
+
return await this._evaluate(`
|
|
252
|
+
(() => {
|
|
253
|
+
const btn = document.querySelector("[data-qa='huddle_channel_header_button__start_button']");
|
|
254
|
+
if (!btn) return false;
|
|
255
|
+
const propsKey = Object.keys(btn).find(k => k.startsWith('__reactProps'));
|
|
256
|
+
if (propsKey && btn[propsKey] && btn[propsKey].onClick) {
|
|
257
|
+
btn[propsKey].onClick({
|
|
258
|
+
target: btn, currentTarget: btn,
|
|
259
|
+
nativeEvent: new MouseEvent('click', {bubbles: true}),
|
|
260
|
+
type: 'click', preventDefault: () => {}, stopPropagation: () => {}, persist: () => {}
|
|
261
|
+
});
|
|
262
|
+
return true;
|
|
263
|
+
}
|
|
264
|
+
btn.click();
|
|
265
|
+
return true;
|
|
266
|
+
})()
|
|
267
|
+
`);
|
|
268
|
+
} catch {
|
|
269
|
+
return false;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Click "Start huddle" in the Huddle preview window using system-level
|
|
275
|
+
* mouse click. Discovers the preview window position via AppleScript
|
|
276
|
+
* and the button position via CDP on the preview renderer target.
|
|
277
|
+
* @returns {Promise<boolean>}
|
|
278
|
+
*/
|
|
279
|
+
async _clickPreviewStartButton() {
|
|
280
|
+
try {
|
|
281
|
+
// Get preview window position via AppleScript
|
|
282
|
+
const { stdout: windowInfo } = await execFileAsync("osascript", ["-e", `
|
|
283
|
+
tell application "System Events"
|
|
284
|
+
tell process "Slack"
|
|
285
|
+
repeat with i from 1 to (count of windows)
|
|
286
|
+
set w to window i
|
|
287
|
+
if name of w contains "Huddle preview" or name of w contains "Huddle:" then
|
|
288
|
+
set p to position of w
|
|
289
|
+
return (item 1 of p as text) & "," & (item 2 of p as text)
|
|
290
|
+
end if
|
|
291
|
+
end repeat
|
|
292
|
+
return "not_found"
|
|
293
|
+
end tell
|
|
294
|
+
end tell
|
|
295
|
+
`]);
|
|
296
|
+
|
|
297
|
+
const pos = windowInfo.trim();
|
|
298
|
+
if (pos === "not_found") {
|
|
299
|
+
console.warn("[HUDDLE] Preview window not found");
|
|
300
|
+
return false;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const [winX, winY] = pos.split(",").map(Number);
|
|
304
|
+
|
|
305
|
+
// Get button position in the preview window via CDP
|
|
306
|
+
// Find the preview renderer target
|
|
307
|
+
const targets = await this._fetchTargets();
|
|
308
|
+
const previewTarget = targets.find(t =>
|
|
309
|
+
t.title && t.title.includes("Huddle") && t.type === "page"
|
|
310
|
+
);
|
|
311
|
+
|
|
312
|
+
if (!previewTarget) {
|
|
313
|
+
console.warn("[HUDDLE] Preview CDP target not found");
|
|
314
|
+
// Fallback: use known default position (center-bottom of preview)
|
|
315
|
+
const screenX = winX + 316;
|
|
316
|
+
const screenY = winY + 974;
|
|
317
|
+
await execFileAsync("cliclick", [`c:${screenX},${screenY}`]);
|
|
318
|
+
return true;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Connect to preview target to find button position
|
|
322
|
+
const btnPos = await this._getPreviewButtonPosition(previewTarget.webSocketDebuggerUrl);
|
|
323
|
+
if (!btnPos) {
|
|
324
|
+
// Fallback to default position
|
|
325
|
+
const screenX = winX + 316;
|
|
326
|
+
const screenY = winY + 974;
|
|
327
|
+
await execFileAsync("cliclick", [`c:${screenX},${screenY}`]);
|
|
328
|
+
return true;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const screenX = winX + Math.round(btnPos.x);
|
|
332
|
+
const screenY = winY + Math.round(btnPos.y);
|
|
333
|
+
console.log(`[HUDDLE] Clicking Start at screen (${screenX}, ${screenY})`);
|
|
334
|
+
await execFileAsync("cliclick", [`c:${screenX},${screenY}`]);
|
|
335
|
+
return true;
|
|
336
|
+
} catch (err) {
|
|
337
|
+
console.error(`[HUDDLE] Preview click error: ${err.message}`);
|
|
338
|
+
return false;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Connect to the huddle preview CDP target and find the Start button position.
|
|
344
|
+
* @param {string} wsUrl - WebSocket debugger URL for the preview target
|
|
345
|
+
* @returns {Promise<{x: number, y: number}|null>}
|
|
346
|
+
*/
|
|
347
|
+
async _getPreviewButtonPosition(wsUrl) {
|
|
348
|
+
return new Promise((resolve) => {
|
|
349
|
+
const previewWs = new WebSocket(wsUrl);
|
|
350
|
+
const timeout = setTimeout(() => { previewWs.close(); resolve(null); }, 5000);
|
|
351
|
+
|
|
352
|
+
previewWs.on("open", () => {
|
|
353
|
+
previewWs.send(JSON.stringify({
|
|
354
|
+
id: 1,
|
|
355
|
+
method: "Runtime.evaluate",
|
|
356
|
+
params: {
|
|
357
|
+
expression: `(() => {
|
|
358
|
+
const btn = document.querySelector("[data-qa='huddle_invite_window_with_preview_content_join_button']");
|
|
359
|
+
if (!btn) return null;
|
|
360
|
+
const rect = btn.getBoundingClientRect();
|
|
361
|
+
return { x: rect.x + rect.width/2, y: rect.y + rect.height/2 };
|
|
362
|
+
})()`,
|
|
363
|
+
returnByValue: true,
|
|
364
|
+
},
|
|
365
|
+
}));
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
previewWs.on("message", (data) => {
|
|
369
|
+
const msg = JSON.parse(data.toString());
|
|
370
|
+
if (msg.id === 1) {
|
|
371
|
+
clearTimeout(timeout);
|
|
372
|
+
previewWs.close();
|
|
373
|
+
resolve(msg.result?.result?.value || null);
|
|
374
|
+
}
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
previewWs.on("error", () => { clearTimeout(timeout); resolve(null); });
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Leave the current huddle.
|
|
383
|
+
* @returns {Promise<boolean>}
|
|
384
|
+
*/
|
|
385
|
+
async leaveHuddle() {
|
|
386
|
+
if (!this.connected) throw new Error("Not connected to Slack");
|
|
387
|
+
if (!this.huddleActive) return true; // Already not in a huddle
|
|
388
|
+
|
|
389
|
+
// Toggle huddle off via system-level Cmd+Shift+H
|
|
390
|
+
await this._systemKeystroke("h", ["command", "shift"]);
|
|
391
|
+
await this._sleep(2000);
|
|
392
|
+
|
|
393
|
+
const stillActive = await this._checkHuddleActive();
|
|
394
|
+
if (!stillActive) {
|
|
395
|
+
this.huddleActive = false;
|
|
396
|
+
this.emit("huddle:left");
|
|
397
|
+
return true;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// Fallback: try clicking the Leave button via DOM
|
|
401
|
+
try {
|
|
402
|
+
await this._clickLeaveButton();
|
|
403
|
+
await this._sleep(1000);
|
|
404
|
+
this.huddleActive = false;
|
|
405
|
+
this.emit("huddle:left");
|
|
406
|
+
return true;
|
|
407
|
+
} catch {
|
|
408
|
+
return false;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Accept an incoming huddle invitation by clicking the Join button.
|
|
414
|
+
* @returns {Promise<boolean>}
|
|
415
|
+
*/
|
|
416
|
+
async acceptInvitation() {
|
|
417
|
+
if (!this.connected) throw new Error("Not connected to Slack");
|
|
418
|
+
|
|
419
|
+
// Try to find and click the Join button in the invitation popup
|
|
420
|
+
const clicked = await this._clickJoinButton();
|
|
421
|
+
if (clicked) {
|
|
422
|
+
await this._sleep(2000);
|
|
423
|
+
this.huddleActive = true;
|
|
424
|
+
this.emit("huddle:joined", { channel: this.currentChannel });
|
|
425
|
+
return true;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// Fallback: use keyboard shortcut
|
|
429
|
+
return this.joinHuddle();
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Mute/unmute the agent's microphone in the huddle.
|
|
434
|
+
* @param {boolean} muted - true to mute, false to unmute
|
|
435
|
+
*/
|
|
436
|
+
async setMute(muted) {
|
|
437
|
+
if (!this.connected || !this.huddleActive) return;
|
|
438
|
+
|
|
439
|
+
// Slack huddle mute shortcut: M key when huddle is focused
|
|
440
|
+
// Or use the mute button via DOM
|
|
441
|
+
const currentlyMuted = await this._checkMuteState();
|
|
442
|
+
if (currentlyMuted !== muted) {
|
|
443
|
+
await this._sendKey("m");
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// -------------------------------------------------------------------------
|
|
448
|
+
// State observation
|
|
449
|
+
// -------------------------------------------------------------------------
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Check if the agent is currently in a huddle.
|
|
453
|
+
* @returns {Promise<boolean>}
|
|
454
|
+
*/
|
|
455
|
+
async isInHuddle() {
|
|
456
|
+
if (!this.connected) return false;
|
|
457
|
+
return this._checkHuddleActive();
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Get current huddle participants.
|
|
462
|
+
* @returns {Promise<string[]>} List of participant display names
|
|
463
|
+
*/
|
|
464
|
+
async getParticipants() {
|
|
465
|
+
if (!this.connected || !this.huddleActive) return [];
|
|
466
|
+
|
|
467
|
+
try {
|
|
468
|
+
const result = await this._evaluate(`
|
|
469
|
+
(() => {
|
|
470
|
+
// Look for participant avatars/names in the huddle UI
|
|
471
|
+
const participantEls = document.querySelectorAll(
|
|
472
|
+
'[data-qa="huddle-participant-name"], ' +
|
|
473
|
+
'[class*="huddle"] [class*="participant"] [class*="name"], ' +
|
|
474
|
+
'[class*="huddle"] [class*="avatar"][aria-label]'
|
|
475
|
+
);
|
|
476
|
+
return Array.from(participantEls).map(el =>
|
|
477
|
+
el.textContent?.trim() || el.getAttribute('aria-label') || ''
|
|
478
|
+
).filter(Boolean);
|
|
479
|
+
})()
|
|
480
|
+
`);
|
|
481
|
+
return result || [];
|
|
482
|
+
} catch {
|
|
483
|
+
return [];
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// -------------------------------------------------------------------------
|
|
488
|
+
// CDP communication internals
|
|
489
|
+
// -------------------------------------------------------------------------
|
|
490
|
+
|
|
491
|
+
async _fetchTargets() {
|
|
492
|
+
const resp = await fetch(`${this.cdpUrl}/json`);
|
|
493
|
+
if (!resp.ok) throw new Error(`CDP /json returned ${resp.status}`);
|
|
494
|
+
return resp.json();
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
_connectWebSocket(url) {
|
|
498
|
+
return new Promise((resolve, reject) => {
|
|
499
|
+
this.ws = new WebSocket(url);
|
|
500
|
+
|
|
501
|
+
this.ws.onopen = () => resolve();
|
|
502
|
+
this.ws.onerror = (err) => reject(err);
|
|
503
|
+
|
|
504
|
+
this.ws.onmessage = (event) => {
|
|
505
|
+
try {
|
|
506
|
+
const msg = JSON.parse(typeof event.data === "string" ? event.data : event.data.toString());
|
|
507
|
+
|
|
508
|
+
// Handle response to a pending message
|
|
509
|
+
if (msg.id !== undefined && this.pendingMessages.has(msg.id)) {
|
|
510
|
+
const { resolve, reject, timer } = this.pendingMessages.get(msg.id);
|
|
511
|
+
this.pendingMessages.delete(msg.id);
|
|
512
|
+
clearTimeout(timer);
|
|
513
|
+
|
|
514
|
+
if (msg.error) {
|
|
515
|
+
reject(new Error(msg.error.message));
|
|
516
|
+
} else {
|
|
517
|
+
resolve(msg.result);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
} catch (err) {
|
|
521
|
+
// Ignore parse errors from CDP events we don't care about
|
|
522
|
+
}
|
|
523
|
+
};
|
|
524
|
+
|
|
525
|
+
this.ws.onclose = () => {
|
|
526
|
+
this.connected = false;
|
|
527
|
+
this.emit("disconnected");
|
|
528
|
+
this._scheduleReconnect();
|
|
529
|
+
};
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* Send a CDP command and wait for its response.
|
|
535
|
+
* @param {string} method - CDP method (e.g., "Runtime.evaluate")
|
|
536
|
+
* @param {object} params - CDP method parameters
|
|
537
|
+
* @param {number} timeoutMs - Response timeout
|
|
538
|
+
* @returns {Promise<any>} CDP response result
|
|
539
|
+
*/
|
|
540
|
+
_sendCommand(method, params = {}, timeoutMs = 10000) {
|
|
541
|
+
return new Promise((resolve, reject) => {
|
|
542
|
+
if (!this.ws || this.ws.readyState !== 1) {
|
|
543
|
+
return reject(new Error("WebSocket not connected"));
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
const id = ++this.messageId;
|
|
547
|
+
const timer = setTimeout(() => {
|
|
548
|
+
this.pendingMessages.delete(id);
|
|
549
|
+
reject(new Error(`CDP command ${method} timed out after ${timeoutMs}ms`));
|
|
550
|
+
}, timeoutMs);
|
|
551
|
+
|
|
552
|
+
this.pendingMessages.set(id, { resolve, reject, timer });
|
|
553
|
+
this.ws.send(JSON.stringify({ id, method, params }));
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* Evaluate JavaScript in the Slack renderer context.
|
|
559
|
+
*/
|
|
560
|
+
async _evaluate(expression) {
|
|
561
|
+
const result = await this._sendCommand("Runtime.evaluate", {
|
|
562
|
+
expression,
|
|
563
|
+
returnByValue: true,
|
|
564
|
+
awaitPromise: true,
|
|
565
|
+
});
|
|
566
|
+
return result?.result?.value;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// -------------------------------------------------------------------------
|
|
570
|
+
// System-level keyboard (via AppleScript)
|
|
571
|
+
//
|
|
572
|
+
// Slack's Cmd+K and Cmd+Shift+H are Electron menu accelerators, not DOM
|
|
573
|
+
// keyboard events. CDP's Input.dispatchKeyEvent only fires DOM events, so
|
|
574
|
+
// these shortcuts don't trigger. We use AppleScript to send system-level
|
|
575
|
+
// keystrokes directly to the Slack process.
|
|
576
|
+
// -------------------------------------------------------------------------
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* Bring Slack to front and send a system-level keystroke via AppleScript.
|
|
580
|
+
* @param {string} key - The key character (e.g., "k", "h")
|
|
581
|
+
* @param {string[]} modifiers - e.g. ["command"], ["command", "shift"]
|
|
582
|
+
*/
|
|
583
|
+
async _systemKeystroke(key, modifiers = ["command"]) {
|
|
584
|
+
const using = modifiers.map((m) => `${m} down`).join(", ");
|
|
585
|
+
const script = `
|
|
586
|
+
tell application "Slack" to activate
|
|
587
|
+
delay 0.3
|
|
588
|
+
tell application "System Events"
|
|
589
|
+
keystroke "${key}" using {${using}}
|
|
590
|
+
end tell
|
|
591
|
+
`;
|
|
592
|
+
await execFileAsync("osascript", ["-e", script]);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
/**
|
|
596
|
+
* Type text into the frontmost app via AppleScript (for quick switcher input).
|
|
597
|
+
* @param {string} text
|
|
598
|
+
*/
|
|
599
|
+
async _systemType(text) {
|
|
600
|
+
// Escape special chars for AppleScript string
|
|
601
|
+
const escaped = text.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
602
|
+
const script = `
|
|
603
|
+
tell application "System Events"
|
|
604
|
+
keystroke "${escaped}"
|
|
605
|
+
end tell
|
|
606
|
+
`;
|
|
607
|
+
await execFileAsync("osascript", ["-e", script]);
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
/**
|
|
611
|
+
* Send a single key (Enter, Escape, etc.) via AppleScript.
|
|
612
|
+
* @param {string} keyName - e.g., "return", "escape", "tab"
|
|
613
|
+
*/
|
|
614
|
+
async _systemKeyPress(keyName) {
|
|
615
|
+
const script = `
|
|
616
|
+
tell application "System Events"
|
|
617
|
+
key code ${keyName === "return" ? 36 : keyName === "escape" ? 53 : keyName === "tab" ? 48 : 0}
|
|
618
|
+
end tell
|
|
619
|
+
`;
|
|
620
|
+
await execFileAsync("osascript", ["-e", script]);
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// -------------------------------------------------------------------------
|
|
624
|
+
// CDP-level keyboard / input helpers (for DOM interactions)
|
|
625
|
+
// -------------------------------------------------------------------------
|
|
626
|
+
|
|
627
|
+
/**
|
|
628
|
+
* Send a keyboard shortcut (e.g., Cmd+K, Cmd+Shift+H).
|
|
629
|
+
*/
|
|
630
|
+
async _sendKeyCombo(key, modifiers = []) {
|
|
631
|
+
const keyCode = key.toUpperCase().charCodeAt(0);
|
|
632
|
+
const modFlag =
|
|
633
|
+
(modifiers.includes("Alt") ? 1 : 0) |
|
|
634
|
+
(modifiers.includes("Control") ? 2 : 0) |
|
|
635
|
+
(modifiers.includes("Meta") ? 4 : 0) |
|
|
636
|
+
(modifiers.includes("Shift") ? 8 : 0);
|
|
637
|
+
|
|
638
|
+
// Key down with modifiers
|
|
639
|
+
await this._sendCommand("Input.dispatchKeyEvent", {
|
|
640
|
+
type: "keyDown",
|
|
641
|
+
key,
|
|
642
|
+
code: `Key${key.toUpperCase()}`,
|
|
643
|
+
windowsVirtualKeyCode: keyCode,
|
|
644
|
+
nativeVirtualKeyCode: keyCode,
|
|
645
|
+
modifiers: modFlag,
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
// Small delay
|
|
649
|
+
await this._sleep(50);
|
|
650
|
+
|
|
651
|
+
// Key up
|
|
652
|
+
await this._sendCommand("Input.dispatchKeyEvent", {
|
|
653
|
+
type: "keyUp",
|
|
654
|
+
key,
|
|
655
|
+
code: `Key${key.toUpperCase()}`,
|
|
656
|
+
windowsVirtualKeyCode: keyCode,
|
|
657
|
+
nativeVirtualKeyCode: keyCode,
|
|
658
|
+
modifiers: modFlag,
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
async _sendKey(key) {
|
|
663
|
+
const keyMap = {
|
|
664
|
+
Enter: { code: "Enter", keyCode: 13 },
|
|
665
|
+
Escape: { code: "Escape", keyCode: 27 },
|
|
666
|
+
Tab: { code: "Tab", keyCode: 9 },
|
|
667
|
+
};
|
|
668
|
+
|
|
669
|
+
const mapped = keyMap[key] || { code: `Key${key.toUpperCase()}`, keyCode: key.toUpperCase().charCodeAt(0) };
|
|
670
|
+
|
|
671
|
+
await this._sendCommand("Input.dispatchKeyEvent", {
|
|
672
|
+
type: "keyDown",
|
|
673
|
+
key,
|
|
674
|
+
code: mapped.code,
|
|
675
|
+
windowsVirtualKeyCode: mapped.keyCode,
|
|
676
|
+
nativeVirtualKeyCode: mapped.keyCode,
|
|
677
|
+
});
|
|
678
|
+
await this._sleep(50);
|
|
679
|
+
await this._sendCommand("Input.dispatchKeyEvent", {
|
|
680
|
+
type: "keyUp",
|
|
681
|
+
key,
|
|
682
|
+
code: mapped.code,
|
|
683
|
+
windowsVirtualKeyCode: mapped.keyCode,
|
|
684
|
+
nativeVirtualKeyCode: mapped.keyCode,
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
async _typeText(text) {
|
|
689
|
+
for (const char of text) {
|
|
690
|
+
await this._sendCommand("Input.dispatchKeyEvent", {
|
|
691
|
+
type: "keyDown",
|
|
692
|
+
key: char,
|
|
693
|
+
text: char,
|
|
694
|
+
});
|
|
695
|
+
await this._sendCommand("Input.dispatchKeyEvent", {
|
|
696
|
+
type: "keyUp",
|
|
697
|
+
key: char,
|
|
698
|
+
});
|
|
699
|
+
await this._sleep(30); // Realistic typing speed
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
// -------------------------------------------------------------------------
|
|
704
|
+
// DOM queries for huddle state
|
|
705
|
+
// -------------------------------------------------------------------------
|
|
706
|
+
|
|
707
|
+
async _checkHuddleActive() {
|
|
708
|
+
try {
|
|
709
|
+
// Check main window DOM for huddle indicators
|
|
710
|
+
const mainActive = await this._evaluate(`
|
|
711
|
+
(() => {
|
|
712
|
+
// Primary: huddle workspace dialog has children when active
|
|
713
|
+
const workspace = document.querySelector('div.p-huddle_workspace[role="dialog"]');
|
|
714
|
+
if (workspace && workspace.children.length > 0) return true;
|
|
715
|
+
|
|
716
|
+
// Fallback: look for leave/end buttons
|
|
717
|
+
const leaveBtn = document.querySelector(
|
|
718
|
+
'[aria-label*="Leave huddle"], [aria-label*="End huddle"], ' +
|
|
719
|
+
'[data-qa*="huddle"][data-qa*="leave"], [data-qa*="huddle"][data-qa*="end"]'
|
|
720
|
+
);
|
|
721
|
+
if (leaveBtn) return true;
|
|
722
|
+
|
|
723
|
+
// Fallback: audio element is actively playing
|
|
724
|
+
const activeAudio = [...document.querySelectorAll('audio')].some(a => !a.paused);
|
|
725
|
+
if (activeAudio) return true;
|
|
726
|
+
|
|
727
|
+
// Fallback: title contains 🔊 (active audio indicator)
|
|
728
|
+
if (document.title.includes('🔊')) return true;
|
|
729
|
+
|
|
730
|
+
return false;
|
|
731
|
+
})()
|
|
732
|
+
`);
|
|
733
|
+
if (mainActive) return true;
|
|
734
|
+
|
|
735
|
+
// Check preview window for active huddle UI (the "Leave" button
|
|
736
|
+
// only appears in the preview window once the huddle has started)
|
|
737
|
+
try {
|
|
738
|
+
const targets = await this._fetchTargets();
|
|
739
|
+
const previewTarget = targets.find(t =>
|
|
740
|
+
t.title && (t.title.includes("Huddle:") || t.title.includes("🎤")) && t.type === "page"
|
|
741
|
+
);
|
|
742
|
+
if (previewTarget) return true;
|
|
743
|
+
} catch { /* optional check */ }
|
|
744
|
+
|
|
745
|
+
return false;
|
|
746
|
+
} catch {
|
|
747
|
+
return false;
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
async _checkMuteState() {
|
|
752
|
+
try {
|
|
753
|
+
// Mute button appears inside p-huddle_workspace when huddle is active
|
|
754
|
+
return await this._evaluate(`
|
|
755
|
+
(() => {
|
|
756
|
+
const muteBtn = document.querySelector(
|
|
757
|
+
'.p-huddle_workspace [aria-label*="Mute"], ' +
|
|
758
|
+
'.p-huddle_workspace [aria-label*="mute"], ' +
|
|
759
|
+
'[data-qa*="huddle"][data-qa*="mute"]'
|
|
760
|
+
);
|
|
761
|
+
if (!muteBtn) return null;
|
|
762
|
+
return muteBtn.getAttribute('aria-pressed') === 'true' ||
|
|
763
|
+
muteBtn.getAttribute('aria-label')?.toLowerCase().includes('unmute');
|
|
764
|
+
})()
|
|
765
|
+
`);
|
|
766
|
+
} catch {
|
|
767
|
+
return null;
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
async _clickJoinButton() {
|
|
772
|
+
try {
|
|
773
|
+
// Verified: the start/join button has data-qa="huddle_channel_header_button__start_button"
|
|
774
|
+
// and aria-label="Start huddle in {channel}" (changes to "Join" when active)
|
|
775
|
+
return await this._evaluate(`
|
|
776
|
+
(() => {
|
|
777
|
+
// Primary: the huddle start/join button in channel header
|
|
778
|
+
const startBtn = document.querySelector(
|
|
779
|
+
'[data-qa="huddle_channel_header_button__start_button"]'
|
|
780
|
+
);
|
|
781
|
+
if (startBtn) { startBtn.click(); return true; }
|
|
782
|
+
|
|
783
|
+
// Fallback: any button with "Join" text in a huddle context
|
|
784
|
+
const joinBtns = [...document.querySelectorAll('button, [role="button"]')].filter(el => {
|
|
785
|
+
const text = (el.textContent || '').trim().toLowerCase();
|
|
786
|
+
const label = (el.getAttribute('aria-label') || '').toLowerCase();
|
|
787
|
+
return text === 'join' || label.includes('join huddle') || label.includes('join this huddle');
|
|
788
|
+
});
|
|
789
|
+
if (joinBtns.length > 0) { joinBtns[0].click(); return true; }
|
|
790
|
+
|
|
791
|
+
return false;
|
|
792
|
+
})()
|
|
793
|
+
`);
|
|
794
|
+
} catch {
|
|
795
|
+
return false;
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
async _clickLeaveButton() {
|
|
800
|
+
try {
|
|
801
|
+
return await this._evaluate(`
|
|
802
|
+
(() => {
|
|
803
|
+
// Look inside the huddle workspace dialog for leave/end buttons
|
|
804
|
+
const workspace = document.querySelector('.p-huddle_workspace');
|
|
805
|
+
const scope = workspace || document;
|
|
806
|
+
|
|
807
|
+
const leaveBtns = [...scope.querySelectorAll('button, [role="button"]')].filter(el => {
|
|
808
|
+
const text = (el.textContent || '').trim().toLowerCase();
|
|
809
|
+
const label = (el.getAttribute('aria-label') || '').toLowerCase();
|
|
810
|
+
const qa = (el.getAttribute('data-qa') || '').toLowerCase();
|
|
811
|
+
return text === 'leave' || label.includes('leave huddle') ||
|
|
812
|
+
label.includes('end huddle') || qa.includes('leave') || qa.includes('end');
|
|
813
|
+
});
|
|
814
|
+
if (leaveBtns.length > 0) { leaveBtns[0].click(); return true; }
|
|
815
|
+
return false;
|
|
816
|
+
})()
|
|
817
|
+
`);
|
|
818
|
+
} catch {
|
|
819
|
+
return false;
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
// -------------------------------------------------------------------------
|
|
824
|
+
// Huddle invitation detection (polling)
|
|
825
|
+
// -------------------------------------------------------------------------
|
|
826
|
+
|
|
827
|
+
_startPolling() {
|
|
828
|
+
this._stopPolling();
|
|
829
|
+
this.pollTimer = setInterval(() => this._pollHuddleState(), POLL_INTERVAL_MS);
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
_stopPolling() {
|
|
833
|
+
if (this.pollTimer) {
|
|
834
|
+
clearInterval(this.pollTimer);
|
|
835
|
+
this.pollTimer = null;
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
async _pollHuddleState() {
|
|
840
|
+
if (!this.connected) return;
|
|
841
|
+
|
|
842
|
+
try {
|
|
843
|
+
// Check for incoming huddle invitation popup or active huddle indicator
|
|
844
|
+
const invitation = await this._evaluate(`
|
|
845
|
+
(() => {
|
|
846
|
+
// Method 1: huddle workspace dialog became populated (someone started a huddle)
|
|
847
|
+
const workspace = document.querySelector('div.p-huddle_workspace[role="dialog"]');
|
|
848
|
+
if (workspace && workspace.children.length > 0) {
|
|
849
|
+
// Extract participant info from the huddle workspace
|
|
850
|
+
const nameEl = workspace.querySelector(
|
|
851
|
+
'[class*="name"], [class*="caller"], [class*="participant"]'
|
|
852
|
+
);
|
|
853
|
+
return {
|
|
854
|
+
from: nameEl?.textContent?.trim() || 'Someone',
|
|
855
|
+
channel: 'active-huddle',
|
|
856
|
+
};
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
// Method 2: Incoming notification/toast for huddle invitation
|
|
860
|
+
const toasts = document.querySelectorAll(
|
|
861
|
+
'[class*="huddle"][class*="incoming"], ' +
|
|
862
|
+
'[class*="huddle"][class*="invitation"], ' +
|
|
863
|
+
'[class*="huddle"][class*="ringing"], ' +
|
|
864
|
+
'[class*="huddle_notification"]'
|
|
865
|
+
);
|
|
866
|
+
for (const toast of toasts) {
|
|
867
|
+
const nameEl = toast.querySelector('[class*="name"], [class*="title"]');
|
|
868
|
+
const channelEl = toast.querySelector('[class*="channel"], [class*="subtitle"]');
|
|
869
|
+
return {
|
|
870
|
+
from: nameEl?.textContent?.trim() || 'Unknown',
|
|
871
|
+
channel: channelEl?.textContent?.trim() || 'Unknown',
|
|
872
|
+
};
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
// Method 3: The start button label changed to indicate an active huddle
|
|
876
|
+
const startBtn = document.querySelector('[data-qa="huddle_channel_header_button__start_button"]');
|
|
877
|
+
if (startBtn) {
|
|
878
|
+
const label = startBtn.getAttribute('aria-label') || '';
|
|
879
|
+
if (label.toLowerCase().includes('join')) {
|
|
880
|
+
// Button says "Join huddle" instead of "Start huddle" — someone is in a huddle
|
|
881
|
+
const channelBtn = document.querySelector('[data-qa="channel_name_button"]');
|
|
882
|
+
return {
|
|
883
|
+
from: 'Active huddle',
|
|
884
|
+
channel: channelBtn?.textContent?.trim() || 'current channel',
|
|
885
|
+
};
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
return null;
|
|
890
|
+
})()
|
|
891
|
+
`);
|
|
892
|
+
|
|
893
|
+
if (invitation && !this.huddleActive) {
|
|
894
|
+
this.emit("huddle:invitation", invitation);
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
// Track huddle state transitions
|
|
898
|
+
const isActive = await this._checkHuddleActive();
|
|
899
|
+
if (isActive && !this.huddleActive) {
|
|
900
|
+
this.huddleActive = true;
|
|
901
|
+
this.emit("huddle:joined", { channel: this.currentChannel || "active-huddle" });
|
|
902
|
+
} else if (!isActive && this.huddleActive) {
|
|
903
|
+
// The main window DOM is unreliable for detecting huddle end because
|
|
904
|
+
// the huddle UI runs in a separate Electron window. Only mark as left
|
|
905
|
+
// after multiple consecutive "not active" checks to avoid false alarms.
|
|
906
|
+
if (!this._leaveCheckCount) this._leaveCheckCount = 0;
|
|
907
|
+
this._leaveCheckCount++;
|
|
908
|
+
if (this._leaveCheckCount >= 5) { // 5 polls × 2s = 10 seconds of inactivity
|
|
909
|
+
this.huddleActive = false;
|
|
910
|
+
this._leaveCheckCount = 0;
|
|
911
|
+
this.emit("huddle:left");
|
|
912
|
+
}
|
|
913
|
+
} else {
|
|
914
|
+
this._leaveCheckCount = 0;
|
|
915
|
+
}
|
|
916
|
+
} catch {
|
|
917
|
+
// Polling errors are non-fatal
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
// -------------------------------------------------------------------------
|
|
922
|
+
// Reconnection
|
|
923
|
+
// -------------------------------------------------------------------------
|
|
924
|
+
|
|
925
|
+
_scheduleReconnect() {
|
|
926
|
+
if (this.reconnectTimer) return;
|
|
927
|
+
this.reconnectTimer = setTimeout(async () => {
|
|
928
|
+
this.reconnectTimer = null;
|
|
929
|
+
await this.connect();
|
|
930
|
+
}, RECONNECT_INTERVAL_MS);
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
// -------------------------------------------------------------------------
|
|
934
|
+
// Utilities
|
|
935
|
+
// -------------------------------------------------------------------------
|
|
936
|
+
|
|
937
|
+
_sleep(ms) {
|
|
938
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
export default HuddleController;
|