agent-relay 1.0.22 → 1.2.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/README.md +1 -1
- package/dist/bridge/shadow-cli.d.ts +17 -0
- package/dist/bridge/shadow-cli.d.ts.map +1 -0
- package/dist/bridge/shadow-cli.js +75 -0
- package/dist/bridge/shadow-cli.js.map +1 -0
- package/dist/bridge/shadow-config.d.ts +87 -0
- package/dist/bridge/shadow-config.d.ts.map +1 -0
- package/dist/bridge/shadow-config.js +134 -0
- package/dist/bridge/shadow-config.js.map +1 -0
- package/dist/bridge/spawner.d.ts +68 -1
- package/dist/bridge/spawner.d.ts.map +1 -1
- package/dist/bridge/spawner.js +360 -16
- package/dist/bridge/spawner.js.map +1 -1
- package/dist/bridge/types.d.ts +67 -0
- package/dist/bridge/types.d.ts.map +1 -1
- package/dist/cli/index.js +1196 -15
- package/dist/cli/index.js.map +1 -1
- package/dist/cloud/api/auth.d.ts +20 -0
- package/dist/cloud/api/auth.d.ts.map +1 -0
- package/dist/cloud/api/auth.js +128 -0
- package/dist/cloud/api/auth.js.map +1 -0
- package/dist/cloud/api/billing.d.ts +17 -0
- package/dist/cloud/api/billing.d.ts.map +1 -0
- package/dist/cloud/api/billing.js +353 -0
- package/dist/cloud/api/billing.js.map +1 -0
- package/dist/cloud/api/cli-pty-runner.d.ts +54 -0
- package/dist/cloud/api/cli-pty-runner.d.ts.map +1 -0
- package/dist/cloud/api/cli-pty-runner.js +119 -0
- package/dist/cloud/api/cli-pty-runner.js.map +1 -0
- package/dist/cloud/api/coordinators.d.ts +8 -0
- package/dist/cloud/api/coordinators.d.ts.map +1 -0
- package/dist/cloud/api/coordinators.js +347 -0
- package/dist/cloud/api/coordinators.js.map +1 -0
- package/dist/cloud/api/daemons.d.ts +12 -0
- package/dist/cloud/api/daemons.d.ts.map +1 -0
- package/dist/cloud/api/daemons.js +320 -0
- package/dist/cloud/api/daemons.js.map +1 -0
- package/dist/cloud/api/generic-webhooks.d.ts +8 -0
- package/dist/cloud/api/generic-webhooks.d.ts.map +1 -0
- package/dist/cloud/api/generic-webhooks.js +129 -0
- package/dist/cloud/api/generic-webhooks.js.map +1 -0
- package/dist/cloud/api/git.d.ts +8 -0
- package/dist/cloud/api/git.d.ts.map +1 -0
- package/dist/cloud/api/git.js +131 -0
- package/dist/cloud/api/git.js.map +1 -0
- package/dist/cloud/api/github-app.d.ts +11 -0
- package/dist/cloud/api/github-app.d.ts.map +1 -0
- package/dist/cloud/api/github-app.js +189 -0
- package/dist/cloud/api/github-app.js.map +1 -0
- package/dist/cloud/api/middleware/planLimits.d.ts +43 -0
- package/dist/cloud/api/middleware/planLimits.d.ts.map +1 -0
- package/dist/cloud/api/middleware/planLimits.js +202 -0
- package/dist/cloud/api/middleware/planLimits.js.map +1 -0
- package/dist/cloud/api/monitoring.d.ts +11 -0
- package/dist/cloud/api/monitoring.d.ts.map +1 -0
- package/dist/cloud/api/monitoring.js +578 -0
- package/dist/cloud/api/monitoring.js.map +1 -0
- package/dist/cloud/api/nango-auth.d.ts +9 -0
- package/dist/cloud/api/nango-auth.d.ts.map +1 -0
- package/dist/cloud/api/nango-auth.js +377 -0
- package/dist/cloud/api/nango-auth.js.map +1 -0
- package/dist/cloud/api/onboarding.d.ts +15 -0
- package/dist/cloud/api/onboarding.d.ts.map +1 -0
- package/dist/cloud/api/onboarding.js +588 -0
- package/dist/cloud/api/onboarding.js.map +1 -0
- package/dist/cloud/api/policy.d.ts +8 -0
- package/dist/cloud/api/policy.d.ts.map +1 -0
- package/dist/cloud/api/policy.js +229 -0
- package/dist/cloud/api/policy.js.map +1 -0
- package/dist/cloud/api/providers.d.ts +7 -0
- package/dist/cloud/api/providers.d.ts.map +1 -0
- package/dist/cloud/api/providers.js +507 -0
- package/dist/cloud/api/providers.js.map +1 -0
- package/dist/cloud/api/repos.d.ts +7 -0
- package/dist/cloud/api/repos.d.ts.map +1 -0
- package/dist/cloud/api/repos.js +314 -0
- package/dist/cloud/api/repos.js.map +1 -0
- package/dist/cloud/api/teams.d.ts +7 -0
- package/dist/cloud/api/teams.d.ts.map +1 -0
- package/dist/cloud/api/teams.js +279 -0
- package/dist/cloud/api/teams.js.map +1 -0
- package/dist/cloud/api/test-helpers.d.ts +10 -0
- package/dist/cloud/api/test-helpers.d.ts.map +1 -0
- package/dist/cloud/api/test-helpers.js +575 -0
- package/dist/cloud/api/test-helpers.js.map +1 -0
- package/dist/cloud/api/usage.d.ts +7 -0
- package/dist/cloud/api/usage.d.ts.map +1 -0
- package/dist/cloud/api/usage.js +98 -0
- package/dist/cloud/api/usage.js.map +1 -0
- package/dist/cloud/api/webhooks.d.ts +7 -0
- package/dist/cloud/api/webhooks.d.ts.map +1 -0
- package/dist/cloud/api/webhooks.js +496 -0
- package/dist/cloud/api/webhooks.js.map +1 -0
- package/dist/cloud/api/workspaces.d.ts +7 -0
- package/dist/cloud/api/workspaces.d.ts.map +1 -0
- package/dist/cloud/api/workspaces.js +727 -0
- package/dist/cloud/api/workspaces.js.map +1 -0
- package/dist/cloud/billing/index.d.ts +9 -0
- package/dist/cloud/billing/index.d.ts.map +1 -0
- package/dist/cloud/billing/index.js +9 -0
- package/dist/cloud/billing/index.js.map +1 -0
- package/dist/cloud/billing/plans.d.ts +39 -0
- package/dist/cloud/billing/plans.d.ts.map +1 -0
- package/dist/cloud/billing/plans.js +245 -0
- package/dist/cloud/billing/plans.js.map +1 -0
- package/dist/cloud/billing/service.d.ts +80 -0
- package/dist/cloud/billing/service.d.ts.map +1 -0
- package/dist/cloud/billing/service.js +388 -0
- package/dist/cloud/billing/service.js.map +1 -0
- package/dist/cloud/billing/types.d.ts +141 -0
- package/dist/cloud/billing/types.d.ts.map +1 -0
- package/dist/cloud/billing/types.js +7 -0
- package/dist/cloud/billing/types.js.map +1 -0
- package/dist/cloud/config.d.ts +66 -0
- package/dist/cloud/config.d.ts.map +1 -0
- package/dist/cloud/config.js +92 -0
- package/dist/cloud/config.js.map +1 -0
- package/dist/cloud/db/drizzle.d.ts +215 -0
- package/dist/cloud/db/drizzle.d.ts.map +1 -0
- package/dist/cloud/db/drizzle.js +1083 -0
- package/dist/cloud/db/drizzle.js.map +1 -0
- package/dist/cloud/db/index.d.ts +35 -0
- package/dist/cloud/db/index.d.ts.map +1 -0
- package/dist/cloud/db/index.js +52 -0
- package/dist/cloud/db/index.js.map +1 -0
- package/dist/cloud/db/schema.d.ts +4519 -0
- package/dist/cloud/db/schema.d.ts.map +1 -0
- package/dist/cloud/db/schema.js +547 -0
- package/dist/cloud/db/schema.js.map +1 -0
- package/dist/cloud/index.d.ts +12 -0
- package/dist/cloud/index.d.ts.map +1 -0
- package/dist/cloud/index.js +39 -0
- package/dist/cloud/index.js.map +1 -0
- package/dist/cloud/provisioner/index.d.ts +75 -0
- package/dist/cloud/provisioner/index.d.ts.map +1 -0
- package/dist/cloud/provisioner/index.js +977 -0
- package/dist/cloud/provisioner/index.js.map +1 -0
- package/dist/cloud/server.d.ts +17 -0
- package/dist/cloud/server.d.ts.map +1 -0
- package/dist/cloud/server.js +534 -0
- package/dist/cloud/server.js.map +1 -0
- package/dist/cloud/services/auto-scaler.d.ts +152 -0
- package/dist/cloud/services/auto-scaler.d.ts.map +1 -0
- package/dist/cloud/services/auto-scaler.js +439 -0
- package/dist/cloud/services/auto-scaler.js.map +1 -0
- package/dist/cloud/services/capacity-manager.d.ts +148 -0
- package/dist/cloud/services/capacity-manager.d.ts.map +1 -0
- package/dist/cloud/services/capacity-manager.js +449 -0
- package/dist/cloud/services/capacity-manager.js.map +1 -0
- package/dist/cloud/services/ci-agent-spawner.d.ts +49 -0
- package/dist/cloud/services/ci-agent-spawner.d.ts.map +1 -0
- package/dist/cloud/services/ci-agent-spawner.js +373 -0
- package/dist/cloud/services/ci-agent-spawner.js.map +1 -0
- package/dist/cloud/services/coordinator.d.ts +62 -0
- package/dist/cloud/services/coordinator.d.ts.map +1 -0
- package/dist/cloud/services/coordinator.js +389 -0
- package/dist/cloud/services/coordinator.js.map +1 -0
- package/dist/cloud/services/index.d.ts +12 -0
- package/dist/cloud/services/index.d.ts.map +1 -0
- package/dist/cloud/services/index.js +15 -0
- package/dist/cloud/services/index.js.map +1 -0
- package/dist/cloud/services/mention-handler.d.ts +65 -0
- package/dist/cloud/services/mention-handler.d.ts.map +1 -0
- package/dist/cloud/services/mention-handler.js +405 -0
- package/dist/cloud/services/mention-handler.js.map +1 -0
- package/dist/cloud/services/nango.d.ts +126 -0
- package/dist/cloud/services/nango.d.ts.map +1 -0
- package/dist/cloud/services/nango.js +191 -0
- package/dist/cloud/services/nango.js.map +1 -0
- package/dist/cloud/services/persistence.d.ts +131 -0
- package/dist/cloud/services/persistence.d.ts.map +1 -0
- package/dist/cloud/services/persistence.js +200 -0
- package/dist/cloud/services/persistence.js.map +1 -0
- package/dist/cloud/services/planLimits.d.ts +125 -0
- package/dist/cloud/services/planLimits.d.ts.map +1 -0
- package/dist/cloud/services/planLimits.js +282 -0
- package/dist/cloud/services/planLimits.js.map +1 -0
- package/dist/cloud/services/scaling-orchestrator.d.ts +159 -0
- package/dist/cloud/services/scaling-orchestrator.d.ts.map +1 -0
- package/dist/cloud/services/scaling-orchestrator.js +502 -0
- package/dist/cloud/services/scaling-orchestrator.js.map +1 -0
- package/dist/cloud/services/scaling-policy.d.ts +121 -0
- package/dist/cloud/services/scaling-policy.d.ts.map +1 -0
- package/dist/cloud/services/scaling-policy.js +415 -0
- package/dist/cloud/services/scaling-policy.js.map +1 -0
- package/dist/cloud/vault/index.d.ts +76 -0
- package/dist/cloud/vault/index.d.ts.map +1 -0
- package/dist/cloud/vault/index.js +219 -0
- package/dist/cloud/vault/index.js.map +1 -0
- package/dist/cloud/webhooks/index.d.ts +24 -0
- package/dist/cloud/webhooks/index.d.ts.map +1 -0
- package/dist/cloud/webhooks/index.js +29 -0
- package/dist/cloud/webhooks/index.js.map +1 -0
- package/dist/cloud/webhooks/parsers/github.d.ts +8 -0
- package/dist/cloud/webhooks/parsers/github.d.ts.map +1 -0
- package/dist/cloud/webhooks/parsers/github.js +234 -0
- package/dist/cloud/webhooks/parsers/github.js.map +1 -0
- package/dist/cloud/webhooks/parsers/index.d.ts +23 -0
- package/dist/cloud/webhooks/parsers/index.d.ts.map +1 -0
- package/dist/cloud/webhooks/parsers/index.js +30 -0
- package/dist/cloud/webhooks/parsers/index.js.map +1 -0
- package/dist/cloud/webhooks/parsers/linear.d.ts +9 -0
- package/dist/cloud/webhooks/parsers/linear.d.ts.map +1 -0
- package/dist/cloud/webhooks/parsers/linear.js +258 -0
- package/dist/cloud/webhooks/parsers/linear.js.map +1 -0
- package/dist/cloud/webhooks/parsers/slack.d.ts +9 -0
- package/dist/cloud/webhooks/parsers/slack.d.ts.map +1 -0
- package/dist/cloud/webhooks/parsers/slack.js +214 -0
- package/dist/cloud/webhooks/parsers/slack.js.map +1 -0
- package/dist/cloud/webhooks/responders/github.d.ts +8 -0
- package/dist/cloud/webhooks/responders/github.d.ts.map +1 -0
- package/dist/cloud/webhooks/responders/github.js +73 -0
- package/dist/cloud/webhooks/responders/github.js.map +1 -0
- package/dist/cloud/webhooks/responders/index.d.ts +23 -0
- package/dist/cloud/webhooks/responders/index.d.ts.map +1 -0
- package/dist/cloud/webhooks/responders/index.js +30 -0
- package/dist/cloud/webhooks/responders/index.js.map +1 -0
- package/dist/cloud/webhooks/responders/linear.d.ts +9 -0
- package/dist/cloud/webhooks/responders/linear.d.ts.map +1 -0
- package/dist/cloud/webhooks/responders/linear.js +149 -0
- package/dist/cloud/webhooks/responders/linear.js.map +1 -0
- package/dist/cloud/webhooks/responders/slack.d.ts +20 -0
- package/dist/cloud/webhooks/responders/slack.d.ts.map +1 -0
- package/dist/cloud/webhooks/responders/slack.js +178 -0
- package/dist/cloud/webhooks/responders/slack.js.map +1 -0
- package/dist/cloud/webhooks/router.d.ts +25 -0
- package/dist/cloud/webhooks/router.d.ts.map +1 -0
- package/dist/cloud/webhooks/router.js +504 -0
- package/dist/cloud/webhooks/router.js.map +1 -0
- package/dist/cloud/webhooks/rules-engine.d.ts +24 -0
- package/dist/cloud/webhooks/rules-engine.d.ts.map +1 -0
- package/dist/cloud/webhooks/rules-engine.js +287 -0
- package/dist/cloud/webhooks/rules-engine.js.map +1 -0
- package/dist/cloud/webhooks/types.d.ts +186 -0
- package/dist/cloud/webhooks/types.d.ts.map +1 -0
- package/dist/cloud/webhooks/types.js +8 -0
- package/dist/cloud/webhooks/types.js.map +1 -0
- package/dist/continuity/formatter.d.ts +51 -0
- package/dist/continuity/formatter.d.ts.map +1 -0
- package/dist/continuity/formatter.js +313 -0
- package/dist/continuity/formatter.js.map +1 -0
- package/dist/continuity/handoff-store.d.ts +67 -0
- package/dist/continuity/handoff-store.d.ts.map +1 -0
- package/dist/continuity/handoff-store.js +472 -0
- package/dist/continuity/handoff-store.js.map +1 -0
- package/dist/continuity/index.d.ts +45 -0
- package/dist/continuity/index.d.ts.map +1 -0
- package/dist/continuity/index.js +48 -0
- package/dist/continuity/index.js.map +1 -0
- package/dist/continuity/ledger-store.d.ts +110 -0
- package/dist/continuity/ledger-store.d.ts.map +1 -0
- package/dist/continuity/ledger-store.js +500 -0
- package/dist/continuity/ledger-store.js.map +1 -0
- package/dist/continuity/manager.d.ts +178 -0
- package/dist/continuity/manager.d.ts.map +1 -0
- package/dist/continuity/manager.js +562 -0
- package/dist/continuity/manager.js.map +1 -0
- package/dist/continuity/parser.d.ts +76 -0
- package/dist/continuity/parser.d.ts.map +1 -0
- package/dist/continuity/parser.js +579 -0
- package/dist/continuity/parser.js.map +1 -0
- package/dist/continuity/types.d.ts +180 -0
- package/dist/continuity/types.d.ts.map +1 -0
- package/dist/continuity/types.js +9 -0
- package/dist/continuity/types.js.map +1 -0
- package/dist/daemon/agent-manager.d.ts +114 -0
- package/dist/daemon/agent-manager.d.ts.map +1 -0
- package/dist/daemon/agent-manager.js +513 -0
- package/dist/daemon/agent-manager.js.map +1 -0
- package/dist/daemon/agent-registry.d.ts +34 -0
- package/dist/daemon/agent-registry.d.ts.map +1 -1
- package/dist/daemon/agent-registry.js +45 -2
- package/dist/daemon/agent-registry.js.map +1 -1
- package/dist/daemon/api.d.ts +81 -0
- package/dist/daemon/api.d.ts.map +1 -0
- package/dist/daemon/api.js +554 -0
- package/dist/daemon/api.js.map +1 -0
- package/dist/daemon/cli-auth.d.ts +67 -0
- package/dist/daemon/cli-auth.d.ts.map +1 -0
- package/dist/daemon/cli-auth.js +537 -0
- package/dist/daemon/cli-auth.js.map +1 -0
- package/dist/daemon/cloud-sync.d.ts +101 -0
- package/dist/daemon/cloud-sync.d.ts.map +1 -0
- package/dist/daemon/cloud-sync.js +263 -0
- package/dist/daemon/cloud-sync.js.map +1 -0
- package/dist/daemon/index.d.ts +4 -0
- package/dist/daemon/index.d.ts.map +1 -1
- package/dist/daemon/index.js +6 -0
- package/dist/daemon/index.js.map +1 -1
- package/dist/daemon/orchestrator.d.ts +155 -0
- package/dist/daemon/orchestrator.d.ts.map +1 -0
- package/dist/daemon/orchestrator.js +766 -0
- package/dist/daemon/orchestrator.js.map +1 -0
- package/dist/daemon/router.d.ts +29 -0
- package/dist/daemon/router.d.ts.map +1 -1
- package/dist/daemon/router.js +143 -21
- package/dist/daemon/router.js.map +1 -1
- package/dist/daemon/server.d.ts +42 -0
- package/dist/daemon/server.d.ts.map +1 -1
- package/dist/daemon/server.js +199 -16
- package/dist/daemon/server.js.map +1 -1
- package/dist/daemon/services/browser-testing.d.ts +88 -0
- package/dist/daemon/services/browser-testing.d.ts.map +1 -0
- package/dist/daemon/services/browser-testing.js +244 -0
- package/dist/daemon/services/browser-testing.js.map +1 -0
- package/dist/daemon/services/container-spawner.d.ts +135 -0
- package/dist/daemon/services/container-spawner.d.ts.map +1 -0
- package/dist/daemon/services/container-spawner.js +313 -0
- package/dist/daemon/services/container-spawner.js.map +1 -0
- package/dist/daemon/types.d.ts +131 -0
- package/dist/daemon/types.d.ts.map +1 -0
- package/dist/daemon/types.js +6 -0
- package/dist/daemon/types.js.map +1 -0
- package/dist/daemon/workspace-manager.d.ts +75 -0
- package/dist/daemon/workspace-manager.d.ts.map +1 -0
- package/dist/daemon/workspace-manager.js +289 -0
- package/dist/daemon/workspace-manager.js.map +1 -0
- package/dist/dashboard/out/404.html +1 -1
- package/dist/dashboard/out/_next/static/chunks/116-2502180def231162.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/282-980c2eb8fff20123.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/480-2d4111711d4e473c.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/724-73c1ee5f60abe860.js +9 -0
- package/dist/dashboard/out/_next/static/chunks/766-c3a14283c88d815b.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/app/app/page-7120be68bea622f3.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/app/connect-repos/page-dc2e3a1a22478efc.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/app/history/page-56a8b4616a90dc43.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/app/layout-2433bb48965f4333.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/app/login/page-3eac37ea6f5dd153.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/app/metrics/page-1081dd190a331a91.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/app/page-daf87e86f783f980.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/app/pricing/page-4d72d5a5d8a9b618.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/app/providers/page-b68a681526eb145e.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/app/signup/page-fee4ed1709070bcd.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/e868780c-48e5f147c90a3a41.js +18 -0
- package/dist/dashboard/out/_next/static/chunks/{main-e0a1f53fe0617a63.js → main-97850e03d723ea8c.js} +1 -1
- package/dist/dashboard/out/_next/static/chunks/main-app-5d692157a8eb1fd9.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/webpack-1cdd8ed57114d5e1.js +1 -0
- package/dist/dashboard/out/_next/static/css/29852f26181969a0.css +1 -0
- package/dist/dashboard/out/_next/static/css/411ce23ffeae9f76.css +1 -0
- package/dist/dashboard/out/alt-logos/agent-relay-logo-128.png +0 -0
- package/dist/dashboard/out/alt-logos/agent-relay-logo-256.png +0 -0
- package/dist/dashboard/out/alt-logos/agent-relay-logo-32.png +0 -0
- package/dist/dashboard/out/alt-logos/agent-relay-logo-512.png +0 -0
- package/dist/dashboard/out/alt-logos/agent-relay-logo-64.png +0 -0
- package/dist/dashboard/out/alt-logos/agent-relay-logo.svg +45 -0
- package/dist/dashboard/out/alt-logos/logo.svg +38 -0
- package/dist/dashboard/out/alt-logos/monogram-logo-128.png +0 -0
- package/dist/dashboard/out/alt-logos/monogram-logo-256.png +0 -0
- package/dist/dashboard/out/alt-logos/monogram-logo-32.png +0 -0
- package/dist/dashboard/out/alt-logos/monogram-logo-512.png +0 -0
- package/dist/dashboard/out/alt-logos/monogram-logo-64.png +0 -0
- package/dist/dashboard/out/alt-logos/monogram-logo.svg +38 -0
- package/dist/dashboard/out/app.html +1 -0
- package/dist/dashboard/out/app.txt +7 -0
- package/dist/dashboard/out/connect-repos.html +1 -0
- package/dist/dashboard/out/connect-repos.txt +7 -0
- package/dist/dashboard/out/history.html +1 -0
- package/dist/dashboard/out/history.txt +7 -0
- package/dist/dashboard/out/index.html +1 -1
- package/dist/dashboard/out/index.txt +2 -2
- package/dist/dashboard/out/login.html +6 -0
- package/dist/dashboard/out/login.txt +7 -0
- package/dist/dashboard/out/metrics.html +1 -515
- package/dist/dashboard/out/metrics.txt +2 -2
- package/dist/dashboard/out/pricing.html +13 -0
- package/dist/dashboard/out/pricing.txt +7 -0
- package/dist/dashboard/out/providers.html +1 -0
- package/dist/dashboard/out/providers.txt +7 -0
- package/dist/dashboard/out/signup.html +6 -0
- package/dist/dashboard/out/signup.txt +7 -0
- package/dist/dashboard-server/metrics.d.ts.map +1 -1
- package/dist/dashboard-server/metrics.js +3 -2
- package/dist/dashboard-server/metrics.js.map +1 -1
- package/dist/dashboard-server/server.d.ts.map +1 -1
- package/dist/dashboard-server/server.js +2653 -130
- package/dist/dashboard-server/server.js.map +1 -1
- package/dist/hooks/emitter.d.ts +40 -0
- package/dist/hooks/emitter.d.ts.map +1 -0
- package/dist/hooks/emitter.js +63 -0
- package/dist/hooks/emitter.js.map +1 -0
- package/dist/hooks/index.d.ts +3 -0
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +3 -0
- package/dist/hooks/index.js.map +1 -1
- package/dist/hooks/registry.d.ts +173 -0
- package/dist/hooks/registry.d.ts.map +1 -0
- package/dist/hooks/registry.js +476 -0
- package/dist/hooks/registry.js.map +1 -0
- package/dist/hooks/trajectory-hooks.d.ts +52 -0
- package/dist/hooks/trajectory-hooks.d.ts.map +1 -0
- package/dist/hooks/trajectory-hooks.js +183 -0
- package/dist/hooks/trajectory-hooks.js.map +1 -0
- package/dist/hooks/types.d.ts +141 -0
- package/dist/hooks/types.d.ts.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/memory/adapters/index.d.ts +8 -0
- package/dist/memory/adapters/index.d.ts.map +1 -0
- package/dist/memory/adapters/index.js +8 -0
- package/dist/memory/adapters/index.js.map +1 -0
- package/dist/memory/adapters/inmemory.d.ts +59 -0
- package/dist/memory/adapters/inmemory.d.ts.map +1 -0
- package/dist/memory/adapters/inmemory.js +195 -0
- package/dist/memory/adapters/inmemory.js.map +1 -0
- package/dist/memory/adapters/supermemory.d.ts +71 -0
- package/dist/memory/adapters/supermemory.d.ts.map +1 -0
- package/dist/memory/adapters/supermemory.js +338 -0
- package/dist/memory/adapters/supermemory.js.map +1 -0
- package/dist/memory/factory.d.ts +48 -0
- package/dist/memory/factory.d.ts.map +1 -0
- package/dist/memory/factory.js +143 -0
- package/dist/memory/factory.js.map +1 -0
- package/dist/memory/index.d.ts +32 -0
- package/dist/memory/index.d.ts.map +1 -0
- package/dist/memory/index.js +32 -0
- package/dist/memory/index.js.map +1 -0
- package/dist/memory/memory-hooks.d.ts +60 -0
- package/dist/memory/memory-hooks.d.ts.map +1 -0
- package/dist/memory/memory-hooks.js +313 -0
- package/dist/memory/memory-hooks.js.map +1 -0
- package/dist/memory/service.d.ts +49 -0
- package/dist/memory/service.d.ts.map +1 -0
- package/dist/memory/service.js +146 -0
- package/dist/memory/service.js.map +1 -0
- package/dist/memory/types.d.ts +195 -0
- package/dist/memory/types.d.ts.map +1 -0
- package/dist/memory/types.js +8 -0
- package/dist/memory/types.js.map +1 -0
- package/dist/policy/agent-policy.d.ts +225 -0
- package/dist/policy/agent-policy.d.ts.map +1 -0
- package/dist/policy/agent-policy.js +665 -0
- package/dist/policy/agent-policy.js.map +1 -0
- package/dist/policy/cloud-policy-fetcher.d.ts +12 -0
- package/dist/policy/cloud-policy-fetcher.d.ts.map +1 -0
- package/dist/policy/cloud-policy-fetcher.js +64 -0
- package/dist/policy/cloud-policy-fetcher.js.map +1 -0
- package/dist/protocol/types.d.ts +10 -1
- package/dist/protocol/types.d.ts.map +1 -1
- package/dist/resiliency/context-persistence.d.ts +140 -0
- package/dist/resiliency/context-persistence.d.ts.map +1 -0
- package/dist/resiliency/context-persistence.js +397 -0
- package/dist/resiliency/context-persistence.js.map +1 -0
- package/dist/resiliency/crash-insights.d.ts +156 -0
- package/dist/resiliency/crash-insights.d.ts.map +1 -0
- package/dist/resiliency/crash-insights.js +492 -0
- package/dist/resiliency/crash-insights.js.map +1 -0
- package/dist/resiliency/gossip-health.d.ts +137 -0
- package/dist/resiliency/gossip-health.d.ts.map +1 -0
- package/dist/resiliency/gossip-health.js +241 -0
- package/dist/resiliency/gossip-health.js.map +1 -0
- package/dist/resiliency/health-monitor.d.ts +97 -0
- package/dist/resiliency/health-monitor.d.ts.map +1 -0
- package/dist/resiliency/health-monitor.js +291 -0
- package/dist/resiliency/health-monitor.js.map +1 -0
- package/dist/resiliency/index.d.ts +68 -0
- package/dist/resiliency/index.d.ts.map +1 -0
- package/dist/resiliency/index.js +68 -0
- package/dist/resiliency/index.js.map +1 -0
- package/dist/resiliency/leader-watchdog.d.ts +109 -0
- package/dist/resiliency/leader-watchdog.d.ts.map +1 -0
- package/dist/resiliency/leader-watchdog.js +189 -0
- package/dist/resiliency/leader-watchdog.js.map +1 -0
- package/dist/resiliency/logger.d.ts +114 -0
- package/dist/resiliency/logger.d.ts.map +1 -0
- package/dist/resiliency/logger.js +250 -0
- package/dist/resiliency/logger.js.map +1 -0
- package/dist/resiliency/memory-monitor.d.ts +172 -0
- package/dist/resiliency/memory-monitor.d.ts.map +1 -0
- package/dist/resiliency/memory-monitor.js +593 -0
- package/dist/resiliency/memory-monitor.js.map +1 -0
- package/dist/resiliency/metrics.d.ts +115 -0
- package/dist/resiliency/metrics.d.ts.map +1 -0
- package/dist/resiliency/metrics.js +239 -0
- package/dist/resiliency/metrics.js.map +1 -0
- package/dist/resiliency/provider-context.d.ts +100 -0
- package/dist/resiliency/provider-context.d.ts.map +1 -0
- package/dist/resiliency/provider-context.js +360 -0
- package/dist/resiliency/provider-context.js.map +1 -0
- package/dist/resiliency/stateless-lead.d.ts +149 -0
- package/dist/resiliency/stateless-lead.d.ts.map +1 -0
- package/dist/resiliency/stateless-lead.js +308 -0
- package/dist/resiliency/stateless-lead.js.map +1 -0
- package/dist/resiliency/supervisor.d.ts +147 -0
- package/dist/resiliency/supervisor.d.ts.map +1 -0
- package/dist/resiliency/supervisor.js +459 -0
- package/dist/resiliency/supervisor.js.map +1 -0
- package/dist/shared/cli-auth-config.d.ts +91 -0
- package/dist/shared/cli-auth-config.d.ts.map +1 -0
- package/dist/shared/cli-auth-config.js +264 -0
- package/dist/shared/cli-auth-config.js.map +1 -0
- package/dist/storage/adapter.d.ts +3 -1
- package/dist/storage/adapter.d.ts.map +1 -1
- package/dist/storage/adapter.js +12 -2
- package/dist/storage/adapter.js.map +1 -1
- package/dist/storage/sqlite-adapter.d.ts.map +1 -1
- package/dist/storage/sqlite-adapter.js +18 -14
- package/dist/storage/sqlite-adapter.js.map +1 -1
- package/dist/trajectory/config.d.ts +84 -0
- package/dist/trajectory/config.d.ts.map +1 -0
- package/dist/trajectory/config.js +163 -0
- package/dist/trajectory/config.js.map +1 -0
- package/dist/trajectory/index.d.ts +8 -0
- package/dist/trajectory/index.d.ts.map +1 -0
- package/dist/trajectory/index.js +8 -0
- package/dist/trajectory/index.js.map +1 -0
- package/dist/trajectory/integration.d.ts +292 -0
- package/dist/trajectory/integration.d.ts.map +1 -0
- package/dist/trajectory/integration.js +834 -0
- package/dist/trajectory/integration.js.map +1 -0
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +1 -0
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/logger.d.ts +40 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +84 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/project-namespace.d.ts +24 -0
- package/dist/utils/project-namespace.d.ts.map +1 -1
- package/dist/utils/project-namespace.js +84 -0
- package/dist/utils/project-namespace.js.map +1 -1
- package/dist/wrapper/client.d.ts +16 -1
- package/dist/wrapper/client.d.ts.map +1 -1
- package/dist/wrapper/client.js +32 -1
- package/dist/wrapper/client.js.map +1 -1
- package/dist/wrapper/parser.d.ts +13 -0
- package/dist/wrapper/parser.d.ts.map +1 -1
- package/dist/wrapper/parser.js +217 -47
- package/dist/wrapper/parser.js.map +1 -1
- package/dist/wrapper/pty-wrapper.d.ts +219 -17
- package/dist/wrapper/pty-wrapper.d.ts.map +1 -1
- package/dist/wrapper/pty-wrapper.js +1050 -104
- package/dist/wrapper/pty-wrapper.js.map +1 -1
- package/dist/wrapper/shared.d.ts +165 -0
- package/dist/wrapper/shared.d.ts.map +1 -0
- package/dist/wrapper/shared.js +270 -0
- package/dist/wrapper/shared.js.map +1 -0
- package/dist/wrapper/tmux-wrapper.d.ts +78 -11
- package/dist/wrapper/tmux-wrapper.d.ts.map +1 -1
- package/dist/wrapper/tmux-wrapper.js +567 -106
- package/dist/wrapper/tmux-wrapper.js.map +1 -1
- package/docs/CLOUD-ARCHITECTURE.md +804 -0
- package/docs/CLOUD-ONBOARDING-DESIGN.md +1983 -0
- package/docs/HOOKS_API.md +394 -0
- package/docs/WRAPPER_EVENTS.md +358 -0
- package/docs/agent-policy-snippet.md +40 -0
- package/docs/agent-relay-protocol.md +238 -0
- package/docs/agent-relay-snippet.md +115 -6
- package/docs/archive/EXECUTIVE_SUMMARY.md +358 -0
- package/docs/archive/ROADMAP.md +329 -0
- package/docs/archive/TESTING_PRESENCE_FEATURES.md +327 -0
- package/docs/competitive/GASTOWN.md +451 -0
- package/docs/{COMPETITIVE_ANALYSIS.md → competitive/OVERVIEW.md} +1 -0
- package/docs/competitive/README.md +34 -0
- package/docs/competitive/TMUX_ORCHESTRATOR.md +605 -0
- package/docs/dashboard.png +0 -0
- package/docs/design/ci-failure-webhooks.md +812 -0
- package/docs/design/comprehensive-integrations.md +238 -0
- package/docs/design/e2b-sandbox-integration.md +504 -0
- package/docs/design/github-app-permissions.md +264 -0
- package/docs/guides/CLOUD.md +236 -0
- package/docs/guides/LOCAL.md +535 -0
- package/docs/guides/SELF-HOSTED.md +494 -0
- package/docs/local-testing.md +428 -0
- package/docs/proposals/continuous-claude-integration.md +622 -0
- package/docs/proposals/custom-commands.md +368 -0
- package/docs/proposals/shadow-as-subagent.md +765 -0
- package/docs/proposals/slack-bot-integration.md +1457 -0
- package/docs/tasks/global-skills-system.tasks.md +230 -0
- package/docs/tasks/webhook-integrations.tasks.md +184 -0
- package/docs/tasks/workspace-capabilities.tasks.md +121 -0
- package/docs/testing/RESILIENCY-TEST-PLAN-2026-01-01.md +366 -0
- package/package.json +45 -7
- package/scripts/cloud-setup.sh +96 -0
- package/scripts/manual-qa.sh +293 -0
- package/scripts/postinstall.js +60 -0
- package/scripts/run-cloud-qa.sh +220 -0
- package/scripts/test-cli-auth/Dockerfile +44 -0
- package/scripts/test-cli-auth/Dockerfile.real +79 -0
- package/scripts/test-cli-auth/README.md +286 -0
- package/scripts/test-cli-auth/ci-test-real-clis.ts +251 -0
- package/scripts/test-cli-auth/ci-test-runner.ts +263 -0
- package/scripts/test-cli-auth/mock-cli.sh +147 -0
- package/scripts/test-cli-auth/package.json +14 -0
- package/scripts/test-cli-auth/test-oauth-flow.ts +220 -0
- package/scripts/test-pty-input-auto.js +222 -0
- package/scripts/test-pty-input.js +150 -0
- package/dist/dashboard/out/_next/static/chunks/app/layout-c9d8c5d95e48c6bf.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/app/metrics/page-8aa9936bc6c771ab.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/app/page-4498be09a5157759.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/main-app-bae2e535de00de50.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/webpack-c81f7fd28659d64f.js +0 -1
- package/dist/dashboard/out/_next/static/css/50ed6996e3df7bdd.css +0 -1
- /package/dist/dashboard/out/_next/static/{DXFA-jj8wb3PcY5DX2xcU → H5aWG0udPB4iOUIl_gytz}/_buildManifest.js +0 -0
- /package/dist/dashboard/out/_next/static/{DXFA-jj8wb3PcY5DX2xcU → H5aWG0udPB4iOUIl_gytz}/_ssgManifest.js +0 -0
- /package/dist/dashboard/out/_next/static/chunks/{117-3bef7b19f3e60751.js → 117-b100311aff8d5c61.js} +0 -0
- /package/dist/dashboard/out/_next/static/chunks/{648-6cf686106c891ad3.js → 648-a13d3c2b1be45466.js} +0 -0
- /package/dist/dashboard/out/_next/static/chunks/app/_not-found/{page-8ff6572bc7c9bc61.js → page-a4973f3e3c82fb67.js} +0 -0
- /package/dist/dashboard/out/_next/static/chunks/{fd9d1056-26bd8d656b496dba.js → fd9d1056-bf46c09eb57e019c.js} +0 -0
- /package/docs/{CHANGELOG.md → archive/CHANGELOG.md} +0 -0
- /package/docs/{CLI-SIMPLIFICATION-COMPLETE.md → archive/CLI-SIMPLIFICATION-COMPLETE.md} +0 -0
- /package/docs/{DESIGN_BRIDGE_STAFFING.md → archive/DESIGN_BRIDGE_STAFFING.md} +0 -0
- /package/docs/{DESIGN_V2.md → archive/DESIGN_V2.md} +0 -0
- /package/docs/{MONETIZATION.md → archive/MONETIZATION.md} +0 -0
- /package/docs/{PROPOSAL-trajectories.md → archive/PROPOSAL-trajectories.md} +0 -0
- /package/docs/{SCALING_ANALYSIS.md → archive/SCALING_ANALYSIS.md} +0 -0
- /package/docs/{TMUX_IMPLEMENTATION_NOTES.md → archive/TMUX_IMPLEMENTATION_NOTES.md} +0 -0
- /package/docs/{TMUX_IMPROVEMENTS.md → archive/TMUX_IMPROVEMENTS.md} +0 -0
- /package/docs/{dashboard-v2-plan.md → archive/dashboard-v2-plan.md} +0 -0
- /package/docs/{removable-code-analysis.md → archive/removable-code-analysis.md} +0 -0
- /package/docs/{competitive-analysis-mcp-agent-mail.md → competitive/MCP_AGENT_MAIL.md} +0 -0
|
@@ -0,0 +1,1457 @@
|
|
|
1
|
+
# Slack Bot Integration Proposal
|
|
2
|
+
|
|
3
|
+
**Author:** Claude
|
|
4
|
+
**Date:** 2025-12-30
|
|
5
|
+
**Status:** Draft
|
|
6
|
+
**Estimated Effort:** 5-7 days
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Executive Summary
|
|
11
|
+
|
|
12
|
+
This proposal outlines a plan to integrate agent-relay messaging with Slack, following the **cloud-first architecture** established in PR #35. The integration enables:
|
|
13
|
+
|
|
14
|
+
- AI agents to communicate in Slack channels alongside humans
|
|
15
|
+
- Humans to interact with agents via @mentions and slash commands
|
|
16
|
+
- Real-time bidirectional sync between relay daemon and Slack
|
|
17
|
+
- Thread preservation across both systems
|
|
18
|
+
- **Cloud-managed OAuth and credentials** via Nango integration platform
|
|
19
|
+
- **Multi-workspace support** through the daemon orchestrator
|
|
20
|
+
- **Plan-based access** (Pro/Team/Enterprise tiers)
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Table of Contents
|
|
25
|
+
|
|
26
|
+
1. [Goals & Non-Goals](#1-goals--non-goals)
|
|
27
|
+
2. [Architecture Overview](#2-architecture-overview)
|
|
28
|
+
3. [Cloud Components](#3-cloud-components)
|
|
29
|
+
4. [Daemon Components](#4-daemon-components)
|
|
30
|
+
5. [Database Schema](#5-database-schema)
|
|
31
|
+
6. [Dashboard UI](#6-dashboard-ui)
|
|
32
|
+
7. [Implementation Phases](#7-implementation-phases)
|
|
33
|
+
8. [API Specifications](#8-api-specifications)
|
|
34
|
+
9. [Security & Plan Limits](#9-security--plan-limits)
|
|
35
|
+
10. [Testing Strategy](#10-testing-strategy)
|
|
36
|
+
11. [Open Questions](#11-open-questions)
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## 1. Goals & Non-Goals
|
|
41
|
+
|
|
42
|
+
### Goals
|
|
43
|
+
|
|
44
|
+
| Goal | Description |
|
|
45
|
+
|------|-------------|
|
|
46
|
+
| **Cloud-managed credentials** | Slack OAuth tokens stored in encrypted vault, not local files |
|
|
47
|
+
| **Multi-workspace support** | Each workspace can have its own Slack integration |
|
|
48
|
+
| **Bidirectional messaging** | Messages flow Slack ↔ Relay in real-time |
|
|
49
|
+
| **Thread preservation** | Relay threads map to Slack threads and vice versa |
|
|
50
|
+
| **Plan-gated access** | Slack integration available on Pro+ plans |
|
|
51
|
+
| **Dashboard configuration** | Connect/disconnect Slack via UI |
|
|
52
|
+
| **Self-hosted parity** | Works in cloud-hosted, self-hosted, and hybrid modes |
|
|
53
|
+
|
|
54
|
+
### Non-Goals (v1)
|
|
55
|
+
|
|
56
|
+
| Non-Goal | Rationale |
|
|
57
|
+
|----------|-----------|
|
|
58
|
+
| Multi-Slack-workspace per relay workspace | Complexity; one Slack workspace per relay workspace |
|
|
59
|
+
| Slack-only agents | Agents should exist in relay first |
|
|
60
|
+
| Rich Block Kit formatting | Plain text first; enhance later |
|
|
61
|
+
| Slack Enterprise Grid | Requires org-level OAuth; v2 |
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## 2. Architecture Overview
|
|
66
|
+
|
|
67
|
+
### High-Level Design (Cloud Paradigm)
|
|
68
|
+
|
|
69
|
+
```
|
|
70
|
+
┌─────────────────────────────────────────────────────────────────────────────┐
|
|
71
|
+
│ SLACK WORKSPACE │
|
|
72
|
+
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
|
|
73
|
+
│ │ #agents │ │ #alerts │ │ @agent-bot │ │
|
|
74
|
+
│ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │
|
|
75
|
+
│ └────────────────┴────────────────┘ │
|
|
76
|
+
│ │ │
|
|
77
|
+
│ Slack Events API (Socket Mode) │
|
|
78
|
+
└──────────────────────────┼──────────────────────────────────────────────────┘
|
|
79
|
+
│
|
|
80
|
+
▼
|
|
81
|
+
┌──────────────────────────────────────────────────────────────────────────────┐
|
|
82
|
+
│ AGENT RELAY CLOUD │
|
|
83
|
+
│ │
|
|
84
|
+
│ ┌────────────────────────────────────────────────────────────────────────┐ │
|
|
85
|
+
│ │ src/cloud/ │ │
|
|
86
|
+
│ │ ┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐ │ │
|
|
87
|
+
│ │ │ api/integrations │ │ services/slack │ │ Nango │ │ │
|
|
88
|
+
│ │ │ /slack.ts │ │ SlackService │ │ (external) │ │ │
|
|
89
|
+
│ │ │ │ │ │ │ │ │ │
|
|
90
|
+
│ │ │ • OAuth trigger │ │ • Token fetch │ │ • OAuth flow │ │ │
|
|
91
|
+
│ │ │ • Disconnect │ │ • Workspace sync │ │ • Token storage │ │ │
|
|
92
|
+
│ │ │ • Status │ │ • Health check │ │ • Auto refresh │ │ │
|
|
93
|
+
│ │ └──────────────────┘ └──────────────────┘ └──────────────────┘ │ │
|
|
94
|
+
│ │ │ │
|
|
95
|
+
│ │ ┌──────────────────┐ ┌──────────────────┐ │ │
|
|
96
|
+
│ │ │ db/schema.ts │ │ api/middleware/ │ │ │
|
|
97
|
+
│ │ │ │ │ planLimits.ts │ │ │
|
|
98
|
+
│ │ │ • slack_integrations│ • requirePro() │ │ │
|
|
99
|
+
│ │ │ • slack_channels │ │ • checkSlackLimit│ │ │
|
|
100
|
+
│ │ └──────────────────┘ └──────────────────┘ │ │
|
|
101
|
+
│ └────────────────────────────────────────────────────────────────────────┘ │
|
|
102
|
+
│ │ │
|
|
103
|
+
│ Cloud Sync API │
|
|
104
|
+
│ │ │
|
|
105
|
+
└────────────────────────────────────┼────────────────────────────────────────┘
|
|
106
|
+
│
|
|
107
|
+
▼
|
|
108
|
+
┌──────────────────────────────────────────────────────────────────────────────┐
|
|
109
|
+
│ LOCAL DAEMON │
|
|
110
|
+
│ │
|
|
111
|
+
│ ┌────────────────────────────────────────────────────────────────────────┐ │
|
|
112
|
+
│ │ src/daemon/ │ │
|
|
113
|
+
│ │ ┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐ │ │
|
|
114
|
+
│ │ │ orchestrator.ts │ │ slack-bridge.ts │ │ cloud-sync.ts │ │ │
|
|
115
|
+
│ │ │ │ │ (NEW) │ │ │ │ │
|
|
116
|
+
│ │ │ • Manages │ │ │ │ • Pulls Slack │ │ │
|
|
117
|
+
│ │ │ workspaces │ │ • Slack ↔ Relay │ │ credentials │ │ │
|
|
118
|
+
│ │ │ • Starts bridge │ │ • Thread mapping │ │ • Syncs config │ │ │
|
|
119
|
+
│ │ │ per workspace │ │ • Event handling │ │ • Token refresh │ │ │
|
|
120
|
+
│ │ └──────────────────┘ └──────────────────┘ └──────────────────┘ │ │
|
|
121
|
+
│ │ │ │
|
|
122
|
+
│ │ ┌──────────────────┐ ┌──────────────────┐ │ │
|
|
123
|
+
│ │ │ router.ts │ │ agent-manager.ts │ │ │
|
|
124
|
+
│ │ │ │ │ │ │ │
|
|
125
|
+
│ │ │ • Routes msgs │ │ • Agent lifecycle│ │ │
|
|
126
|
+
│ │ │ • SlackBridge │ │ • Health monitor │ │ │
|
|
127
|
+
│ │ │ as agent │ │ │ │ │
|
|
128
|
+
│ │ └──────────────────┘ └──────────────────┘ │ │
|
|
129
|
+
│ └────────────────────────────────────────────────────────────────────────┘ │
|
|
130
|
+
│ │ │
|
|
131
|
+
│ Unix Domain Socket │
|
|
132
|
+
│ │ │
|
|
133
|
+
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
|
|
134
|
+
│ │ Alice │ │ Bob │ │ SlackBridge │ │ Dashboard │ │
|
|
135
|
+
│ │ (Claude) │ │ (Claude) │ │ (daemon) │ │ (observer) │ │
|
|
136
|
+
│ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ │
|
|
137
|
+
└──────────────────────────────────────────────────────────────────────────────┘
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### Deployment Model Alignment
|
|
141
|
+
|
|
142
|
+
| Mode | How Slack Integration Works |
|
|
143
|
+
|------|----------------------------|
|
|
144
|
+
| **Cloud Hosted** | OAuth via cloud, credentials in vault, daemon bridge runs in cloud workspace |
|
|
145
|
+
| **Self-Hosted** | OAuth via cloud servers, credentials synced to local daemon |
|
|
146
|
+
| **Hybrid/Local** | OAuth via cloud, daemon runs locally with synced credentials |
|
|
147
|
+
|
|
148
|
+
This follows the same pattern as provider credentials (Claude API keys, etc.) established in PR #35.
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
## 3. Cloud Components
|
|
153
|
+
|
|
154
|
+
### 3.1 Nango Integration
|
|
155
|
+
|
|
156
|
+
[Nango](https://www.nango.dev/) handles all OAuth complexity:
|
|
157
|
+
- OAuth flow (authorization URL, token exchange)
|
|
158
|
+
- Secure token storage (encrypted at rest)
|
|
159
|
+
- Automatic token refresh before expiry
|
|
160
|
+
- Connection status monitoring
|
|
161
|
+
|
|
162
|
+
**Nango Setup:**
|
|
163
|
+
```yaml
|
|
164
|
+
# nango.yaml - Integration configuration
|
|
165
|
+
integrations:
|
|
166
|
+
slack:
|
|
167
|
+
provider: slack
|
|
168
|
+
syncs: []
|
|
169
|
+
actions:
|
|
170
|
+
- name: post-message
|
|
171
|
+
- name: list-channels
|
|
172
|
+
scopes:
|
|
173
|
+
- app_mentions:read
|
|
174
|
+
- channels:history
|
|
175
|
+
- channels:read
|
|
176
|
+
- chat:write
|
|
177
|
+
- groups:history
|
|
178
|
+
- groups:read
|
|
179
|
+
- im:history
|
|
180
|
+
- im:read
|
|
181
|
+
- im:write
|
|
182
|
+
- users:read
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### 3.2 API Routes (`src/cloud/api/integrations/slack.ts`)
|
|
186
|
+
|
|
187
|
+
```typescript
|
|
188
|
+
// src/cloud/api/integrations/slack.ts
|
|
189
|
+
|
|
190
|
+
import { Router } from 'express';
|
|
191
|
+
import { Nango } from '@nangohq/node';
|
|
192
|
+
import { requireAuth } from '../middleware/auth';
|
|
193
|
+
import { requirePlan } from '../middleware/planLimits';
|
|
194
|
+
import { SlackService } from '../../services/slack';
|
|
195
|
+
import { db, eq } from '../../db';
|
|
196
|
+
import { slackIntegrations } from '../../db/schema';
|
|
197
|
+
|
|
198
|
+
const router = Router();
|
|
199
|
+
const nango = new Nango({ secretKey: process.env.NANGO_SECRET_KEY });
|
|
200
|
+
|
|
201
|
+
// All Slack routes require Pro+ plan
|
|
202
|
+
router.use(requireAuth, requirePlan('pro'));
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* GET /api/integrations/slack/status
|
|
206
|
+
* Get Slack integration status for current workspace
|
|
207
|
+
*/
|
|
208
|
+
router.get('/status', async (req, res) => {
|
|
209
|
+
const { workspaceId } = req.query;
|
|
210
|
+
|
|
211
|
+
const integration = await db.query.slackIntegrations.findFirst({
|
|
212
|
+
where: eq(slackIntegrations.workspaceId, workspaceId),
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
if (!integration) {
|
|
216
|
+
return res.json({ connected: false });
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Check Nango connection status
|
|
220
|
+
try {
|
|
221
|
+
const connection = await nango.getConnection('slack', integration.nangoConnectionId);
|
|
222
|
+
|
|
223
|
+
res.json({
|
|
224
|
+
connected: true,
|
|
225
|
+
valid: connection.credentials?.access_token != null,
|
|
226
|
+
slackWorkspace: integration.slackWorkspaceName,
|
|
227
|
+
slackTeamId: integration.slackTeamId,
|
|
228
|
+
channels: {
|
|
229
|
+
broadcast: integration.broadcastChannel,
|
|
230
|
+
alerts: integration.alertsChannel,
|
|
231
|
+
},
|
|
232
|
+
connectedAt: integration.createdAt,
|
|
233
|
+
connectedBy: integration.connectedByUserId,
|
|
234
|
+
});
|
|
235
|
+
} catch (error) {
|
|
236
|
+
res.json({ connected: true, valid: false, error: 'Token expired' });
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* GET /api/integrations/slack/connect
|
|
242
|
+
* Get Nango connect URL for Slack OAuth
|
|
243
|
+
*/
|
|
244
|
+
router.get('/connect', async (req, res) => {
|
|
245
|
+
const { workspaceId } = req.query;
|
|
246
|
+
|
|
247
|
+
// Generate unique connection ID for this workspace
|
|
248
|
+
const connectionId = `workspace-${workspaceId}-slack`;
|
|
249
|
+
|
|
250
|
+
// Nango handles the entire OAuth flow
|
|
251
|
+
const connectUrl = await nango.auth('slack', connectionId, {
|
|
252
|
+
detectClosedAuthWindow: true,
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
// Store pending connection
|
|
256
|
+
await db.insert(slackIntegrations).values({
|
|
257
|
+
id: generateId(),
|
|
258
|
+
workspaceId,
|
|
259
|
+
nangoConnectionId: connectionId,
|
|
260
|
+
status: 'pending',
|
|
261
|
+
connectedByUserId: req.user.id,
|
|
262
|
+
}).onConflictDoUpdate({
|
|
263
|
+
target: slackIntegrations.workspaceId,
|
|
264
|
+
set: { status: 'pending', updatedAt: new Date() },
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
res.json({ connectUrl });
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* POST /api/integrations/slack/webhook
|
|
272
|
+
* Nango webhook for connection events
|
|
273
|
+
*/
|
|
274
|
+
router.post('/webhook', async (req, res) => {
|
|
275
|
+
const { type, connectionId, provider } = req.body;
|
|
276
|
+
|
|
277
|
+
if (provider !== 'slack') {
|
|
278
|
+
return res.json({ ok: true });
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Extract workspaceId from connectionId
|
|
282
|
+
const match = connectionId.match(/^workspace-(.+)-slack$/);
|
|
283
|
+
if (!match) {
|
|
284
|
+
return res.status(400).json({ error: 'Invalid connection ID' });
|
|
285
|
+
}
|
|
286
|
+
const workspaceId = match[1];
|
|
287
|
+
|
|
288
|
+
if (type === 'auth') {
|
|
289
|
+
// OAuth completed - fetch team info
|
|
290
|
+
const connection = await nango.getConnection('slack', connectionId);
|
|
291
|
+
const teamInfo = connection.connection_config?.team;
|
|
292
|
+
|
|
293
|
+
await db.update(slackIntegrations)
|
|
294
|
+
.set({
|
|
295
|
+
status: 'connected',
|
|
296
|
+
slackTeamId: teamInfo?.id,
|
|
297
|
+
slackWorkspaceName: teamInfo?.name,
|
|
298
|
+
updatedAt: new Date(),
|
|
299
|
+
})
|
|
300
|
+
.where(eq(slackIntegrations.workspaceId, workspaceId));
|
|
301
|
+
|
|
302
|
+
// Notify daemon to connect
|
|
303
|
+
await SlackService.notifyDaemonConnect(workspaceId);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (type === 'token_refreshed') {
|
|
307
|
+
// Token was refreshed - notify daemon to reconnect
|
|
308
|
+
await SlackService.notifyDaemonReconnect(workspaceId);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
res.json({ ok: true });
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* POST /api/integrations/slack/disconnect
|
|
316
|
+
* Disconnect Slack integration
|
|
317
|
+
*/
|
|
318
|
+
router.post('/disconnect', async (req, res) => {
|
|
319
|
+
const { workspaceId } = req.body;
|
|
320
|
+
|
|
321
|
+
const integration = await db.query.slackIntegrations.findFirst({
|
|
322
|
+
where: eq(slackIntegrations.workspaceId, workspaceId),
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
if (!integration) {
|
|
326
|
+
return res.status(404).json({ error: 'No Slack integration found' });
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Delete connection from Nango (revokes token)
|
|
330
|
+
await nango.deleteConnection('slack', integration.nangoConnectionId);
|
|
331
|
+
|
|
332
|
+
// Delete integration record
|
|
333
|
+
await db.delete(slackIntegrations)
|
|
334
|
+
.where(eq(slackIntegrations.id, integration.id));
|
|
335
|
+
|
|
336
|
+
// Notify daemon to disconnect
|
|
337
|
+
await SlackService.notifyDaemonDisconnect(workspaceId);
|
|
338
|
+
|
|
339
|
+
res.json({ success: true });
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* PUT /api/integrations/slack/config
|
|
344
|
+
* Update Slack integration configuration
|
|
345
|
+
*/
|
|
346
|
+
router.put('/config', async (req, res) => {
|
|
347
|
+
const { workspaceId, broadcastChannel, alertsChannel, showAgentToAgent, showThinking } = req.body;
|
|
348
|
+
|
|
349
|
+
await db.update(slackIntegrations)
|
|
350
|
+
.set({
|
|
351
|
+
broadcastChannel,
|
|
352
|
+
alertsChannel,
|
|
353
|
+
config: { showAgentToAgent, showThinking },
|
|
354
|
+
updatedAt: new Date(),
|
|
355
|
+
})
|
|
356
|
+
.where(eq(slackIntegrations.workspaceId, workspaceId));
|
|
357
|
+
|
|
358
|
+
// Notify daemon to reload config
|
|
359
|
+
await SlackService.notifyDaemonConfigUpdate(workspaceId);
|
|
360
|
+
|
|
361
|
+
res.json({ success: true });
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* GET /api/integrations/slack/channels
|
|
366
|
+
* List available Slack channels for configuration
|
|
367
|
+
*/
|
|
368
|
+
router.get('/channels', async (req, res) => {
|
|
369
|
+
const { workspaceId } = req.query;
|
|
370
|
+
|
|
371
|
+
const channels = await SlackService.listChannels(workspaceId);
|
|
372
|
+
res.json({ channels });
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
export default router;
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
### 3.3 Slack Service (`src/cloud/services/slack.ts`)
|
|
379
|
+
|
|
380
|
+
```typescript
|
|
381
|
+
// src/cloud/services/slack.ts
|
|
382
|
+
|
|
383
|
+
import { Nango } from '@nangohq/node';
|
|
384
|
+
import { WebClient } from '@slack/web-api';
|
|
385
|
+
import { db, eq } from '../db';
|
|
386
|
+
import { slackIntegrations } from '../db/schema';
|
|
387
|
+
|
|
388
|
+
const nango = new Nango({ secretKey: process.env.NANGO_SECRET_KEY });
|
|
389
|
+
|
|
390
|
+
export class SlackService {
|
|
391
|
+
/**
|
|
392
|
+
* Get Slack access token from Nango
|
|
393
|
+
* Nango automatically refreshes expired tokens
|
|
394
|
+
*/
|
|
395
|
+
static async getAccessToken(workspaceId: string): Promise<string | null> {
|
|
396
|
+
const integration = await db.query.slackIntegrations.findFirst({
|
|
397
|
+
where: eq(slackIntegrations.workspaceId, workspaceId),
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
if (!integration?.nangoConnectionId) return null;
|
|
401
|
+
|
|
402
|
+
try {
|
|
403
|
+
const connection = await nango.getConnection('slack', integration.nangoConnectionId);
|
|
404
|
+
return connection.credentials?.access_token || null;
|
|
405
|
+
} catch {
|
|
406
|
+
return null;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* List channels the bot can access
|
|
412
|
+
*/
|
|
413
|
+
static async listChannels(workspaceId: string): Promise<SlackChannel[]> {
|
|
414
|
+
const token = await this.getAccessToken(workspaceId);
|
|
415
|
+
if (!token) throw new Error('No Slack connection found');
|
|
416
|
+
|
|
417
|
+
const client = new WebClient(token);
|
|
418
|
+
const result = await client.conversations.list({
|
|
419
|
+
types: 'public_channel,private_channel',
|
|
420
|
+
exclude_archived: true,
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
return result.channels?.map(ch => ({
|
|
424
|
+
id: ch.id!,
|
|
425
|
+
name: ch.name!,
|
|
426
|
+
isPrivate: ch.is_private || false,
|
|
427
|
+
isMember: ch.is_member || false,
|
|
428
|
+
})) || [];
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Get credentials for daemon sync
|
|
433
|
+
* Called by daemon cloud-sync to retrieve Slack config
|
|
434
|
+
*/
|
|
435
|
+
static async getCredentialsForDaemon(workspaceId: string): Promise<SlackDaemonConfig | null> {
|
|
436
|
+
const integration = await db.query.slackIntegrations.findFirst({
|
|
437
|
+
where: eq(slackIntegrations.workspaceId, workspaceId),
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
if (!integration?.nangoConnectionId) return null;
|
|
441
|
+
|
|
442
|
+
try {
|
|
443
|
+
const connection = await nango.getConnection('slack', integration.nangoConnectionId);
|
|
444
|
+
|
|
445
|
+
if (!connection.credentials?.access_token) return null;
|
|
446
|
+
|
|
447
|
+
return {
|
|
448
|
+
botToken: connection.credentials.access_token,
|
|
449
|
+
// App token for Socket Mode - stored in connection metadata or env
|
|
450
|
+
appToken: process.env.SLACK_APP_TOKEN,
|
|
451
|
+
teamId: integration.slackTeamId!,
|
|
452
|
+
teamName: integration.slackWorkspaceName!,
|
|
453
|
+
broadcastChannel: integration.broadcastChannel,
|
|
454
|
+
alertsChannel: integration.alertsChannel,
|
|
455
|
+
config: integration.config || {},
|
|
456
|
+
};
|
|
457
|
+
} catch {
|
|
458
|
+
return null;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Notify daemon of Slack connection changes
|
|
464
|
+
*/
|
|
465
|
+
static async notifyDaemonConnect(workspaceId: string): Promise<void> {
|
|
466
|
+
await this.sendDaemonNotification(workspaceId, 'slack:connect');
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
static async notifyDaemonDisconnect(workspaceId: string): Promise<void> {
|
|
470
|
+
await this.sendDaemonNotification(workspaceId, 'slack:disconnect');
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
static async notifyDaemonConfigUpdate(workspaceId: string): Promise<void> {
|
|
474
|
+
await this.sendDaemonNotification(workspaceId, 'slack:config-update');
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
static async notifyDaemonReconnect(workspaceId: string): Promise<void> {
|
|
478
|
+
await this.sendDaemonNotification(workspaceId, 'slack:reconnect');
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
private static async sendDaemonNotification(workspaceId: string, event: string): Promise<void> {
|
|
482
|
+
// Implementation depends on daemon connection method
|
|
483
|
+
// Could be WebSocket push, Redis pub/sub, or polling
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
interface SlackChannel {
|
|
488
|
+
id: string;
|
|
489
|
+
name: string;
|
|
490
|
+
isPrivate: boolean;
|
|
491
|
+
isMember: boolean;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
interface SlackDaemonConfig {
|
|
495
|
+
botToken: string;
|
|
496
|
+
appToken: string;
|
|
497
|
+
teamId: string;
|
|
498
|
+
teamName: string;
|
|
499
|
+
broadcastChannel: string | null;
|
|
500
|
+
alertsChannel: string | null;
|
|
501
|
+
config: {
|
|
502
|
+
showAgentToAgent?: boolean;
|
|
503
|
+
showThinking?: boolean;
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
### 3.4 Why Nango?
|
|
509
|
+
|
|
510
|
+
| Concern | Without Nango | With Nango |
|
|
511
|
+
|---------|---------------|------------|
|
|
512
|
+
| OAuth flow | Custom implementation | Hosted UI, handles edge cases |
|
|
513
|
+
| Token storage | Custom vault, encryption | Encrypted at rest, compliant |
|
|
514
|
+
| Token refresh | Manual refresh logic | Automatic, before expiry |
|
|
515
|
+
| Multiple providers | Per-provider code | Unified API |
|
|
516
|
+
| Maintenance | Security updates needed | Managed service |
|
|
517
|
+
|
|
518
|
+
**Dependencies:**
|
|
519
|
+
```json
|
|
520
|
+
{
|
|
521
|
+
"dependencies": {
|
|
522
|
+
"@nangohq/node": "^0.40.0"
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
---
|
|
528
|
+
|
|
529
|
+
## 4. Daemon Components
|
|
530
|
+
|
|
531
|
+
### 4.1 Slack Bridge (`src/daemon/slack-bridge.ts`)
|
|
532
|
+
|
|
533
|
+
```typescript
|
|
534
|
+
// src/daemon/slack-bridge.ts
|
|
535
|
+
|
|
536
|
+
import { App, LogLevel } from '@slack/bolt';
|
|
537
|
+
import { RelayClient } from '../wrapper/client';
|
|
538
|
+
import { SlackThreadStore } from './slack-thread-store';
|
|
539
|
+
import { SlackMessageFormatter } from './slack-formatter';
|
|
540
|
+
import { logger } from '../resiliency/logger';
|
|
541
|
+
|
|
542
|
+
export interface SlackBridgeConfig {
|
|
543
|
+
botToken: string;
|
|
544
|
+
appToken: string;
|
|
545
|
+
teamId: string;
|
|
546
|
+
teamName: string;
|
|
547
|
+
broadcastChannel: string | null;
|
|
548
|
+
alertsChannel: string | null;
|
|
549
|
+
config: {
|
|
550
|
+
showAgentToAgent?: boolean;
|
|
551
|
+
showThinking?: boolean;
|
|
552
|
+
};
|
|
553
|
+
// Relay connection
|
|
554
|
+
socketPath: string;
|
|
555
|
+
workspaceId: string;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
export class SlackBridge {
|
|
559
|
+
private slackApp: App | null = null;
|
|
560
|
+
private relayClient: RelayClient | null = null;
|
|
561
|
+
private threadStore: SlackThreadStore;
|
|
562
|
+
private formatter: SlackMessageFormatter;
|
|
563
|
+
private config: SlackBridgeConfig;
|
|
564
|
+
private running = false;
|
|
565
|
+
|
|
566
|
+
constructor(config: SlackBridgeConfig) {
|
|
567
|
+
this.config = config;
|
|
568
|
+
this.threadStore = new SlackThreadStore();
|
|
569
|
+
this.formatter = new SlackMessageFormatter();
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
async start(): Promise<void> {
|
|
573
|
+
if (this.running) return;
|
|
574
|
+
|
|
575
|
+
logger.info('Starting Slack bridge', {
|
|
576
|
+
workspaceId: this.config.workspaceId,
|
|
577
|
+
slackTeam: this.config.teamName,
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
// 1. Initialize Slack App (Socket Mode)
|
|
581
|
+
this.slackApp = new App({
|
|
582
|
+
token: this.config.botToken,
|
|
583
|
+
appToken: this.config.appToken,
|
|
584
|
+
socketMode: true,
|
|
585
|
+
logLevel: LogLevel.WARN,
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
// 2. Register event handlers
|
|
589
|
+
this.registerSlackHandlers();
|
|
590
|
+
|
|
591
|
+
// 3. Connect to local relay daemon
|
|
592
|
+
this.relayClient = new RelayClient({
|
|
593
|
+
socketPath: this.config.socketPath,
|
|
594
|
+
agentName: 'SlackBridge',
|
|
595
|
+
cli: 'slack',
|
|
596
|
+
reconnect: true,
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
this.relayClient.onMessage = this.onRelayMessage.bind(this);
|
|
600
|
+
this.relayClient.onStateChange = this.onRelayStateChange.bind(this);
|
|
601
|
+
|
|
602
|
+
await this.relayClient.connect();
|
|
603
|
+
|
|
604
|
+
// 4. Subscribe to all messages
|
|
605
|
+
this.relayClient.subscribe('*');
|
|
606
|
+
|
|
607
|
+
// 5. Start Slack app
|
|
608
|
+
await this.slackApp.start();
|
|
609
|
+
|
|
610
|
+
this.running = true;
|
|
611
|
+
logger.info('Slack bridge started successfully');
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
async stop(): Promise<void> {
|
|
615
|
+
if (!this.running) return;
|
|
616
|
+
|
|
617
|
+
logger.info('Stopping Slack bridge');
|
|
618
|
+
|
|
619
|
+
if (this.slackApp) {
|
|
620
|
+
await this.slackApp.stop();
|
|
621
|
+
this.slackApp = null;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
if (this.relayClient) {
|
|
625
|
+
await this.relayClient.disconnect();
|
|
626
|
+
this.relayClient = null;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
this.running = false;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
async updateConfig(newConfig: Partial<SlackBridgeConfig>): Promise<void> {
|
|
633
|
+
// Update config without full restart for channel changes
|
|
634
|
+
this.config = { ...this.config, ...newConfig };
|
|
635
|
+
logger.info('Slack bridge config updated', { workspaceId: this.config.workspaceId });
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
isRunning(): boolean {
|
|
639
|
+
return this.running;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// ─────────────────────────────────────────────────────────────
|
|
643
|
+
// Slack → Relay handlers
|
|
644
|
+
// ─────────────────────────────────────────────────────────────
|
|
645
|
+
|
|
646
|
+
private registerSlackHandlers(): void {
|
|
647
|
+
if (!this.slackApp) return;
|
|
648
|
+
|
|
649
|
+
// Handle @mentions
|
|
650
|
+
this.slackApp.event('app_mention', async ({ event, say }) => {
|
|
651
|
+
await this.handleMention(event, say);
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
// Handle DMs
|
|
655
|
+
this.slackApp.event('message', async ({ event, say }) => {
|
|
656
|
+
if (event.channel_type === 'im' && !event.bot_id) {
|
|
657
|
+
await this.handleDirectMessage(event, say);
|
|
658
|
+
}
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
// Handle thread replies
|
|
662
|
+
this.slackApp.event('message', async ({ event }) => {
|
|
663
|
+
if (event.thread_ts && event.thread_ts !== event.ts && !event.bot_id) {
|
|
664
|
+
await this.handleThreadReply(event);
|
|
665
|
+
}
|
|
666
|
+
});
|
|
667
|
+
|
|
668
|
+
// Slash command
|
|
669
|
+
this.slackApp.command('/relay', async ({ command, ack, respond }) => {
|
|
670
|
+
await ack();
|
|
671
|
+
await this.handleSlashCommand(command, respond);
|
|
672
|
+
});
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
private async handleMention(event: any, say: Function): Promise<void> {
|
|
676
|
+
// Extract agent name from message (e.g., "@relay Alice please help")
|
|
677
|
+
const agentMatch = event.text.match(/<@[A-Z0-9]+>\s*@?(\w+)\s*(.*)/s);
|
|
678
|
+
|
|
679
|
+
if (!agentMatch) {
|
|
680
|
+
await say({
|
|
681
|
+
text: 'Usage: @AgentRelay AgentName your message',
|
|
682
|
+
thread_ts: event.ts,
|
|
683
|
+
});
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
const [, agentName, messageBody] = agentMatch;
|
|
688
|
+
const slackUser = await this.resolveUser(event.user);
|
|
689
|
+
const thread = this.threadStore.getOrCreate(event.thread_ts || event.ts, event.channel);
|
|
690
|
+
|
|
691
|
+
this.relayClient?.sendMessage(
|
|
692
|
+
agentName,
|
|
693
|
+
messageBody.trim(),
|
|
694
|
+
'message',
|
|
695
|
+
{
|
|
696
|
+
slack_user: slackUser,
|
|
697
|
+
slack_channel: event.channel,
|
|
698
|
+
slack_ts: event.ts,
|
|
699
|
+
slack_thread_ts: event.thread_ts,
|
|
700
|
+
},
|
|
701
|
+
thread
|
|
702
|
+
);
|
|
703
|
+
|
|
704
|
+
logger.debug('Forwarded Slack mention to relay', { agentName, slackUser });
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
private async handleDirectMessage(event: any, say: Function): Promise<void> {
|
|
708
|
+
// Parse agent target from DM: "Alice: help me" or just broadcast
|
|
709
|
+
const match = event.text.match(/^@?(\w+):\s*(.+)$/s);
|
|
710
|
+
|
|
711
|
+
const agentName = match ? match[1] : '*';
|
|
712
|
+
const messageBody = match ? match[2] : event.text;
|
|
713
|
+
const slackUser = await this.resolveUser(event.user);
|
|
714
|
+
|
|
715
|
+
this.relayClient?.sendMessage(
|
|
716
|
+
agentName,
|
|
717
|
+
messageBody.trim(),
|
|
718
|
+
'message',
|
|
719
|
+
{
|
|
720
|
+
slack_user: slackUser,
|
|
721
|
+
slack_channel: event.channel,
|
|
722
|
+
slack_ts: event.ts,
|
|
723
|
+
slack_dm: true,
|
|
724
|
+
}
|
|
725
|
+
);
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
private async handleThreadReply(event: any): Promise<void> {
|
|
729
|
+
const relayThread = this.threadStore.getRelayThread(event.thread_ts, event.channel);
|
|
730
|
+
if (!relayThread) return; // Not a relay thread
|
|
731
|
+
|
|
732
|
+
const slackUser = await this.resolveUser(event.user);
|
|
733
|
+
const threadMeta = this.threadStore.getMeta(event.thread_ts, event.channel);
|
|
734
|
+
|
|
735
|
+
this.relayClient?.sendMessage(
|
|
736
|
+
threadMeta?.targetAgent || '*',
|
|
737
|
+
event.text,
|
|
738
|
+
'message',
|
|
739
|
+
{
|
|
740
|
+
slack_user: slackUser,
|
|
741
|
+
slack_thread_ts: event.thread_ts,
|
|
742
|
+
},
|
|
743
|
+
relayThread
|
|
744
|
+
);
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
private async handleSlashCommand(command: any, respond: Function): Promise<void> {
|
|
748
|
+
const match = command.text.match(/^@?(\w+)\s+(.+)$/s);
|
|
749
|
+
|
|
750
|
+
if (!match) {
|
|
751
|
+
await respond('Usage: /relay @AgentName your message');
|
|
752
|
+
return;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
const [, agentName, messageBody] = match;
|
|
756
|
+
const slackUser = await this.resolveUser(command.user_id);
|
|
757
|
+
|
|
758
|
+
this.relayClient?.sendMessage(
|
|
759
|
+
agentName,
|
|
760
|
+
messageBody.trim(),
|
|
761
|
+
'message',
|
|
762
|
+
{
|
|
763
|
+
slack_user: slackUser,
|
|
764
|
+
slack_channel: command.channel_id,
|
|
765
|
+
slack_command: true,
|
|
766
|
+
}
|
|
767
|
+
);
|
|
768
|
+
|
|
769
|
+
await respond(`Message sent to ${agentName}`);
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
// ─────────────────────────────────────────────────────────────
|
|
773
|
+
// Relay → Slack handlers
|
|
774
|
+
// ─────────────────────────────────────────────────────────────
|
|
775
|
+
|
|
776
|
+
private async onRelayMessage(
|
|
777
|
+
from: string,
|
|
778
|
+
payload: { kind: string; body: string; data?: Record<string, unknown>; thread?: string },
|
|
779
|
+
messageId: string,
|
|
780
|
+
meta?: { importance?: number }
|
|
781
|
+
): Promise<void> {
|
|
782
|
+
// Skip self-messages
|
|
783
|
+
if (from === 'SlackBridge') return;
|
|
784
|
+
|
|
785
|
+
// Skip messages originating from Slack (prevent loop)
|
|
786
|
+
if (payload.data?.slack_ts) return;
|
|
787
|
+
|
|
788
|
+
// Skip thinking unless configured
|
|
789
|
+
if (payload.kind === 'thinking' && !this.config.config.showThinking) return;
|
|
790
|
+
|
|
791
|
+
// Determine channel
|
|
792
|
+
let channel = this.config.broadcastChannel;
|
|
793
|
+
let threadTs: string | undefined;
|
|
794
|
+
|
|
795
|
+
// Reply to Slack conversation
|
|
796
|
+
if (payload.data?.slack_channel) {
|
|
797
|
+
channel = payload.data.slack_channel as string;
|
|
798
|
+
threadTs = payload.data.slack_thread_ts as string;
|
|
799
|
+
}
|
|
800
|
+
// High importance → alerts channel
|
|
801
|
+
else if (meta?.importance && meta.importance >= 80 && this.config.alertsChannel) {
|
|
802
|
+
channel = this.config.alertsChannel;
|
|
803
|
+
}
|
|
804
|
+
// Map relay thread to Slack thread
|
|
805
|
+
else if (payload.thread) {
|
|
806
|
+
const slackThread = this.threadStore.getSlackThread(payload.thread);
|
|
807
|
+
if (slackThread) {
|
|
808
|
+
channel = slackThread.channel;
|
|
809
|
+
threadTs = slackThread.ts;
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
if (!channel) {
|
|
814
|
+
logger.warn('No channel configured for Slack message');
|
|
815
|
+
return;
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
const formatted = this.formatter.format(from, payload);
|
|
819
|
+
|
|
820
|
+
try {
|
|
821
|
+
const result = await this.slackApp?.client.chat.postMessage({
|
|
822
|
+
channel,
|
|
823
|
+
text: formatted,
|
|
824
|
+
thread_ts: threadTs,
|
|
825
|
+
unfurl_links: false,
|
|
826
|
+
unfurl_media: false,
|
|
827
|
+
});
|
|
828
|
+
|
|
829
|
+
// Track thread mapping
|
|
830
|
+
if (result?.ts && payload.thread) {
|
|
831
|
+
this.threadStore.map(payload.thread, {
|
|
832
|
+
ts: result.ts,
|
|
833
|
+
channel,
|
|
834
|
+
targetAgent: from,
|
|
835
|
+
});
|
|
836
|
+
}
|
|
837
|
+
} catch (error) {
|
|
838
|
+
logger.error('Failed to post to Slack', { error, channel });
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
private onRelayStateChange(state: string): void {
|
|
843
|
+
logger.info('Slack bridge relay connection state', { state });
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
private async resolveUser(userId: string): Promise<string> {
|
|
847
|
+
try {
|
|
848
|
+
const result = await this.slackApp?.client.users.info({ user: userId });
|
|
849
|
+
return result?.user?.real_name || result?.user?.name || userId;
|
|
850
|
+
} catch {
|
|
851
|
+
return userId;
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
```
|
|
856
|
+
|
|
857
|
+
### 4.2 Integration with Orchestrator (`src/daemon/orchestrator.ts`)
|
|
858
|
+
|
|
859
|
+
```typescript
|
|
860
|
+
// Addition to src/daemon/orchestrator.ts
|
|
861
|
+
|
|
862
|
+
import { SlackBridge, SlackBridgeConfig } from './slack-bridge';
|
|
863
|
+
|
|
864
|
+
export class DaemonOrchestrator {
|
|
865
|
+
private slackBridges: Map<string, SlackBridge> = new Map();
|
|
866
|
+
|
|
867
|
+
// Called during workspace initialization or when Slack is connected
|
|
868
|
+
async initializeSlackBridge(workspaceId: string): Promise<void> {
|
|
869
|
+
// Check if already running
|
|
870
|
+
if (this.slackBridges.has(workspaceId)) {
|
|
871
|
+
return;
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
// Get Slack config from cloud sync
|
|
875
|
+
const slackConfig = await this.cloudSync.getSlackConfig(workspaceId);
|
|
876
|
+
if (!slackConfig) {
|
|
877
|
+
logger.debug('No Slack integration for workspace', { workspaceId });
|
|
878
|
+
return;
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
const bridge = new SlackBridge({
|
|
882
|
+
...slackConfig,
|
|
883
|
+
socketPath: this.getSocketPath(workspaceId),
|
|
884
|
+
workspaceId,
|
|
885
|
+
});
|
|
886
|
+
|
|
887
|
+
try {
|
|
888
|
+
await bridge.start();
|
|
889
|
+
this.slackBridges.set(workspaceId, bridge);
|
|
890
|
+
logger.info('Slack bridge initialized', { workspaceId });
|
|
891
|
+
} catch (error) {
|
|
892
|
+
logger.error('Failed to initialize Slack bridge', { workspaceId, error });
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
async stopSlackBridge(workspaceId: string): Promise<void> {
|
|
897
|
+
const bridge = this.slackBridges.get(workspaceId);
|
|
898
|
+
if (bridge) {
|
|
899
|
+
await bridge.stop();
|
|
900
|
+
this.slackBridges.delete(workspaceId);
|
|
901
|
+
logger.info('Slack bridge stopped', { workspaceId });
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
// Called when cloud sync receives Slack notification
|
|
906
|
+
async handleSlackNotification(workspaceId: string, event: string): Promise<void> {
|
|
907
|
+
switch (event) {
|
|
908
|
+
case 'slack:connect':
|
|
909
|
+
await this.initializeSlackBridge(workspaceId);
|
|
910
|
+
break;
|
|
911
|
+
case 'slack:disconnect':
|
|
912
|
+
await this.stopSlackBridge(workspaceId);
|
|
913
|
+
break;
|
|
914
|
+
case 'slack:config-update':
|
|
915
|
+
const bridge = this.slackBridges.get(workspaceId);
|
|
916
|
+
if (bridge) {
|
|
917
|
+
const newConfig = await this.cloudSync.getSlackConfig(workspaceId);
|
|
918
|
+
if (newConfig) {
|
|
919
|
+
await bridge.updateConfig(newConfig);
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
break;
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
// Health check includes Slack bridges
|
|
927
|
+
getHealth(): DaemonHealth {
|
|
928
|
+
return {
|
|
929
|
+
...this.baseHealth(),
|
|
930
|
+
slackBridges: Array.from(this.slackBridges.entries()).map(([id, bridge]) => ({
|
|
931
|
+
workspaceId: id,
|
|
932
|
+
running: bridge.isRunning(),
|
|
933
|
+
})),
|
|
934
|
+
};
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
```
|
|
938
|
+
|
|
939
|
+
### 4.3 Cloud Sync Extension (`src/daemon/cloud-sync.ts`)
|
|
940
|
+
|
|
941
|
+
```typescript
|
|
942
|
+
// Addition to src/daemon/cloud-sync.ts
|
|
943
|
+
|
|
944
|
+
export class CloudSync {
|
|
945
|
+
/**
|
|
946
|
+
* Get Slack configuration for a workspace
|
|
947
|
+
* Called by orchestrator when initializing Slack bridge
|
|
948
|
+
*/
|
|
949
|
+
async getSlackConfig(workspaceId: string): Promise<SlackBridgeConfig | null> {
|
|
950
|
+
try {
|
|
951
|
+
const response = await this.apiClient.get(
|
|
952
|
+
`/api/integrations/slack/daemon-config?workspaceId=${workspaceId}`
|
|
953
|
+
);
|
|
954
|
+
|
|
955
|
+
if (!response.data.connected) {
|
|
956
|
+
return null;
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
return response.data.config;
|
|
960
|
+
} catch (error) {
|
|
961
|
+
logger.error('Failed to fetch Slack config', { workspaceId, error });
|
|
962
|
+
return null;
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
/**
|
|
967
|
+
* Subscribe to Slack notifications
|
|
968
|
+
*/
|
|
969
|
+
subscribeToSlackNotifications(callback: (workspaceId: string, event: string) => void): void {
|
|
970
|
+
// WebSocket subscription or polling
|
|
971
|
+
this.on('slack:notification', callback);
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
```
|
|
975
|
+
|
|
976
|
+
---
|
|
977
|
+
|
|
978
|
+
## 5. Database Schema
|
|
979
|
+
|
|
980
|
+
### 5.1 Drizzle Schema (`src/cloud/db/schema.ts`)
|
|
981
|
+
|
|
982
|
+
```typescript
|
|
983
|
+
// Addition to src/cloud/db/schema.ts
|
|
984
|
+
|
|
985
|
+
import { pgTable, text, timestamp, jsonb, boolean } from 'drizzle-orm/pg-core';
|
|
986
|
+
|
|
987
|
+
export const slackIntegrations = pgTable('slack_integrations', {
|
|
988
|
+
id: text('id').primaryKey(),
|
|
989
|
+
workspaceId: text('workspace_id').notNull().references(() => workspaces.id, { onDelete: 'cascade' }),
|
|
990
|
+
|
|
991
|
+
// Nango connection (handles OAuth, token storage, refresh)
|
|
992
|
+
nangoConnectionId: text('nango_connection_id').notNull(),
|
|
993
|
+
status: text('status').notNull().default('pending'), // pending, connected, error
|
|
994
|
+
|
|
995
|
+
// Slack workspace info (populated after OAuth)
|
|
996
|
+
slackTeamId: text('slack_team_id'),
|
|
997
|
+
slackWorkspaceName: text('slack_workspace_name'),
|
|
998
|
+
|
|
999
|
+
// Channel configuration
|
|
1000
|
+
broadcastChannel: text('broadcast_channel'),
|
|
1001
|
+
alertsChannel: text('alerts_channel'),
|
|
1002
|
+
|
|
1003
|
+
// Behavior configuration
|
|
1004
|
+
config: jsonb('config').$type<{
|
|
1005
|
+
showAgentToAgent?: boolean;
|
|
1006
|
+
showThinking?: boolean;
|
|
1007
|
+
threadTTLHours?: number;
|
|
1008
|
+
}>().default({}),
|
|
1009
|
+
|
|
1010
|
+
// Metadata
|
|
1011
|
+
connectedByUserId: text('connected_by_user_id').references(() => users.id),
|
|
1012
|
+
createdAt: timestamp('created_at').defaultNow().notNull(),
|
|
1013
|
+
updatedAt: timestamp('updated_at').defaultNow().notNull(),
|
|
1014
|
+
});
|
|
1015
|
+
|
|
1016
|
+
export const slackChannelMappings = pgTable('slack_channel_mappings', {
|
|
1017
|
+
id: text('id').primaryKey(),
|
|
1018
|
+
integrationId: text('integration_id').notNull().references(() => slackIntegrations.id, { onDelete: 'cascade' }),
|
|
1019
|
+
|
|
1020
|
+
// Relay topic → Slack channel mapping
|
|
1021
|
+
relayTopic: text('relay_topic').notNull(),
|
|
1022
|
+
slackChannelId: text('slack_channel_id').notNull(),
|
|
1023
|
+
slackChannelName: text('slack_channel_name'),
|
|
1024
|
+
|
|
1025
|
+
createdAt: timestamp('created_at').defaultNow().notNull(),
|
|
1026
|
+
});
|
|
1027
|
+
|
|
1028
|
+
// Index for fast lookup
|
|
1029
|
+
export const slackIntegrationsWorkspaceIdx = index('slack_integrations_workspace_idx')
|
|
1030
|
+
.on(slackIntegrations.workspaceId);
|
|
1031
|
+
```
|
|
1032
|
+
|
|
1033
|
+
### 5.2 SQL Migration
|
|
1034
|
+
|
|
1035
|
+
```sql
|
|
1036
|
+
-- deploy/migrations/004_slack_integrations.sql
|
|
1037
|
+
|
|
1038
|
+
CREATE TABLE slack_integrations (
|
|
1039
|
+
id TEXT PRIMARY KEY,
|
|
1040
|
+
workspace_id TEXT NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
|
|
1041
|
+
nango_connection_id TEXT NOT NULL,
|
|
1042
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
1043
|
+
slack_team_id TEXT,
|
|
1044
|
+
slack_workspace_name TEXT,
|
|
1045
|
+
broadcast_channel TEXT,
|
|
1046
|
+
alerts_channel TEXT,
|
|
1047
|
+
config JSONB DEFAULT '{}',
|
|
1048
|
+
connected_by_user_id TEXT REFERENCES users(id),
|
|
1049
|
+
created_at TIMESTAMP DEFAULT NOW() NOT NULL,
|
|
1050
|
+
updated_at TIMESTAMP DEFAULT NOW() NOT NULL
|
|
1051
|
+
);
|
|
1052
|
+
|
|
1053
|
+
CREATE INDEX slack_integrations_workspace_idx ON slack_integrations(workspace_id);
|
|
1054
|
+
|
|
1055
|
+
CREATE TABLE slack_channel_mappings (
|
|
1056
|
+
id TEXT PRIMARY KEY,
|
|
1057
|
+
integration_id TEXT NOT NULL REFERENCES slack_integrations(id) ON DELETE CASCADE,
|
|
1058
|
+
relay_topic TEXT NOT NULL,
|
|
1059
|
+
slack_channel_id TEXT NOT NULL,
|
|
1060
|
+
slack_channel_name TEXT,
|
|
1061
|
+
created_at TIMESTAMP DEFAULT NOW() NOT NULL
|
|
1062
|
+
);
|
|
1063
|
+
|
|
1064
|
+
CREATE INDEX slack_channel_mappings_integration_idx ON slack_channel_mappings(integration_id);
|
|
1065
|
+
```
|
|
1066
|
+
|
|
1067
|
+
---
|
|
1068
|
+
|
|
1069
|
+
## 6. Dashboard UI
|
|
1070
|
+
|
|
1071
|
+
### 6.1 Slack Integration Panel (`src/dashboard/react-components/SlackIntegrationPanel.tsx`)
|
|
1072
|
+
|
|
1073
|
+
```typescript
|
|
1074
|
+
// src/dashboard/react-components/SlackIntegrationPanel.tsx
|
|
1075
|
+
|
|
1076
|
+
import React, { useState, useEffect } from 'react';
|
|
1077
|
+
import { useSession } from './hooks/useSession';
|
|
1078
|
+
import { api } from '../lib/api';
|
|
1079
|
+
|
|
1080
|
+
interface SlackStatus {
|
|
1081
|
+
connected: boolean;
|
|
1082
|
+
valid?: boolean;
|
|
1083
|
+
slackWorkspace?: string;
|
|
1084
|
+
channels?: {
|
|
1085
|
+
broadcast: string | null;
|
|
1086
|
+
alerts: string | null;
|
|
1087
|
+
};
|
|
1088
|
+
connectedAt?: string;
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
export function SlackIntegrationPanel({ workspaceId }: { workspaceId: string }) {
|
|
1092
|
+
const { plan } = useSession();
|
|
1093
|
+
const [status, setStatus] = useState<SlackStatus | null>(null);
|
|
1094
|
+
const [channels, setChannels] = useState<{ id: string; name: string }[]>([]);
|
|
1095
|
+
const [loading, setLoading] = useState(true);
|
|
1096
|
+
const [configuring, setConfiguring] = useState(false);
|
|
1097
|
+
|
|
1098
|
+
// Config form state
|
|
1099
|
+
const [broadcastChannel, setBroadcastChannel] = useState('');
|
|
1100
|
+
const [alertsChannel, setAlertsChannel] = useState('');
|
|
1101
|
+
const [showAgentToAgent, setShowAgentToAgent] = useState(true);
|
|
1102
|
+
|
|
1103
|
+
useEffect(() => {
|
|
1104
|
+
loadStatus();
|
|
1105
|
+
}, [workspaceId]);
|
|
1106
|
+
|
|
1107
|
+
async function loadStatus() {
|
|
1108
|
+
setLoading(true);
|
|
1109
|
+
try {
|
|
1110
|
+
const res = await api.get(`/api/integrations/slack/status?workspaceId=${workspaceId}`);
|
|
1111
|
+
setStatus(res.data);
|
|
1112
|
+
if (res.data.connected) {
|
|
1113
|
+
setBroadcastChannel(res.data.channels?.broadcast || '');
|
|
1114
|
+
setAlertsChannel(res.data.channels?.alerts || '');
|
|
1115
|
+
loadChannels();
|
|
1116
|
+
}
|
|
1117
|
+
} catch (error) {
|
|
1118
|
+
console.error('Failed to load Slack status', error);
|
|
1119
|
+
}
|
|
1120
|
+
setLoading(false);
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
async function loadChannels() {
|
|
1124
|
+
try {
|
|
1125
|
+
const res = await api.get(`/api/integrations/slack/channels?workspaceId=${workspaceId}`);
|
|
1126
|
+
setChannels(res.data.channels);
|
|
1127
|
+
} catch (error) {
|
|
1128
|
+
console.error('Failed to load channels', error);
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
async function handleConnect() {
|
|
1133
|
+
try {
|
|
1134
|
+
const res = await api.get(`/api/integrations/slack/oauth/start?workspaceId=${workspaceId}`);
|
|
1135
|
+
window.location.href = res.data.authUrl;
|
|
1136
|
+
} catch (error) {
|
|
1137
|
+
console.error('Failed to start OAuth', error);
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
async function handleDisconnect() {
|
|
1142
|
+
if (!confirm('Disconnect Slack integration?')) return;
|
|
1143
|
+
|
|
1144
|
+
try {
|
|
1145
|
+
await api.post('/api/integrations/slack/disconnect', { workspaceId });
|
|
1146
|
+
setStatus({ connected: false });
|
|
1147
|
+
} catch (error) {
|
|
1148
|
+
console.error('Failed to disconnect', error);
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
async function handleSaveConfig() {
|
|
1153
|
+
setConfiguring(true);
|
|
1154
|
+
try {
|
|
1155
|
+
await api.put('/api/integrations/slack/config', {
|
|
1156
|
+
workspaceId,
|
|
1157
|
+
broadcastChannel,
|
|
1158
|
+
alertsChannel,
|
|
1159
|
+
showAgentToAgent,
|
|
1160
|
+
});
|
|
1161
|
+
await loadStatus();
|
|
1162
|
+
} catch (error) {
|
|
1163
|
+
console.error('Failed to save config', error);
|
|
1164
|
+
}
|
|
1165
|
+
setConfiguring(false);
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
// Plan check
|
|
1169
|
+
if (plan === 'free') {
|
|
1170
|
+
return (
|
|
1171
|
+
<div className="slack-panel disabled">
|
|
1172
|
+
<h3>Slack Integration</h3>
|
|
1173
|
+
<p>Slack integration is available on Pro plans and above.</p>
|
|
1174
|
+
<a href="/pricing" className="upgrade-btn">Upgrade to Pro</a>
|
|
1175
|
+
</div>
|
|
1176
|
+
);
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
if (loading) {
|
|
1180
|
+
return <div className="slack-panel loading">Loading...</div>;
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
return (
|
|
1184
|
+
<div className="slack-panel">
|
|
1185
|
+
<h3>Slack Integration</h3>
|
|
1186
|
+
|
|
1187
|
+
{!status?.connected ? (
|
|
1188
|
+
<div className="slack-connect">
|
|
1189
|
+
<p>Connect Slack to see agent messages in your workspace.</p>
|
|
1190
|
+
<button onClick={handleConnect} className="connect-btn">
|
|
1191
|
+
<SlackLogo /> Connect to Slack
|
|
1192
|
+
</button>
|
|
1193
|
+
</div>
|
|
1194
|
+
) : (
|
|
1195
|
+
<div className="slack-connected">
|
|
1196
|
+
<div className="status-row">
|
|
1197
|
+
<span className={`status-dot ${status.valid ? 'green' : 'red'}`} />
|
|
1198
|
+
<span>Connected to <strong>{status.slackWorkspace}</strong></span>
|
|
1199
|
+
<button onClick={handleDisconnect} className="disconnect-btn">Disconnect</button>
|
|
1200
|
+
</div>
|
|
1201
|
+
|
|
1202
|
+
<div className="config-section">
|
|
1203
|
+
<h4>Channel Configuration</h4>
|
|
1204
|
+
|
|
1205
|
+
<label>
|
|
1206
|
+
Broadcast Channel
|
|
1207
|
+
<select value={broadcastChannel} onChange={e => setBroadcastChannel(e.target.value)}>
|
|
1208
|
+
<option value="">Select channel...</option>
|
|
1209
|
+
{channels.map(ch => (
|
|
1210
|
+
<option key={ch.id} value={ch.id}>#{ch.name}</option>
|
|
1211
|
+
))}
|
|
1212
|
+
</select>
|
|
1213
|
+
</label>
|
|
1214
|
+
|
|
1215
|
+
<label>
|
|
1216
|
+
Alerts Channel (optional)
|
|
1217
|
+
<select value={alertsChannel} onChange={e => setAlertsChannel(e.target.value)}>
|
|
1218
|
+
<option value="">None</option>
|
|
1219
|
+
{channels.map(ch => (
|
|
1220
|
+
<option key={ch.id} value={ch.id}>#{ch.name}</option>
|
|
1221
|
+
))}
|
|
1222
|
+
</select>
|
|
1223
|
+
</label>
|
|
1224
|
+
|
|
1225
|
+
<label className="checkbox">
|
|
1226
|
+
<input
|
|
1227
|
+
type="checkbox"
|
|
1228
|
+
checked={showAgentToAgent}
|
|
1229
|
+
onChange={e => setShowAgentToAgent(e.target.checked)}
|
|
1230
|
+
/>
|
|
1231
|
+
Show agent-to-agent messages
|
|
1232
|
+
</label>
|
|
1233
|
+
|
|
1234
|
+
<button onClick={handleSaveConfig} disabled={configuring}>
|
|
1235
|
+
{configuring ? 'Saving...' : 'Save Configuration'}
|
|
1236
|
+
</button>
|
|
1237
|
+
</div>
|
|
1238
|
+
</div>
|
|
1239
|
+
)}
|
|
1240
|
+
</div>
|
|
1241
|
+
);
|
|
1242
|
+
}
|
|
1243
|
+
```
|
|
1244
|
+
|
|
1245
|
+
### 6.2 Integration into Settings Page
|
|
1246
|
+
|
|
1247
|
+
```typescript
|
|
1248
|
+
// In src/dashboard/app/workspace/[id]/settings/page.tsx
|
|
1249
|
+
|
|
1250
|
+
import { SlackIntegrationPanel } from '@/react-components/SlackIntegrationPanel';
|
|
1251
|
+
|
|
1252
|
+
export default function WorkspaceSettings({ params }) {
|
|
1253
|
+
return (
|
|
1254
|
+
<div className="settings-page">
|
|
1255
|
+
<h2>Workspace Settings</h2>
|
|
1256
|
+
|
|
1257
|
+
{/* Other settings... */}
|
|
1258
|
+
|
|
1259
|
+
<section className="integrations-section">
|
|
1260
|
+
<h3>Integrations</h3>
|
|
1261
|
+
<SlackIntegrationPanel workspaceId={params.id} />
|
|
1262
|
+
{/* Future: Discord, Teams, etc. */}
|
|
1263
|
+
</section>
|
|
1264
|
+
</div>
|
|
1265
|
+
);
|
|
1266
|
+
}
|
|
1267
|
+
```
|
|
1268
|
+
|
|
1269
|
+
---
|
|
1270
|
+
|
|
1271
|
+
## 7. Implementation Phases
|
|
1272
|
+
|
|
1273
|
+
### Phase 1: Cloud Infrastructure (Days 1-2)
|
|
1274
|
+
|
|
1275
|
+
- [ ] Database schema and migration
|
|
1276
|
+
- [ ] Slack OAuth API routes
|
|
1277
|
+
- [ ] SlackService for token management
|
|
1278
|
+
- [ ] Vault integration for credentials
|
|
1279
|
+
- [ ] Plan limit middleware
|
|
1280
|
+
|
|
1281
|
+
### Phase 2: Daemon Bridge (Days 2-4)
|
|
1282
|
+
|
|
1283
|
+
- [ ] SlackBridge class (Slack ↔ Relay)
|
|
1284
|
+
- [ ] Thread store for mapping
|
|
1285
|
+
- [ ] Message formatter
|
|
1286
|
+
- [ ] Orchestrator integration
|
|
1287
|
+
- [ ] Cloud sync for credentials
|
|
1288
|
+
|
|
1289
|
+
### Phase 3: Dashboard UI (Days 4-5)
|
|
1290
|
+
|
|
1291
|
+
- [ ] SlackIntegrationPanel component
|
|
1292
|
+
- [ ] OAuth flow UI
|
|
1293
|
+
- [ ] Channel configuration
|
|
1294
|
+
- [ ] Status display
|
|
1295
|
+
|
|
1296
|
+
### Phase 4: Testing & Polish (Days 5-7)
|
|
1297
|
+
|
|
1298
|
+
- [ ] Unit tests for services
|
|
1299
|
+
- [ ] Integration tests
|
|
1300
|
+
- [ ] E2E flow testing
|
|
1301
|
+
- [ ] Documentation
|
|
1302
|
+
- [ ] Error handling & edge cases
|
|
1303
|
+
|
|
1304
|
+
---
|
|
1305
|
+
|
|
1306
|
+
## 8. API Specifications
|
|
1307
|
+
|
|
1308
|
+
### 8.1 Cloud API Endpoints
|
|
1309
|
+
|
|
1310
|
+
| Method | Path | Description | Auth |
|
|
1311
|
+
|--------|------|-------------|------|
|
|
1312
|
+
| GET | `/api/integrations/slack/status` | Get integration status | Pro+ |
|
|
1313
|
+
| GET | `/api/integrations/slack/oauth/start` | Start OAuth flow | Pro+ |
|
|
1314
|
+
| GET | `/api/integrations/slack/oauth/callback` | OAuth callback | Pro+ |
|
|
1315
|
+
| POST | `/api/integrations/slack/disconnect` | Remove integration | Pro+ |
|
|
1316
|
+
| PUT | `/api/integrations/slack/config` | Update config | Pro+ |
|
|
1317
|
+
| GET | `/api/integrations/slack/channels` | List Slack channels | Pro+ |
|
|
1318
|
+
| GET | `/api/integrations/slack/daemon-config` | Get config for daemon | Internal |
|
|
1319
|
+
|
|
1320
|
+
### 8.2 Daemon Sync API
|
|
1321
|
+
|
|
1322
|
+
```typescript
|
|
1323
|
+
// Called by daemon cloud-sync
|
|
1324
|
+
GET /api/integrations/slack/daemon-config?workspaceId=xxx
|
|
1325
|
+
Authorization: Bearer <daemon-token>
|
|
1326
|
+
|
|
1327
|
+
Response:
|
|
1328
|
+
{
|
|
1329
|
+
"connected": true,
|
|
1330
|
+
"config": {
|
|
1331
|
+
"botToken": "xoxb-...",
|
|
1332
|
+
"appToken": "xapp-...",
|
|
1333
|
+
"teamId": "T123",
|
|
1334
|
+
"teamName": "My Workspace",
|
|
1335
|
+
"broadcastChannel": "C456",
|
|
1336
|
+
"alertsChannel": null,
|
|
1337
|
+
"config": { "showAgentToAgent": true }
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
```
|
|
1341
|
+
|
|
1342
|
+
---
|
|
1343
|
+
|
|
1344
|
+
## 9. Security & Plan Limits
|
|
1345
|
+
|
|
1346
|
+
### 9.1 Plan-Based Access
|
|
1347
|
+
|
|
1348
|
+
| Plan | Slack Integration |
|
|
1349
|
+
|------|-------------------|
|
|
1350
|
+
| Free | ❌ Not available |
|
|
1351
|
+
| Pro | ✅ 1 Slack workspace |
|
|
1352
|
+
| Team | ✅ 1 Slack workspace per relay workspace |
|
|
1353
|
+
| Enterprise | ✅ Multiple + Enterprise Grid |
|
|
1354
|
+
|
|
1355
|
+
### 9.2 Credential Security (via Nango)
|
|
1356
|
+
|
|
1357
|
+
- **Encrypted storage**: Nango encrypts all tokens at rest (SOC 2 compliant)
|
|
1358
|
+
- **Token refresh**: Automatic refresh before expiry, no manual logic needed
|
|
1359
|
+
- **Revocation**: `nango.deleteConnection()` revokes and removes tokens
|
|
1360
|
+
- **No local storage**: Credentials fetched on-demand, never written to disk
|
|
1361
|
+
- **Audit logging**: Nango provides connection activity logs
|
|
1362
|
+
|
|
1363
|
+
### 9.3 Plan Limit Middleware
|
|
1364
|
+
|
|
1365
|
+
```typescript
|
|
1366
|
+
// src/cloud/api/middleware/planLimits.ts
|
|
1367
|
+
|
|
1368
|
+
export function requireSlackAccess(req, res, next) {
|
|
1369
|
+
const plan = req.user.plan;
|
|
1370
|
+
|
|
1371
|
+
if (plan === 'free') {
|
|
1372
|
+
return res.status(403).json({
|
|
1373
|
+
error: 'Slack integration requires Pro plan or above',
|
|
1374
|
+
upgrade: '/pricing',
|
|
1375
|
+
});
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
next();
|
|
1379
|
+
}
|
|
1380
|
+
```
|
|
1381
|
+
|
|
1382
|
+
---
|
|
1383
|
+
|
|
1384
|
+
## 10. Testing Strategy
|
|
1385
|
+
|
|
1386
|
+
### 10.1 Unit Tests
|
|
1387
|
+
|
|
1388
|
+
```typescript
|
|
1389
|
+
// src/cloud/services/__tests__/slack.test.ts
|
|
1390
|
+
describe('SlackService', () => {
|
|
1391
|
+
it('builds correct OAuth URL', () => { ... });
|
|
1392
|
+
it('validates tokens correctly', () => { ... });
|
|
1393
|
+
it('handles token revocation', () => { ... });
|
|
1394
|
+
});
|
|
1395
|
+
|
|
1396
|
+
// src/daemon/__tests__/slack-bridge.test.ts
|
|
1397
|
+
describe('SlackBridge', () => {
|
|
1398
|
+
it('forwards relay broadcasts to Slack', () => { ... });
|
|
1399
|
+
it('forwards Slack mentions to relay', () => { ... });
|
|
1400
|
+
it('maps threads bidirectionally', () => { ... });
|
|
1401
|
+
it('prevents message loops', () => { ... });
|
|
1402
|
+
});
|
|
1403
|
+
```
|
|
1404
|
+
|
|
1405
|
+
### 10.2 Integration Tests
|
|
1406
|
+
|
|
1407
|
+
```typescript
|
|
1408
|
+
describe('Slack Integration E2E', () => {
|
|
1409
|
+
it('completes OAuth flow and stores credentials', async () => { ... });
|
|
1410
|
+
it('daemon receives credentials via cloud sync', async () => { ... });
|
|
1411
|
+
it('messages flow relay → slack → relay', async () => { ... });
|
|
1412
|
+
});
|
|
1413
|
+
```
|
|
1414
|
+
|
|
1415
|
+
---
|
|
1416
|
+
|
|
1417
|
+
## 11. Open Questions
|
|
1418
|
+
|
|
1419
|
+
### Q1: Slack App Distribution?
|
|
1420
|
+
|
|
1421
|
+
**Options:**
|
|
1422
|
+
1. **Single Anthropic-owned app**: Users install our app
|
|
1423
|
+
2. **Per-customer apps**: Customers create their own Slack apps
|
|
1424
|
+
3. **Both**: Managed app for cloud, bring-your-own for self-hosted
|
|
1425
|
+
|
|
1426
|
+
**Recommendation:** Single managed app for cloud, instructions for self-hosted.
|
|
1427
|
+
|
|
1428
|
+
### Q2: Enterprise Grid Support?
|
|
1429
|
+
|
|
1430
|
+
Enterprise Grid requires org-level OAuth and cross-workspace routing.
|
|
1431
|
+
|
|
1432
|
+
**Recommendation:** Defer to v2, design schema to support it.
|
|
1433
|
+
|
|
1434
|
+
### Q3: Rate Limit Handling?
|
|
1435
|
+
|
|
1436
|
+
Slack has strict rate limits (1 msg/sec/channel).
|
|
1437
|
+
|
|
1438
|
+
**Recommendation:** Implement queue with backoff in SlackBridge.
|
|
1439
|
+
|
|
1440
|
+
---
|
|
1441
|
+
|
|
1442
|
+
## Summary
|
|
1443
|
+
|
|
1444
|
+
This revised proposal aligns Slack integration with the **cloud-first architecture** from PR #35:
|
|
1445
|
+
|
|
1446
|
+
| Component | Location | Purpose |
|
|
1447
|
+
|-----------|----------|---------|
|
|
1448
|
+
| OAuth & API | `src/cloud/api/integrations/slack.ts` | Cloud endpoints |
|
|
1449
|
+
| Slack Service | `src/cloud/services/slack.ts` | Token retrieval via Nango |
|
|
1450
|
+
| Credentials | Nango (external) | OAuth, storage, refresh |
|
|
1451
|
+
| Database | `src/cloud/db/schema.ts` | Integration config (channels, settings) |
|
|
1452
|
+
| Daemon Bridge | `src/daemon/slack-bridge.ts` | Message routing |
|
|
1453
|
+
| Orchestrator | `src/daemon/orchestrator.ts` | Lifecycle management |
|
|
1454
|
+
| Cloud Sync | `src/daemon/cloud-sync.ts` | Credential retrieval |
|
|
1455
|
+
| Dashboard | `src/dashboard/react-components/` | Configuration UI |
|
|
1456
|
+
|
|
1457
|
+
The integration uses Nango for OAuth/credentials (same pattern planned for other providers like Discord, Teams), keeping our code focused on the Slack ↔ Relay bridge logic.
|