@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,1045 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Slack Events API Server — Real-Time Event Receiver
|
|
4
|
+
*
|
|
5
|
+
* Receives real-time events from Slack via the bot's Event Subscriptions.
|
|
6
|
+
* Writes inbound events to state/inbox/slack/ in YAML format (matching poller output).
|
|
7
|
+
* Creates priority trigger files for CEO DMs.
|
|
8
|
+
*
|
|
9
|
+
* This replaces polling — Slack pushes events to us instantly.
|
|
10
|
+
*
|
|
11
|
+
* Events handled:
|
|
12
|
+
* - message (DM, channel messages, thread replies — all channel types)
|
|
13
|
+
* - app_mention (the agent @mentioned)
|
|
14
|
+
* - reaction_added (emoji reactions)
|
|
15
|
+
*
|
|
16
|
+
* Deduplication:
|
|
17
|
+
* - Tracks event_id in state/locks/slack-events/ to prevent duplicate processing
|
|
18
|
+
* - Also deduplicates against the poller's tracker in state/slack-thread-tracker/
|
|
19
|
+
*
|
|
20
|
+
* Usage: node scripts/slack-events-server.mjs
|
|
21
|
+
* Requires: SLACK_BOT_TOKEN (or SLACK_TOKEN), SLACK_SIGNING_SECRET in .env
|
|
22
|
+
* Port: 3100 (configurable via SLACK_EVENTS_PORT)
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import http from "node:http";
|
|
26
|
+
import crypto from "node:crypto";
|
|
27
|
+
import fs from "node:fs";
|
|
28
|
+
import path from "node:path";
|
|
29
|
+
import { fileURLToPath } from "node:url";
|
|
30
|
+
|
|
31
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
32
|
+
const ROOT = path.resolve(__dirname, "..");
|
|
33
|
+
|
|
34
|
+
// ── Singleton guard — prevent duplicate instances ────────────────────────────
|
|
35
|
+
try {
|
|
36
|
+
process.env.AGENT_ROOT = process.env.AGENT_ROOT || ROOT;
|
|
37
|
+
const { acquireLock } = await import("../lib/singleton.js");
|
|
38
|
+
if (!acquireLock("slack-events")) {
|
|
39
|
+
console.log("[INIT] Slack events server already running — exiting");
|
|
40
|
+
process.exit(0);
|
|
41
|
+
}
|
|
42
|
+
} catch { /* maestro not available — skip guard */ }
|
|
43
|
+
|
|
44
|
+
// ── Load environment ──────────────────────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
const envPath = path.join(ROOT, ".env");
|
|
47
|
+
if (fs.existsSync(envPath)) {
|
|
48
|
+
const envContent = fs.readFileSync(envPath, "utf-8");
|
|
49
|
+
for (const line of envContent.split("\n")) {
|
|
50
|
+
const match = line.match(/^([A-Z_][A-Z0-9_]*)=(.*)$/);
|
|
51
|
+
if (match) {
|
|
52
|
+
let val = match[2].trim();
|
|
53
|
+
// Strip surrounding quotes
|
|
54
|
+
if (
|
|
55
|
+
(val.startsWith('"') && val.endsWith('"')) ||
|
|
56
|
+
(val.startsWith("'") && val.endsWith("'"))
|
|
57
|
+
) {
|
|
58
|
+
val = val.slice(1, -1);
|
|
59
|
+
}
|
|
60
|
+
process.env[match[1]] = val;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ── Constants ─────────────────────────────────────────────────────────────────
|
|
66
|
+
|
|
67
|
+
const PORT = parseInt(process.env.SLACK_EVENTS_PORT || "3100", 10);
|
|
68
|
+
const SIGNING_SECRET = process.env.SLACK_SIGNING_SECRET || "";
|
|
69
|
+
|
|
70
|
+
// Fail-closed: this endpoint accepts inbound Slack events and injects them into
|
|
71
|
+
// the inbox. Without the signing secret it cannot verify requests, which would
|
|
72
|
+
// let anyone on the internet forge inbound messages. Refuse to start rather
|
|
73
|
+
// than degrade to "skip verification". (Mirrors scripts/cloud-relay/index.mjs.)
|
|
74
|
+
if (!SIGNING_SECRET) {
|
|
75
|
+
console.error("FATAL: SLACK_SIGNING_SECRET env var is required (refusing to start fail-open)");
|
|
76
|
+
process.exit(78); // EX_CONFIG
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const INBOX_DIR = path.join(ROOT, "state/inbox/slack");
|
|
80
|
+
const DEDUP_DIR = path.join(ROOT, "state/locks/slack-events");
|
|
81
|
+
const POLLER_TRACKER_DIR = path.join(ROOT, "state/slack-thread-tracker");
|
|
82
|
+
const TRIGGER_DIR = path.join(ROOT, "state/triggers/priority");
|
|
83
|
+
const LOG_DIR = path.join(ROOT, "logs/polling");
|
|
84
|
+
|
|
85
|
+
// Load the running agent's identity (canonical SOT) so we can filter
|
|
86
|
+
// self-messages and resolve the principal's user ID without hardcoding.
|
|
87
|
+
let _agent = null;
|
|
88
|
+
function loadAgent() {
|
|
89
|
+
if (_agent) return _agent;
|
|
90
|
+
try {
|
|
91
|
+
_agent = JSON.parse(fs.readFileSync(path.join(ROOT, "config/agent.json"), "utf-8"));
|
|
92
|
+
} catch {
|
|
93
|
+
_agent = { firstName: "Agent", slackMemberId: "", fullName: "Agent", principal: {} };
|
|
94
|
+
}
|
|
95
|
+
return _agent;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const AGENT_USER_ID = loadAgent().slackMemberId || "";
|
|
99
|
+
const PRINCIPAL_USER_ID = loadAgent().principal?.slackMemberId || "";
|
|
100
|
+
// DM channel between the principal and the agent — resolved at runtime from
|
|
101
|
+
// Slack API on first DM event. The hardcoded MEHRAN_DM_CHANNEL constant was
|
|
102
|
+
// removed; the server detects DM channels via the Slack channel ID prefix
|
|
103
|
+
// (channels starting with "D" are IM channels).
|
|
104
|
+
|
|
105
|
+
// Known users for name/privilege resolution — built from agent.json so each
|
|
106
|
+
// agent's principal gets `privilege: ceo` and the agent itself gets `system`.
|
|
107
|
+
const KNOWN_USERS = (() => {
|
|
108
|
+
const a = loadAgent();
|
|
109
|
+
const map = {};
|
|
110
|
+
if (a.principal?.slackMemberId) {
|
|
111
|
+
map[a.principal.slackMemberId] = {
|
|
112
|
+
name: (a.principal.fullName || "").toLowerCase().replace(/\s+/g, "-"),
|
|
113
|
+
privilege: "ceo",
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
if (a.slackMemberId) {
|
|
117
|
+
map[a.slackMemberId] = {
|
|
118
|
+
name: (a.fullName || "").toLowerCase().replace(/\s+/g, "-"),
|
|
119
|
+
privilege: "system",
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
return map;
|
|
123
|
+
})();
|
|
124
|
+
|
|
125
|
+
// Monitored channels (matches poller/slack-poller.mjs)
|
|
126
|
+
const CHANNEL_INFO = {
|
|
127
|
+
C0ARASMQE1W: { name: "ceo-office", priority: "critical" },
|
|
128
|
+
C099NT6NKK7: { name: "engineering", priority: "high" },
|
|
129
|
+
C097N5R8HUJ: { name: "all-company", priority: "high" },
|
|
130
|
+
C09916RV399: { name: "engineering-platform", priority: "high" },
|
|
131
|
+
C0AQEGXKYT0: { name: "regulatory-remediation", priority: "high" },
|
|
132
|
+
C0AQGK5TLNN: { name: "strategic-initiative", priority: "high" },
|
|
133
|
+
C0AQL7QL2G4: { name: "legal-compliance", priority: "high" },
|
|
134
|
+
C0996CRGF1S: { name: "company-app", priority: "normal" },
|
|
135
|
+
C0992UF9469: { name: "platform", priority: "normal" },
|
|
136
|
+
C0ARAU328JU: { name: "general", priority: "normal" },
|
|
137
|
+
C09CTAFUB09: { name: "product", priority: "normal" },
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const ATTACHMENTS_DIR = path.join(ROOT, "state/inbox/attachments");
|
|
141
|
+
|
|
142
|
+
// Ensure attachments directory exists
|
|
143
|
+
fs.mkdirSync(ATTACHMENTS_DIR, { recursive: true });
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Extract attachment metadata from Slack event files array.
|
|
147
|
+
* Also attempts to download files for local access by Claude Code sessions.
|
|
148
|
+
*/
|
|
149
|
+
function extractAttachments(files) {
|
|
150
|
+
if (!files || !Array.isArray(files) || files.length === 0) return [];
|
|
151
|
+
const token = process.env.SLACK_BOT_TOKEN || process.env.SLACK_TOKEN || "";
|
|
152
|
+
return files.map((f) => {
|
|
153
|
+
const att = {
|
|
154
|
+
id: f.id || "",
|
|
155
|
+
name: f.name || "unnamed",
|
|
156
|
+
mimetype: f.mimetype || f.filetype || "unknown",
|
|
157
|
+
size: f.size || 0,
|
|
158
|
+
url_private: f.url_private || "",
|
|
159
|
+
local_path: null,
|
|
160
|
+
};
|
|
161
|
+
// Attempt synchronous-style download via fetch (non-blocking best-effort)
|
|
162
|
+
if (token && (f.url_private_download || f.url_private)) {
|
|
163
|
+
const safeId = (f.id || "unknown").replace(/[^a-zA-Z0-9_-]/g, "");
|
|
164
|
+
const safeName = (f.name || "file").replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
165
|
+
const localPath = path.join(ATTACHMENTS_DIR, `${safeId}-${safeName}`);
|
|
166
|
+
att.local_path = localPath; // Set optimistically; download happens async
|
|
167
|
+
// Fire-and-forget async download
|
|
168
|
+
downloadAttachment(f.url_private_download || f.url_private, localPath, token);
|
|
169
|
+
}
|
|
170
|
+
return att;
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Download a Slack attachment to local storage (async, fire-and-forget).
|
|
176
|
+
*/
|
|
177
|
+
async function downloadAttachment(url, localPath, token) {
|
|
178
|
+
try {
|
|
179
|
+
if (fs.existsSync(localPath)) return; // Already downloaded
|
|
180
|
+
const res = await fetch(url, { headers: { Authorization: `Bearer ${token}` } });
|
|
181
|
+
if (!res.ok) {
|
|
182
|
+
log("WARN", `Attachment download failed: HTTP ${res.status}`, { url });
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
const buffer = Buffer.from(await res.arrayBuffer());
|
|
186
|
+
fs.writeFileSync(localPath, buffer);
|
|
187
|
+
log("INFO", `Attachment downloaded: ${path.basename(localPath)}`, { size: buffer.length });
|
|
188
|
+
} catch (err) {
|
|
189
|
+
log("WARN", `Attachment download error: ${err.message}`, { url });
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// ── Edit Deduplication ───────────────────────────────────────────────────────
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Calculate word-level similarity between two strings (Jaccard-like).
|
|
197
|
+
* Returns 0.0 (completely different) to 1.0 (identical).
|
|
198
|
+
* Used to distinguish typo fixes from material edits.
|
|
199
|
+
*/
|
|
200
|
+
function wordSimilarity(a, b) {
|
|
201
|
+
if (a === b) return 1.0;
|
|
202
|
+
if (!a && !b) return 1.0;
|
|
203
|
+
if (!a || !b) return 0.0;
|
|
204
|
+
|
|
205
|
+
const wordsA = a.toLowerCase().split(/\s+/).filter(Boolean);
|
|
206
|
+
const wordsB = b.toLowerCase().split(/\s+/).filter(Boolean);
|
|
207
|
+
|
|
208
|
+
if (wordsA.length === 0 && wordsB.length === 0) return 1.0;
|
|
209
|
+
if (wordsA.length === 0 || wordsB.length === 0) return 0.0;
|
|
210
|
+
|
|
211
|
+
const setA = new Set(wordsA);
|
|
212
|
+
const setB = new Set(wordsB);
|
|
213
|
+
const intersection = new Set([...setA].filter((w) => setB.has(w)));
|
|
214
|
+
const union = new Set([...setA, ...setB]);
|
|
215
|
+
|
|
216
|
+
return intersection.size / union.size;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Mark an existing inbox item as superseded by a message edit.
|
|
221
|
+
* Renames the YAML file with a .superseded suffix so the inbox processor
|
|
222
|
+
* skips it and processes only the new version.
|
|
223
|
+
*/
|
|
224
|
+
function supersedeInboxItem(channelId, messageTs) {
|
|
225
|
+
try {
|
|
226
|
+
const tsFormatted = messageTs.replace(".", "-");
|
|
227
|
+
const files = fs.readdirSync(INBOX_DIR);
|
|
228
|
+
for (const file of files) {
|
|
229
|
+
if (file.includes(tsFormatted) && file.endsWith(".yaml") && !file.endsWith(".superseded")) {
|
|
230
|
+
const oldPath = path.join(INBOX_DIR, file);
|
|
231
|
+
const newPath = path.join(INBOX_DIR, file + ".superseded");
|
|
232
|
+
fs.renameSync(oldPath, newPath);
|
|
233
|
+
log("INFO", `Superseded inbox item: ${file}`, { channel: channelId, ts: messageTs });
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
} catch (err) {
|
|
237
|
+
log("WARN", `Failed to supersede inbox item: ${err.message}`, { channel: channelId, ts: messageTs });
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Dedup cache: keep event_ids in memory for fast lookup (last 10,000)
|
|
242
|
+
const eventIdCache = new Set();
|
|
243
|
+
const MAX_CACHE_SIZE = 10000;
|
|
244
|
+
|
|
245
|
+
// Stats for health endpoint
|
|
246
|
+
const stats = {
|
|
247
|
+
started_at: new Date().toISOString(),
|
|
248
|
+
events_received: 0,
|
|
249
|
+
events_processed: 0,
|
|
250
|
+
events_deduplicated: 0,
|
|
251
|
+
events_skipped_bot: 0,
|
|
252
|
+
last_event_at: null,
|
|
253
|
+
errors: 0,
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
// ── Ensure directories exist ──────────────────────────────────────────────────
|
|
257
|
+
|
|
258
|
+
for (const dir of [INBOX_DIR, DEDUP_DIR, TRIGGER_DIR, LOG_DIR]) {
|
|
259
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
263
|
+
|
|
264
|
+
function resolveName(userId) {
|
|
265
|
+
return KNOWN_USERS[userId]?.name || userId;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function resolvePrivilege(userId) {
|
|
269
|
+
return KNOWN_USERS[userId]?.privilege || "unknown";
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function resolveChannelName(channelId, channelType) {
|
|
273
|
+
if (CHANNEL_INFO[channelId]) {
|
|
274
|
+
return CHANNEL_INFO[channelId].name;
|
|
275
|
+
}
|
|
276
|
+
if (channelType === "im") {
|
|
277
|
+
return `dm/${channelId}`;
|
|
278
|
+
}
|
|
279
|
+
return channelId;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Verify Slack request signature (v0 HMAC-SHA256)
|
|
284
|
+
* Returns true if valid, false if invalid.
|
|
285
|
+
* Fails closed: a missing secret must never degrade to "skip verification".
|
|
286
|
+
* (The boot guard already refuses to start without it; this is defence in depth.)
|
|
287
|
+
*/
|
|
288
|
+
function verifySlackSignature(timestamp, body, signature) {
|
|
289
|
+
if (!SIGNING_SECRET) return false;
|
|
290
|
+
|
|
291
|
+
// Reject requests older than 5 minutes (replay protection)
|
|
292
|
+
const now = Math.floor(Date.now() / 1000);
|
|
293
|
+
if (Math.abs(now - parseInt(timestamp, 10)) > 300) {
|
|
294
|
+
log("WARN", "Request timestamp too old, possible replay attack");
|
|
295
|
+
return false;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const sigBasestring = `v0:${timestamp}:${body}`;
|
|
299
|
+
const mySignature =
|
|
300
|
+
"v0=" +
|
|
301
|
+
crypto
|
|
302
|
+
.createHmac("sha256", SIGNING_SECRET)
|
|
303
|
+
.update(sigBasestring, "utf8")
|
|
304
|
+
.digest("hex");
|
|
305
|
+
|
|
306
|
+
try {
|
|
307
|
+
return crypto.timingSafeEqual(
|
|
308
|
+
Buffer.from(mySignature),
|
|
309
|
+
Buffer.from(signature || ""),
|
|
310
|
+
);
|
|
311
|
+
} catch {
|
|
312
|
+
return false;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Check if an event has already been processed.
|
|
318
|
+
* Uses both in-memory cache and filesystem dedup directory.
|
|
319
|
+
*/
|
|
320
|
+
function isDuplicate(eventId) {
|
|
321
|
+
if (!eventId) return false;
|
|
322
|
+
|
|
323
|
+
// Check memory cache first
|
|
324
|
+
if (eventIdCache.has(eventId)) return true;
|
|
325
|
+
|
|
326
|
+
// Check filesystem
|
|
327
|
+
const dedupFile = path.join(DEDUP_DIR, eventId);
|
|
328
|
+
if (fs.existsSync(dedupFile)) {
|
|
329
|
+
eventIdCache.add(eventId);
|
|
330
|
+
return true;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
return false;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Mark an event as processed for deduplication.
|
|
338
|
+
*/
|
|
339
|
+
function markProcessed(eventId) {
|
|
340
|
+
if (!eventId) return;
|
|
341
|
+
|
|
342
|
+
// Add to memory cache
|
|
343
|
+
eventIdCache.add(eventId);
|
|
344
|
+
|
|
345
|
+
// Evict oldest entries if cache is too large
|
|
346
|
+
if (eventIdCache.size > MAX_CACHE_SIZE) {
|
|
347
|
+
const iterator = eventIdCache.values();
|
|
348
|
+
for (let i = 0; i < 1000; i++) {
|
|
349
|
+
eventIdCache.delete(iterator.next().value);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Write to filesystem
|
|
354
|
+
try {
|
|
355
|
+
fs.writeFileSync(path.join(DEDUP_DIR, eventId), String(Date.now()));
|
|
356
|
+
} catch (err) {
|
|
357
|
+
log("ERROR", `Failed to write dedup file for ${eventId}: ${err.message}`);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Also mark in the poller's tracker so the comms-monitor.sh doesn't re-process.
|
|
363
|
+
*/
|
|
364
|
+
function markInPollerTracker(messageTs) {
|
|
365
|
+
if (!messageTs) return;
|
|
366
|
+
try {
|
|
367
|
+
const trackerFile = path.join(POLLER_TRACKER_DIR, messageTs);
|
|
368
|
+
if (!fs.existsSync(trackerFile)) {
|
|
369
|
+
fs.mkdirSync(POLLER_TRACKER_DIR, { recursive: true });
|
|
370
|
+
fs.writeFileSync(trackerFile, messageTs);
|
|
371
|
+
}
|
|
372
|
+
} catch {
|
|
373
|
+
// Non-critical — poller will just reprocess
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Write structured log entry
|
|
379
|
+
*/
|
|
380
|
+
function log(level, message, data = {}) {
|
|
381
|
+
const entry = {
|
|
382
|
+
timestamp: new Date().toISOString(),
|
|
383
|
+
service: "slack-events-server",
|
|
384
|
+
level,
|
|
385
|
+
message,
|
|
386
|
+
...data,
|
|
387
|
+
};
|
|
388
|
+
const line = JSON.stringify(entry);
|
|
389
|
+
console.log(`[${level}] ${message}`);
|
|
390
|
+
|
|
391
|
+
// Append to daily log file
|
|
392
|
+
try {
|
|
393
|
+
const today = new Date().toISOString().split("T")[0];
|
|
394
|
+
const logFile = path.join(LOG_DIR, `${today}-slack-events.jsonl`);
|
|
395
|
+
fs.appendFileSync(logFile, line + "\n");
|
|
396
|
+
} catch {
|
|
397
|
+
// Logging failure should not break event processing
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// ── Inbox Writers ─────────────────────────────────────────────────────────────
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Write an inbox item in YAML format — matching the poller's output exactly.
|
|
405
|
+
* This ensures the inbox processor handles events from both sources identically.
|
|
406
|
+
*/
|
|
407
|
+
function writeInboxYaml(item) {
|
|
408
|
+
const ts = (item.timestamp || new Date().toISOString()).replace(/[:.]/g, "-");
|
|
409
|
+
const filename = `${ts}-${item.id}.yaml`;
|
|
410
|
+
const filepath = path.join(INBOX_DIR, filename);
|
|
411
|
+
|
|
412
|
+
const content = (item.content || "").replace(/\n/g, "\n ");
|
|
413
|
+
let yaml = `id: "${item.id}"
|
|
414
|
+
service: "${item.service}"
|
|
415
|
+
channel: "${item.channel}"
|
|
416
|
+
sender: "${item.sender}"
|
|
417
|
+
sender_privilege: "${item.sender_privilege}"
|
|
418
|
+
timestamp: "${item.timestamp}"
|
|
419
|
+
subject: "${item.subject || ""}"
|
|
420
|
+
content: |
|
|
421
|
+
${content}
|
|
422
|
+
thread_id: "${item.thread_id || ""}"
|
|
423
|
+
is_reply: ${item.is_reply || false}
|
|
424
|
+
is_voice_transcript: false
|
|
425
|
+
priority_signals:
|
|
426
|
+
from_ceo: ${item.priority_signals?.from_ceo || false}
|
|
427
|
+
tagged_urgent: ${item.priority_signals?.tagged_urgent || false}
|
|
428
|
+
contains_deadline: ${item.priority_signals?.contains_deadline || false}
|
|
429
|
+
mentions_agent: ${item.priority_signals?.mentions_agent || false}
|
|
430
|
+
raw_ref: "${item.raw_ref || ""}"
|
|
431
|
+
source: "events-api"
|
|
432
|
+
`;
|
|
433
|
+
|
|
434
|
+
// Add attachments if present
|
|
435
|
+
if (item.attachments && item.attachments.length > 0) {
|
|
436
|
+
yaml += `attachments:\n`;
|
|
437
|
+
for (const att of item.attachments) {
|
|
438
|
+
yaml += ` - id: "${att.id}"\n`;
|
|
439
|
+
yaml += ` name: "${att.name}"\n`;
|
|
440
|
+
yaml += ` mimetype: "${att.mimetype}"\n`;
|
|
441
|
+
yaml += ` size: ${att.size}\n`;
|
|
442
|
+
if (att.local_path) {
|
|
443
|
+
yaml += ` local_path: "${att.local_path}"\n`;
|
|
444
|
+
}
|
|
445
|
+
if (att.url_private) {
|
|
446
|
+
yaml += ` url_private: "${att.url_private}"\n`;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
fs.writeFileSync(filepath, yaml);
|
|
452
|
+
log("INFO", `Inbox item written: ${filename}`, {
|
|
453
|
+
event_type: item._eventType,
|
|
454
|
+
sender: item.sender,
|
|
455
|
+
channel: item.channel,
|
|
456
|
+
});
|
|
457
|
+
return filepath;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Write a priority trigger file for CEO DMs and urgent items.
|
|
462
|
+
* This triggers immediate session pickup by the inbox processor.
|
|
463
|
+
*/
|
|
464
|
+
function writePriorityTrigger(event, reason) {
|
|
465
|
+
const ts = Date.now();
|
|
466
|
+
const triggerFile = path.join(TRIGGER_DIR, `ceo-dm-${ts}.yaml`);
|
|
467
|
+
const content = (event.text || "").replace(/\n/g, "\n ");
|
|
468
|
+
const triggerContent = `type: priority_trigger
|
|
469
|
+
reason: "${reason}"
|
|
470
|
+
source: "slack:${event.channel}:${event.ts}"
|
|
471
|
+
sender: "${resolveName(event.user)}"
|
|
472
|
+
channel: "${resolveChannelName(event.channel, event.channel_type)}"
|
|
473
|
+
timestamp: "${new Date().toISOString()}"
|
|
474
|
+
detected_by: slack-events-server
|
|
475
|
+
content: |
|
|
476
|
+
${content}
|
|
477
|
+
`;
|
|
478
|
+
fs.writeFileSync(triggerFile, triggerContent);
|
|
479
|
+
log("INFO", `Priority trigger written: ceo-dm-${ts}.yaml`, { reason });
|
|
480
|
+
return triggerFile;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// ── Event Handlers ────────────────────────────────────────────────────────────
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* Process a message event (DM, channel, thread reply — any channel type).
|
|
487
|
+
*/
|
|
488
|
+
function handleMessageEvent(event) {
|
|
489
|
+
const userId = event.user || "";
|
|
490
|
+
// Skip the agent's own messages — defensive check (outer dispatch also filters,
|
|
491
|
+
// but message_changed normalization can bypass it)
|
|
492
|
+
if (userId === AGENT_USER_ID) return;
|
|
493
|
+
|
|
494
|
+
const msgText = event.text || "";
|
|
495
|
+
const channelId = event.channel || "";
|
|
496
|
+
const channelType = event.channel_type || "";
|
|
497
|
+
const isThread = !!event.thread_ts && event.thread_ts !== event.ts;
|
|
498
|
+
const isDM = channelType === "im";
|
|
499
|
+
const isPrincipal = userId === PRINCIPAL_USER_ID;
|
|
500
|
+
// The principal's DM channel is whichever IM channel they happen to use to
|
|
501
|
+
// contact the agent — Slack assigns DM channel IDs dynamically. Detect
|
|
502
|
+
// principal-DM context from `isPrincipal && isDM` rather than a hardcode.
|
|
503
|
+
const isPrincipalDmChannel = isPrincipal && isDM;
|
|
504
|
+
const agentFirstName = (loadAgent().firstName || "").toLowerCase();
|
|
505
|
+
const mentionsAgent =
|
|
506
|
+
(agentFirstName && msgText.toLowerCase().includes(agentFirstName)) ||
|
|
507
|
+
(AGENT_USER_ID && msgText.includes(`<@${AGENT_USER_ID}>`));
|
|
508
|
+
const isUrgent = /\b(urgent|emergency|asap|blocker|critical)\b/i.test(
|
|
509
|
+
msgText,
|
|
510
|
+
);
|
|
511
|
+
|
|
512
|
+
// Determine channel name and subject
|
|
513
|
+
let channelName, subject;
|
|
514
|
+
if (isDM) {
|
|
515
|
+
channelName = `dm/${resolveName(userId)}`;
|
|
516
|
+
subject = `DM from ${resolveName(userId)}`;
|
|
517
|
+
} else {
|
|
518
|
+
channelName = resolveChannelName(channelId, channelType);
|
|
519
|
+
subject = `#${channelName}`;
|
|
520
|
+
if (isThread) {
|
|
521
|
+
subject += " (thread reply)";
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
log("INFO", `Message event`, {
|
|
526
|
+
from: resolveName(userId),
|
|
527
|
+
channel: channelName,
|
|
528
|
+
isDM,
|
|
529
|
+
isThread,
|
|
530
|
+
isPrincipal,
|
|
531
|
+
mentionsAgent,
|
|
532
|
+
isUrgent,
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
// Extract attachment metadata from event.files (Slack Events API includes these)
|
|
536
|
+
const attachments = extractAttachments(event.files);
|
|
537
|
+
|
|
538
|
+
// Build inbox item matching poller format
|
|
539
|
+
const msgTs = event.ts || "";
|
|
540
|
+
const inboxItem = {
|
|
541
|
+
id: msgTs.replace(".", "-"),
|
|
542
|
+
service: "slack",
|
|
543
|
+
channel: channelName,
|
|
544
|
+
sender: resolveName(userId),
|
|
545
|
+
sender_privilege: resolvePrivilege(userId),
|
|
546
|
+
timestamp: new Date(parseFloat(msgTs) * 1000).toISOString(),
|
|
547
|
+
subject,
|
|
548
|
+
content: msgText,
|
|
549
|
+
thread_id: event.thread_ts || "",
|
|
550
|
+
is_reply: isThread,
|
|
551
|
+
attachments: attachments.length > 0 ? attachments : undefined,
|
|
552
|
+
priority_signals: {
|
|
553
|
+
from_ceo: isPrincipal,
|
|
554
|
+
tagged_urgent: isUrgent,
|
|
555
|
+
contains_deadline: false,
|
|
556
|
+
mentions_agent: mentionsAgent || isDM,
|
|
557
|
+
},
|
|
558
|
+
raw_ref: `slack:${channelId}:${msgTs}`,
|
|
559
|
+
_eventType: isThread ? "thread_reply" : isDM ? "dm" : "channel_message",
|
|
560
|
+
};
|
|
561
|
+
|
|
562
|
+
writeInboxYaml(inboxItem);
|
|
563
|
+
|
|
564
|
+
// Mark in poller tracker to prevent comms-monitor.sh from re-processing
|
|
565
|
+
markInPollerTracker(msgTs);
|
|
566
|
+
|
|
567
|
+
// Write priority trigger for CEO DMs, @mentions, and urgent messages
|
|
568
|
+
if (isPrincipal && (isDM || isPrincipalDmChannel)) {
|
|
569
|
+
writePriorityTrigger(event, "CEO DM via Events API");
|
|
570
|
+
} else if (isPrincipal && !isDM) {
|
|
571
|
+
writePriorityTrigger(event, "CEO channel message via Events API");
|
|
572
|
+
} else if (mentionsAgent) {
|
|
573
|
+
writePriorityTrigger(event, "Agent @mentioned via Events API");
|
|
574
|
+
} else if (isUrgent) {
|
|
575
|
+
writePriorityTrigger(event, "Urgent message via Events API");
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
/**
|
|
580
|
+
* Process an app_mention event (@-mention of the running agent).
|
|
581
|
+
*/
|
|
582
|
+
function handleMentionEvent(event) {
|
|
583
|
+
const msgText = event.text || "";
|
|
584
|
+
const userId = event.user || "";
|
|
585
|
+
const channelId = event.channel || "";
|
|
586
|
+
const isPrincipal = userId === PRINCIPAL_USER_ID;
|
|
587
|
+
const isUrgent = /\b(urgent|emergency|asap|blocker|critical)\b/i.test(
|
|
588
|
+
msgText,
|
|
589
|
+
);
|
|
590
|
+
const channelName = resolveChannelName(channelId, "channel");
|
|
591
|
+
const msgTs = event.ts || "";
|
|
592
|
+
|
|
593
|
+
log("INFO", `App mention event`, {
|
|
594
|
+
from: resolveName(userId),
|
|
595
|
+
channel: channelName,
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
const inboxItem = {
|
|
599
|
+
id: msgTs.replace(".", "-"),
|
|
600
|
+
service: "slack",
|
|
601
|
+
channel: channelName,
|
|
602
|
+
sender: resolveName(userId),
|
|
603
|
+
sender_privilege: resolvePrivilege(userId),
|
|
604
|
+
timestamp: new Date(parseFloat(msgTs) * 1000).toISOString(),
|
|
605
|
+
subject: `@${loadAgent().firstName || "Agent"} in #${channelName}`,
|
|
606
|
+
content: msgText,
|
|
607
|
+
thread_id: event.thread_ts || "",
|
|
608
|
+
is_reply: !!event.thread_ts && event.thread_ts !== event.ts,
|
|
609
|
+
priority_signals: {
|
|
610
|
+
from_ceo: isPrincipal,
|
|
611
|
+
tagged_urgent: isUrgent,
|
|
612
|
+
contains_deadline: false,
|
|
613
|
+
mentions_agent: true,
|
|
614
|
+
},
|
|
615
|
+
raw_ref: `slack:${channelId}:${msgTs}`,
|
|
616
|
+
_eventType: "mention",
|
|
617
|
+
};
|
|
618
|
+
|
|
619
|
+
writeInboxYaml(inboxItem);
|
|
620
|
+
markInPollerTracker(msgTs);
|
|
621
|
+
|
|
622
|
+
// Mentions always get a priority trigger
|
|
623
|
+
writePriorityTrigger(
|
|
624
|
+
event,
|
|
625
|
+
isPrincipal
|
|
626
|
+
? "CEO @mention via Events API"
|
|
627
|
+
: "Agent @mentioned via Events API",
|
|
628
|
+
);
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
/**
|
|
632
|
+
* Process a reaction_added event.
|
|
633
|
+
*/
|
|
634
|
+
function handleReactionEvent(event) {
|
|
635
|
+
const userId = event.user || "";
|
|
636
|
+
const reaction = event.reaction || "";
|
|
637
|
+
const itemTs = event.item?.ts || "";
|
|
638
|
+
const itemChannel = event.item?.channel || "";
|
|
639
|
+
const isPrincipal = userId === PRINCIPAL_USER_ID;
|
|
640
|
+
const channelName = resolveChannelName(itemChannel, "channel");
|
|
641
|
+
|
|
642
|
+
log("INFO", `Reaction event: :${reaction}: from ${resolveName(userId)}`, {
|
|
643
|
+
channel: channelName,
|
|
644
|
+
on_message: itemTs,
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
// Write reaction as inbox item — reactions from CEO are priority signals
|
|
648
|
+
const inboxItem = {
|
|
649
|
+
id: `${itemTs}-${reaction}-${userId}`.replace(/\./g, "-"),
|
|
650
|
+
service: "slack",
|
|
651
|
+
channel: channelName,
|
|
652
|
+
sender: resolveName(userId),
|
|
653
|
+
sender_privilege: resolvePrivilege(userId),
|
|
654
|
+
timestamp: new Date().toISOString(),
|
|
655
|
+
subject: `:${reaction}: on message in #${channelName}`,
|
|
656
|
+
content: `Reaction :${reaction}: added by ${resolveName(userId)} on message ${itemTs}`,
|
|
657
|
+
thread_id: "",
|
|
658
|
+
is_reply: false,
|
|
659
|
+
priority_signals: {
|
|
660
|
+
from_ceo: isPrincipal,
|
|
661
|
+
tagged_urgent: false,
|
|
662
|
+
contains_deadline: false,
|
|
663
|
+
mentions_agent: false,
|
|
664
|
+
},
|
|
665
|
+
raw_ref: `slack:${itemChannel}:${itemTs}:reaction:${reaction}`,
|
|
666
|
+
_eventType: "reaction",
|
|
667
|
+
};
|
|
668
|
+
|
|
669
|
+
writeInboxYaml(inboxItem);
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
// ── Huddle Event Handler ──────────────────────────────────────────────────────
|
|
673
|
+
|
|
674
|
+
/**
|
|
675
|
+
* Handle user_huddle_changed events — forwarded to the huddle server
|
|
676
|
+
* for real-time huddle detection and auto-join.
|
|
677
|
+
*/
|
|
678
|
+
function handleHuddleEvent(event) {
|
|
679
|
+
const userId = event.user || "";
|
|
680
|
+
const huddleState = event.huddle_state || "";
|
|
681
|
+
|
|
682
|
+
// Don't react to the agent's own huddle changes
|
|
683
|
+
if (userId === AGENT_USER_ID) return;
|
|
684
|
+
|
|
685
|
+
const userName = resolveName(userId);
|
|
686
|
+
|
|
687
|
+
log("INFO", `Huddle event: ${userName} → ${huddleState}`, {
|
|
688
|
+
user: userId,
|
|
689
|
+
huddle_state: huddleState,
|
|
690
|
+
});
|
|
691
|
+
|
|
692
|
+
// Forward to huddle server's event listener
|
|
693
|
+
const huddleServerUrl = `http://127.0.0.1:${process.env.HUDDLE_EVENTS_PORT || 3200}/huddle-event`;
|
|
694
|
+
|
|
695
|
+
const payload = JSON.stringify({
|
|
696
|
+
user: userId,
|
|
697
|
+
user_name: userName,
|
|
698
|
+
huddle_state: huddleState,
|
|
699
|
+
// user_huddle_changed doesn't include channel info directly,
|
|
700
|
+
// but we can derive it from context if available
|
|
701
|
+
channel: event.channel || null,
|
|
702
|
+
channel_type: event.channel_type || null,
|
|
703
|
+
timestamp: new Date().toISOString(),
|
|
704
|
+
});
|
|
705
|
+
|
|
706
|
+
// Fire-and-forget POST to huddle server
|
|
707
|
+
fetch(huddleServerUrl, {
|
|
708
|
+
method: "POST",
|
|
709
|
+
headers: { "Content-Type": "application/json" },
|
|
710
|
+
body: payload,
|
|
711
|
+
}).catch((err) => {
|
|
712
|
+
// Huddle server may not be running — that's fine
|
|
713
|
+
log("DEBUG", `Huddle server unreachable: ${err.message}`);
|
|
714
|
+
});
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
// ── Main Event Router ─────────────────────────────────────────────────────────
|
|
718
|
+
|
|
719
|
+
/**
|
|
720
|
+
* Route an incoming Slack event to the appropriate handler.
|
|
721
|
+
*/
|
|
722
|
+
function handleEvent(payload) {
|
|
723
|
+
const event = payload.event;
|
|
724
|
+
if (!event) return;
|
|
725
|
+
|
|
726
|
+
const eventId = payload.event_id;
|
|
727
|
+
|
|
728
|
+
// Deduplication check
|
|
729
|
+
if (isDuplicate(eventId)) {
|
|
730
|
+
stats.events_deduplicated++;
|
|
731
|
+
log("DEBUG", `Duplicate event skipped: ${eventId}`);
|
|
732
|
+
return;
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
stats.events_received++;
|
|
736
|
+
stats.last_event_at = new Date().toISOString();
|
|
737
|
+
|
|
738
|
+
// Skip bot messages (including the agent's own messages)
|
|
739
|
+
if (event.bot_id || event.subtype === "bot_message") {
|
|
740
|
+
stats.events_skipped_bot++;
|
|
741
|
+
log("DEBUG", `Bot message skipped`, {
|
|
742
|
+
bot_id: event.bot_id,
|
|
743
|
+
subtype: event.subtype,
|
|
744
|
+
});
|
|
745
|
+
markProcessed(eventId);
|
|
746
|
+
return;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
// Skip the agent's own messages
|
|
750
|
+
if (event.user === AGENT_USER_ID) {
|
|
751
|
+
stats.events_skipped_bot++;
|
|
752
|
+
markProcessed(eventId);
|
|
753
|
+
return;
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
// Handle message_changed: Slack sends both old and new text.
|
|
757
|
+
// Compare to determine if the edit is material (rewrite) or immaterial (typo fix).
|
|
758
|
+
if (event.subtype === "message_changed" && event.message) {
|
|
759
|
+
const oldText = event.previous_message?.text || "";
|
|
760
|
+
const newText = event.message.text || "";
|
|
761
|
+
const similarity = wordSimilarity(oldText, newText);
|
|
762
|
+
|
|
763
|
+
if (similarity >= 0.90) {
|
|
764
|
+
// Immaterial edit (typo fix, formatting) — skip reprocessing
|
|
765
|
+
stats.events_deduplicated++;
|
|
766
|
+
log("INFO", `Immaterial edit skipped (${(similarity * 100).toFixed(0)}% similar)`, {
|
|
767
|
+
channel: event.channel,
|
|
768
|
+
ts: event.message.ts,
|
|
769
|
+
});
|
|
770
|
+
markProcessed(eventId);
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
// Material edit — supersede the old inbox item and process the new version
|
|
775
|
+
log("INFO", `Material edit detected (${(similarity * 100).toFixed(0)}% similar)`, {
|
|
776
|
+
channel: event.channel,
|
|
777
|
+
ts: event.message.ts,
|
|
778
|
+
});
|
|
779
|
+
supersedeInboxItem(event.channel, event.message.ts);
|
|
780
|
+
|
|
781
|
+
// Normalize event shape for the message handler
|
|
782
|
+
event.text = newText;
|
|
783
|
+
event.user = event.message.user || event.user;
|
|
784
|
+
event.ts = event.message.ts || event.ts;
|
|
785
|
+
event.thread_ts = event.message.thread_ts || event.thread_ts;
|
|
786
|
+
event.files = event.message.files || event.files;
|
|
787
|
+
event._is_edit = true;
|
|
788
|
+
|
|
789
|
+
// Re-check after normalization: the original event.user for
|
|
790
|
+
// message_changed is often empty/system; the real author is in
|
|
791
|
+
// event.message.user. Without this, the agent's own edited messages
|
|
792
|
+
// bypass the earlier AGENT_USER_ID filter and trigger self-replies.
|
|
793
|
+
if (event.user === AGENT_USER_ID) {
|
|
794
|
+
stats.events_skipped_bot++;
|
|
795
|
+
markProcessed(eventId);
|
|
796
|
+
return;
|
|
797
|
+
}
|
|
798
|
+
// Fall through to message handler with updated content
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
// Skip message subtypes that are not actual user messages
|
|
802
|
+
// (channel_join, channel_leave, channel_topic, etc.)
|
|
803
|
+
const ignoredSubtypes = new Set([
|
|
804
|
+
"channel_join",
|
|
805
|
+
"channel_leave",
|
|
806
|
+
"channel_topic",
|
|
807
|
+
"channel_purpose",
|
|
808
|
+
"channel_name",
|
|
809
|
+
"channel_archive",
|
|
810
|
+
"channel_unarchive",
|
|
811
|
+
"bot_add",
|
|
812
|
+
"bot_remove",
|
|
813
|
+
"pinned_item",
|
|
814
|
+
"unpinned_item",
|
|
815
|
+
]);
|
|
816
|
+
if (event.subtype && ignoredSubtypes.has(event.subtype)) {
|
|
817
|
+
markProcessed(eventId);
|
|
818
|
+
return;
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
// Route by event type
|
|
822
|
+
try {
|
|
823
|
+
switch (event.type) {
|
|
824
|
+
case "message":
|
|
825
|
+
handleMessageEvent(event);
|
|
826
|
+
break;
|
|
827
|
+
case "app_mention":
|
|
828
|
+
handleMentionEvent(event);
|
|
829
|
+
break;
|
|
830
|
+
case "reaction_added":
|
|
831
|
+
handleReactionEvent(event);
|
|
832
|
+
break;
|
|
833
|
+
case "user_huddle_changed":
|
|
834
|
+
handleHuddleEvent(event);
|
|
835
|
+
break;
|
|
836
|
+
default:
|
|
837
|
+
log("WARN", `Unhandled event type: ${event.type}`);
|
|
838
|
+
}
|
|
839
|
+
stats.events_processed++;
|
|
840
|
+
} catch (err) {
|
|
841
|
+
stats.errors++;
|
|
842
|
+
log("ERROR", `Failed to process event: ${err.message}`, {
|
|
843
|
+
event_type: event.type,
|
|
844
|
+
event_id: eventId,
|
|
845
|
+
stack: err.stack,
|
|
846
|
+
});
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
// Mark as processed after handling
|
|
850
|
+
markProcessed(eventId);
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
// ── Dedup Cleanup (runs hourly) ───────────────────────────────────────────────
|
|
854
|
+
|
|
855
|
+
/**
|
|
856
|
+
* Clean up old dedup files (older than 24 hours) to prevent disk bloat.
|
|
857
|
+
*/
|
|
858
|
+
function cleanupDedupFiles() {
|
|
859
|
+
try {
|
|
860
|
+
const files = fs.readdirSync(DEDUP_DIR);
|
|
861
|
+
const cutoff = Date.now() - 24 * 60 * 60 * 1000; // 24 hours ago
|
|
862
|
+
let cleaned = 0;
|
|
863
|
+
|
|
864
|
+
for (const file of files) {
|
|
865
|
+
const filepath = path.join(DEDUP_DIR, file);
|
|
866
|
+
try {
|
|
867
|
+
const content = fs.readFileSync(filepath, "utf-8").trim();
|
|
868
|
+
const ts = parseInt(content, 10);
|
|
869
|
+
if (!isNaN(ts) && ts < cutoff) {
|
|
870
|
+
fs.unlinkSync(filepath);
|
|
871
|
+
cleaned++;
|
|
872
|
+
}
|
|
873
|
+
} catch {
|
|
874
|
+
// If we can't read it, check mtime instead
|
|
875
|
+
try {
|
|
876
|
+
const stat = fs.statSync(filepath);
|
|
877
|
+
if (stat.mtimeMs < cutoff) {
|
|
878
|
+
fs.unlinkSync(filepath);
|
|
879
|
+
cleaned++;
|
|
880
|
+
}
|
|
881
|
+
} catch {
|
|
882
|
+
// Skip
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
if (cleaned > 0) {
|
|
888
|
+
log("INFO", `Dedup cleanup: removed ${cleaned} old entries`);
|
|
889
|
+
}
|
|
890
|
+
} catch (err) {
|
|
891
|
+
log("ERROR", `Dedup cleanup failed: ${err.message}`);
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
// Run cleanup every hour
|
|
896
|
+
setInterval(cleanupDedupFiles, 60 * 60 * 1000);
|
|
897
|
+
|
|
898
|
+
// ── HTTP Server ───────────────────────────────────────────────────────────────
|
|
899
|
+
|
|
900
|
+
const server = http.createServer((req, res) => {
|
|
901
|
+
// Health check endpoint
|
|
902
|
+
if (req.method === "GET" && req.url === "/health") {
|
|
903
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
904
|
+
res.end(
|
|
905
|
+
JSON.stringify({
|
|
906
|
+
status: "ok",
|
|
907
|
+
service: "slack-events-server",
|
|
908
|
+
port: PORT,
|
|
909
|
+
uptime: process.uptime(),
|
|
910
|
+
...stats,
|
|
911
|
+
}),
|
|
912
|
+
);
|
|
913
|
+
return;
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
// Slack events endpoint
|
|
917
|
+
if (req.method === "POST" && req.url === "/slack/events") {
|
|
918
|
+
let body = "";
|
|
919
|
+
req.on("data", (chunk) => {
|
|
920
|
+
body += chunk;
|
|
921
|
+
});
|
|
922
|
+
req.on("end", () => {
|
|
923
|
+
// Check for Slack retry header — if this is a retry and we already
|
|
924
|
+
// processed it, acknowledge immediately
|
|
925
|
+
const retryNum = req.headers["x-slack-retry-num"];
|
|
926
|
+
const retryReason = req.headers["x-slack-retry-reason"];
|
|
927
|
+
if (retryNum) {
|
|
928
|
+
log("WARN", `Slack retry #${retryNum} (reason: ${retryReason})`);
|
|
929
|
+
// If Slack is retrying because we were slow, just ack
|
|
930
|
+
// The original event was likely already processed
|
|
931
|
+
if (retryReason === "http_timeout") {
|
|
932
|
+
res.writeHead(200, { "X-Slack-No-Retry": "1" });
|
|
933
|
+
res.end("ok");
|
|
934
|
+
return;
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
// Verify Slack signature — always (SIGNING_SECRET is guaranteed set by
|
|
939
|
+
// the boot guard; verification never gets skipped).
|
|
940
|
+
const timestamp = req.headers["x-slack-request-timestamp"];
|
|
941
|
+
const signature = req.headers["x-slack-signature"];
|
|
942
|
+
if (!verifySlackSignature(timestamp, body, signature)) {
|
|
943
|
+
log("ERROR", "Invalid Slack signature — rejecting request");
|
|
944
|
+
stats.errors++;
|
|
945
|
+
res.writeHead(401);
|
|
946
|
+
res.end("Invalid signature");
|
|
947
|
+
return;
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
let payload;
|
|
951
|
+
try {
|
|
952
|
+
payload = JSON.parse(body);
|
|
953
|
+
} catch {
|
|
954
|
+
res.writeHead(400);
|
|
955
|
+
res.end("Bad JSON");
|
|
956
|
+
return;
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
// URL verification challenge (one-time setup when registering the endpoint)
|
|
960
|
+
if (payload.type === "url_verification") {
|
|
961
|
+
log("INFO", "URL verification challenge received");
|
|
962
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
963
|
+
res.end(JSON.stringify({ challenge: payload.challenge }));
|
|
964
|
+
return;
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
// Event callback — respond 200 immediately, then process async
|
|
968
|
+
// Slack requires response within 3 seconds or it retries
|
|
969
|
+
res.writeHead(200, { "X-Slack-No-Retry": "1" });
|
|
970
|
+
res.end("ok");
|
|
971
|
+
|
|
972
|
+
// Process event asynchronously (after we've already sent 200)
|
|
973
|
+
if (payload.type === "event_callback") {
|
|
974
|
+
// Use setImmediate to ensure the response is sent before processing
|
|
975
|
+
setImmediate(() => {
|
|
976
|
+
try {
|
|
977
|
+
handleEvent(payload);
|
|
978
|
+
} catch (err) {
|
|
979
|
+
stats.errors++;
|
|
980
|
+
log("ERROR", `Unhandled error processing event: ${err.message}`, {
|
|
981
|
+
stack: err.stack,
|
|
982
|
+
});
|
|
983
|
+
}
|
|
984
|
+
});
|
|
985
|
+
}
|
|
986
|
+
});
|
|
987
|
+
return;
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
// Any other route
|
|
991
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
992
|
+
res.end(
|
|
993
|
+
JSON.stringify({
|
|
994
|
+
error: "Not found",
|
|
995
|
+
endpoints: ["/slack/events", "/health"],
|
|
996
|
+
}),
|
|
997
|
+
);
|
|
998
|
+
});
|
|
999
|
+
|
|
1000
|
+
// ── Startup ───────────────────────────────────────────────────────────────────
|
|
1001
|
+
|
|
1002
|
+
server.listen(PORT, () => {
|
|
1003
|
+
log("INFO", `Slack Events Server started`, { port: PORT });
|
|
1004
|
+
console.log(`[SLACK-EVENTS] Listening on port ${PORT}`);
|
|
1005
|
+
console.log(
|
|
1006
|
+
`[SLACK-EVENTS] Webhook URL: http://localhost:${PORT}/slack/events`,
|
|
1007
|
+
);
|
|
1008
|
+
console.log(`[SLACK-EVENTS] Health: http://localhost:${PORT}/health`);
|
|
1009
|
+
console.log(`[SLACK-EVENTS] Dedup dir: ${DEDUP_DIR}`);
|
|
1010
|
+
console.log(`[SLACK-EVENTS] Inbox dir: ${INBOX_DIR}`);
|
|
1011
|
+
console.log(`[SLACK-EVENTS] Trigger dir: ${TRIGGER_DIR}`);
|
|
1012
|
+
});
|
|
1013
|
+
|
|
1014
|
+
// ── Graceful Shutdown ─────────────────────────────────────────────────────────
|
|
1015
|
+
|
|
1016
|
+
function shutdown(signal) {
|
|
1017
|
+
log("INFO", `Received ${signal}, shutting down gracefully...`);
|
|
1018
|
+
server.close(() => {
|
|
1019
|
+
log("INFO", "Server closed");
|
|
1020
|
+
process.exit(0);
|
|
1021
|
+
});
|
|
1022
|
+
// Force exit after 5 seconds if server hasn't closed
|
|
1023
|
+
setTimeout(() => {
|
|
1024
|
+
log("WARN", "Forced shutdown after timeout");
|
|
1025
|
+
process.exit(1);
|
|
1026
|
+
}, 5000);
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
1030
|
+
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
1031
|
+
|
|
1032
|
+
// Prevent unhandled rejections from crashing the server
|
|
1033
|
+
process.on("unhandledRejection", (err) => {
|
|
1034
|
+
stats.errors++;
|
|
1035
|
+
log("ERROR", `Unhandled rejection: ${err.message || err}`, {
|
|
1036
|
+
stack: err.stack,
|
|
1037
|
+
});
|
|
1038
|
+
});
|
|
1039
|
+
|
|
1040
|
+
process.on("uncaughtException", (err) => {
|
|
1041
|
+
stats.errors++;
|
|
1042
|
+
log("ERROR", `Uncaught exception: ${err.message}`, { stack: err.stack });
|
|
1043
|
+
// For uncaught exceptions, exit after logging — let launchd restart us
|
|
1044
|
+
setTimeout(() => process.exit(1), 1000);
|
|
1045
|
+
});
|