agent-relay 1.6.0 → 2.0.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/.cursor/mcp.json +11 -0
- package/.gitleaks.toml +26 -0
- package/.mcp.json +11 -0
- package/.nvmrc +1 -1
- package/.turbo/cache/013415461711937f-meta.json +1 -0
- package/.turbo/cache/013415461711937f.tar.zst +0 -0
- package/.turbo/cache/0562b1ff326acd6d-meta.json +1 -0
- package/.turbo/cache/0562b1ff326acd6d.tar.zst +0 -0
- package/.turbo/cache/0b46e0e17254882f-meta.json +1 -0
- package/.turbo/cache/0b46e0e17254882f.tar.zst +0 -0
- package/.turbo/cache/3799eda981d53d14-meta.json +1 -0
- package/.turbo/cache/3799eda981d53d14.tar.zst +0 -0
- package/.turbo/cache/47e9d8f404ed064d-meta.json +1 -0
- package/.turbo/cache/47e9d8f404ed064d.tar.zst +0 -0
- package/.turbo/cache/4cde1d1e5b298099-meta.json +1 -0
- package/.turbo/cache/4cde1d1e5b298099.tar.zst +0 -0
- package/.turbo/cache/538eea955c0936ef-meta.json +1 -0
- package/.turbo/cache/538eea955c0936ef.tar.zst +0 -0
- package/.turbo/cache/5dceac7f229f5d5d-meta.json +1 -0
- package/.turbo/cache/5dceac7f229f5d5d.tar.zst +0 -0
- package/.turbo/cache/64c15b201819367d-meta.json +1 -0
- package/.turbo/cache/64c15b201819367d.tar.zst +0 -0
- package/.turbo/cache/6d6a21a05efca434-meta.json +1 -0
- package/.turbo/cache/6d6a21a05efca434.tar.zst +0 -0
- package/.turbo/cache/7562610cb03ec040-meta.json +1 -0
- package/.turbo/cache/7562610cb03ec040.tar.zst +0 -0
- package/.turbo/cache/81a2456e17af4d7f-meta.json +1 -0
- package/.turbo/cache/81a2456e17af4d7f.tar.zst +0 -0
- package/.turbo/cache/823fc2a7b12f724c-meta.json +1 -0
- package/.turbo/cache/823fc2a7b12f724c.tar.zst +0 -0
- package/.turbo/cache/9daad16a073d1f91-meta.json +1 -0
- package/.turbo/cache/9daad16a073d1f91.tar.zst +0 -0
- package/.turbo/cache/b81ccbab0a606b60-meta.json +1 -0
- package/.turbo/cache/b81ccbab0a606b60.tar.zst +0 -0
- package/.turbo/cache/cf98487988bfcf91-meta.json +1 -0
- package/.turbo/cache/cf98487988bfcf91.tar.zst +0 -0
- package/.turbo/cache/cfdf7c57dca71f27-meta.json +1 -0
- package/.turbo/cache/cfdf7c57dca71f27.tar.zst +0 -0
- package/.turbo/cache/d3063ef43811b1e5-meta.json +1 -0
- package/.turbo/cache/d3063ef43811b1e5.tar.zst +0 -0
- package/.turbo/cache/de28892eb7678e65-meta.json +1 -0
- package/.turbo/cache/de28892eb7678e65.tar.zst +0 -0
- package/.turbo/cache/ec29adce408132ba-meta.json +1 -0
- package/.turbo/cache/ec29adce408132ba.tar.zst +0 -0
- package/.turbo/cache/f70450d8d305f172-meta.json +1 -0
- package/.turbo/cache/f70450d8d305f172.tar.zst +0 -0
- package/.turbo/cache/fe384d5d6b7a983a-meta.json +1 -0
- package/.turbo/cache/fe384d5d6b7a983a.tar.zst +0 -0
- package/ARCHITECTURE.md +10 -10
- package/CHANGELOG.md +38 -0
- package/LICENSE +185 -17
- package/README.md +43 -5
- package/SESSION_HANDOFF.md +67 -0
- package/bin/relay-pty +0 -0
- package/bin/relay-pty-darwin-arm64 +0 -0
- package/bin/relay-pty-darwin-x64 +0 -0
- package/bin/relay-pty-linux-x64 +0 -0
- package/deploy/workspace/entrypoint.sh +79 -11
- package/deploy/workspace/git-credential-relay +152 -27
- package/deploy/workspace/git-credential-relay.test.sh +230 -0
- package/dist/dashboard/out/404.html +1 -1
- package/dist/dashboard/out/_next/static/chunks/116-a883fca163f3a5bc.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/320-900169c942e31422.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/631-af51bad94027527a.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/766-2aea80818f7eb0d8.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/891-5cb1513eeb97a891.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/app/app/page-2e525b1dcc790967.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/app/page-4e64923d73c35bc9.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/app/providers/page-e65a0010da6ea5be.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/app/providers/setup/[provider]/page-84161c802b020a1f.js +1 -0
- package/dist/dashboard/out/_next/static/css/99c2552394077586.css +1 -0
- package/dist/dashboard/out/app/onboarding.html +1 -1
- package/dist/dashboard/out/app/onboarding.txt +1 -1
- package/dist/dashboard/out/app.html +1 -1
- package/dist/dashboard/out/app.txt +2 -2
- package/dist/dashboard/out/cloud/link.html +1 -1
- package/dist/dashboard/out/cloud/link.txt +2 -2
- package/dist/dashboard/out/connect-repos.html +1 -1
- package/dist/dashboard/out/connect-repos.txt +1 -1
- package/dist/dashboard/out/history.html +1 -1
- package/dist/dashboard/out/history.txt +2 -2
- package/dist/dashboard/out/index.html +1 -1
- package/dist/dashboard/out/index.txt +2 -2
- package/dist/dashboard/out/login.html +2 -2
- package/dist/dashboard/out/login.txt +1 -1
- package/dist/dashboard/out/metrics.html +1 -1
- package/dist/dashboard/out/metrics.txt +2 -2
- package/dist/dashboard/out/pricing.html +3 -3
- package/dist/dashboard/out/pricing.txt +2 -2
- package/dist/dashboard/out/providers/setup/claude.html +1 -1
- package/dist/dashboard/out/providers/setup/claude.txt +2 -2
- package/dist/dashboard/out/providers/setup/codex.html +1 -1
- package/dist/dashboard/out/providers/setup/codex.txt +2 -2
- package/dist/dashboard/out/providers/setup/cursor.html +1 -0
- package/dist/dashboard/out/providers/setup/cursor.txt +8 -0
- package/dist/dashboard/out/providers.html +1 -1
- package/dist/dashboard/out/providers.txt +2 -2
- package/dist/dashboard/out/signup.html +2 -2
- package/dist/dashboard/out/signup.txt +1 -1
- package/dist/src/bridge/index.d.ts +8 -0
- package/dist/src/bridge/index.js +8 -0
- package/dist/src/cli/index.js +3089 -0
- package/dist/src/cloud/index.d.ts +8 -0
- package/dist/src/cloud/index.js +8 -0
- package/dist/src/config/relay-config.d.ts +5 -0
- package/dist/src/config/relay-config.js +5 -0
- package/dist/src/continuity/index.d.ts +5 -0
- package/dist/src/continuity/index.js +5 -0
- package/dist/src/daemon/index.d.ts +8 -0
- package/dist/src/daemon/index.js +9 -0
- package/dist/src/dashboard-server/index.d.ts +8 -0
- package/dist/src/dashboard-server/index.js +8 -0
- package/dist/src/hooks/index.d.ts +10 -0
- package/dist/src/hooks/index.js +10 -0
- package/dist/src/index.d.ts +13 -0
- package/dist/src/index.js +16 -0
- package/dist/src/memory/index.d.ts +5 -0
- package/dist/src/memory/index.js +5 -0
- package/dist/src/policy/index.d.ts +5 -0
- package/dist/src/policy/index.js +5 -0
- package/dist/src/protocol/index.d.ts +8 -0
- package/dist/src/protocol/index.js +8 -0
- package/dist/src/resiliency/index.d.ts +5 -0
- package/dist/src/resiliency/index.js +5 -0
- package/dist/src/shared/cli-auth-config.d.ts +5 -0
- package/dist/src/shared/cli-auth-config.js +5 -0
- package/dist/src/state/index.d.ts +5 -0
- package/dist/src/state/index.js +5 -0
- package/dist/src/storage/index.d.ts +8 -0
- package/dist/src/storage/index.js +8 -0
- package/dist/src/trajectory/index.d.ts +5 -0
- package/dist/src/trajectory/index.js +5 -0
- package/dist/src/utils/index.d.ts +5 -0
- package/dist/src/utils/index.js +5 -0
- package/dist/src/wrapper/index.d.ts +8 -0
- package/dist/src/wrapper/index.js +11 -0
- package/package.json +64 -19
- package/packages/api-types/dist/index.d.ts +21 -0
- package/packages/api-types/dist/index.js +22 -0
- package/packages/api-types/dist/schemas/agent.d.ts +259 -0
- package/packages/api-types/dist/schemas/agent.js +102 -0
- package/packages/api-types/dist/schemas/api.d.ts +290 -0
- package/packages/api-types/dist/schemas/api.js +162 -0
- package/packages/api-types/dist/schemas/decision.d.ts +230 -0
- package/packages/api-types/dist/schemas/decision.js +104 -0
- package/packages/api-types/dist/schemas/fleet.d.ts +615 -0
- package/packages/api-types/dist/schemas/fleet.js +71 -0
- package/packages/api-types/dist/schemas/history.d.ts +180 -0
- package/packages/api-types/dist/schemas/history.js +72 -0
- package/packages/api-types/dist/schemas/index.d.ts +14 -0
- package/packages/api-types/dist/schemas/index.js +22 -0
- package/packages/api-types/dist/schemas/message.d.ts +456 -0
- package/packages/api-types/dist/schemas/message.js +88 -0
- package/packages/api-types/dist/schemas/session.d.ts +60 -0
- package/packages/api-types/dist/schemas/session.js +36 -0
- package/packages/api-types/dist/schemas/task.d.ts +111 -0
- package/packages/api-types/dist/schemas/task.js +64 -0
- package/packages/api-types/package.json +61 -0
- package/packages/api-types/scripts/generate-openapi.ts +106 -0
- package/packages/bridge/dist/index.d.ts +8 -0
- package/packages/bridge/dist/index.js +9 -0
- package/packages/bridge/dist/multi-project-client.d.ts +99 -0
- package/packages/bridge/dist/multi-project-client.js +389 -0
- package/packages/bridge/dist/shadow-cli.js +75 -0
- package/packages/bridge/dist/spawner.d.ts +210 -0
- package/packages/bridge/dist/spawner.js +1276 -0
- package/packages/bridge/dist/types.d.ts +131 -0
- package/packages/bridge/dist/utils.d.ts +15 -0
- package/packages/bridge/dist/utils.js +60 -0
- package/packages/bridge/package.json +40 -0
- package/packages/cloud/dist/api/admin.js +225 -0
- package/packages/cloud/dist/api/billing.js +564 -0
- package/packages/cloud/dist/api/cli-pty-runner.d.ts +53 -0
- package/packages/cloud/dist/api/cli-pty-runner.js +193 -0
- package/packages/cloud/dist/api/codex-auth-helper.js +327 -0
- package/packages/cloud/dist/api/consensus.js +261 -0
- package/packages/cloud/dist/api/coordinators.js +750 -0
- package/packages/cloud/dist/api/daemons.js +535 -0
- package/packages/cloud/dist/api/generic-webhooks.js +129 -0
- package/packages/cloud/dist/api/github-app.js +223 -0
- package/packages/cloud/dist/api/monitoring.js +578 -0
- package/packages/cloud/dist/api/nango-auth.js +674 -0
- package/packages/cloud/dist/api/onboarding.d.ts +15 -0
- package/packages/cloud/dist/api/onboarding.js +679 -0
- package/packages/cloud/dist/api/policy.js +229 -0
- package/packages/cloud/dist/api/provider-env.d.ts +14 -0
- package/packages/cloud/dist/api/provider-env.js +75 -0
- package/packages/cloud/dist/api/providers.js +564 -0
- package/packages/cloud/dist/api/repos.js +577 -0
- package/packages/cloud/dist/api/sessions.d.ts +11 -0
- package/packages/cloud/dist/api/sessions.js +302 -0
- package/packages/cloud/dist/api/teams.js +281 -0
- package/packages/cloud/dist/api/test-helpers.js +745 -0
- package/packages/cloud/dist/api/workspaces.js +1799 -0
- package/packages/cloud/dist/billing/plans.js +245 -0
- package/packages/cloud/dist/config.d.ts +5 -0
- package/packages/cloud/dist/config.js +5 -0
- package/packages/cloud/dist/db/drizzle.d.ts +256 -0
- package/packages/cloud/dist/db/drizzle.js +1286 -0
- package/packages/cloud/dist/db/schema.d.ts +4873 -0
- package/packages/cloud/dist/db/schema.js +620 -0
- package/packages/cloud/dist/index.d.ts +11 -0
- package/packages/cloud/dist/index.js +38 -0
- package/packages/cloud/dist/provisioner/index.d.ts +207 -0
- package/packages/cloud/dist/provisioner/index.js +2114 -0
- package/packages/cloud/dist/server.js +1924 -0
- package/packages/cloud/dist/services/index.d.ts +17 -0
- package/packages/cloud/dist/services/index.js +25 -0
- package/packages/cloud/dist/services/intro-expiration.d.ts +60 -0
- package/packages/cloud/dist/services/intro-expiration.js +252 -0
- package/packages/cloud/dist/services/nango.d.ts +201 -0
- package/packages/cloud/dist/services/nango.js +392 -0
- package/packages/cloud/dist/services/persistence.d.ts +131 -0
- package/packages/cloud/dist/shims/consensus.d.ts +23 -0
- package/packages/cloud/dist/shims/consensus.js +5 -0
- package/packages/cloud/package.json +55 -0
- package/packages/config/dist/bridge-config.d.ts +52 -0
- package/packages/config/dist/bridge-config.js +143 -0
- package/packages/config/dist/bridge-utils.d.ts +30 -0
- package/packages/config/dist/bridge-utils.js +54 -0
- package/packages/config/dist/cli-auth-config.js +391 -0
- package/packages/config/dist/cloud-config.d.ts +75 -0
- package/packages/config/dist/cloud-config.js +109 -0
- package/packages/config/dist/index.d.ts +13 -0
- package/packages/config/dist/index.js +13 -0
- package/packages/config/dist/project-namespace.d.ts +73 -0
- package/packages/config/dist/project-namespace.js +280 -0
- package/packages/config/dist/relay-config.d.ts +25 -0
- package/packages/config/dist/relay-config.js +25 -0
- package/packages/config/dist/relay-file-writer.d.ts +200 -0
- package/packages/config/dist/relay-file-writer.js +407 -0
- package/packages/config/dist/schemas.d.ts +672 -0
- package/packages/config/dist/schemas.js +180 -0
- package/packages/config/dist/shadow-config.d.ts +87 -0
- package/packages/config/dist/trajectory-config.d.ts +102 -0
- package/packages/config/dist/trajectory-config.js +185 -0
- package/packages/config/package.json +98 -0
- package/packages/continuity/dist/index.d.ts +9 -0
- package/packages/continuity/dist/index.js +9 -0
- package/packages/continuity/dist/types.d.ts +180 -0
- package/packages/continuity/dist/types.js +2 -0
- package/packages/continuity/package.json +32 -0
- package/packages/daemon/dist/agent-manager.d.ts +134 -0
- package/packages/daemon/dist/agent-manager.js +578 -0
- package/packages/daemon/dist/agent-registry.js +213 -0
- package/packages/daemon/dist/api.d.ts +106 -0
- package/packages/daemon/dist/api.js +876 -0
- package/packages/daemon/dist/channel-membership-store.d.ts +55 -0
- package/packages/daemon/dist/channel-membership-store.js +176 -0
- package/packages/daemon/dist/cli-auth.d.ts +89 -0
- package/packages/daemon/dist/cli-auth.js +792 -0
- package/packages/daemon/dist/cloud-sync.d.ts +150 -0
- package/packages/daemon/dist/cloud-sync.js +446 -0
- package/packages/daemon/dist/connection.d.ts +130 -0
- package/packages/daemon/dist/connection.js +438 -0
- package/packages/daemon/dist/consensus-integration.js +371 -0
- package/packages/daemon/dist/delivery-tracker.d.ts +34 -0
- package/packages/daemon/dist/delivery-tracker.js +104 -0
- package/packages/daemon/dist/enhanced-features.d.ts +118 -0
- package/packages/daemon/dist/enhanced-features.js +176 -0
- package/packages/daemon/dist/index.d.ts +31 -0
- package/packages/daemon/dist/index.js +37 -0
- package/packages/daemon/dist/migrations/index.d.ts +73 -0
- package/packages/daemon/dist/migrations/index.js +241 -0
- package/packages/daemon/dist/orchestrator.d.ts +217 -0
- package/packages/daemon/dist/orchestrator.js +1143 -0
- package/packages/daemon/dist/relay-ledger.d.ts +261 -0
- package/packages/daemon/dist/relay-ledger.js +532 -0
- package/packages/daemon/dist/relay-watchdog.d.ts +125 -0
- package/packages/daemon/dist/relay-watchdog.js +611 -0
- package/packages/daemon/dist/repo-manager.js +384 -0
- package/packages/daemon/dist/router.d.ts +370 -0
- package/packages/daemon/dist/router.js +1437 -0
- package/packages/daemon/dist/server.d.ts +174 -0
- package/packages/daemon/dist/server.js +1001 -0
- package/packages/daemon/dist/spawn-manager.d.ts +78 -0
- package/packages/daemon/dist/spawn-manager.js +165 -0
- package/packages/daemon/dist/sync-queue.d.ts +116 -0
- package/packages/daemon/dist/sync-queue.js +361 -0
- package/packages/daemon/dist/types.d.ts +133 -0
- package/packages/daemon/dist/workspace-manager.js +314 -0
- package/packages/daemon/package.json +52 -0
- package/packages/dashboard/README.md +48 -0
- package/packages/dashboard/dist/health-worker-manager.d.ts +62 -0
- package/packages/dashboard/dist/health-worker-manager.js +144 -0
- package/packages/dashboard/dist/health-worker.d.ts +9 -0
- package/packages/dashboard/dist/health-worker.js +79 -0
- package/packages/dashboard/dist/index.d.ts +20 -0
- package/packages/dashboard/dist/index.js +19 -0
- package/packages/dashboard/dist/metrics.d.ts +105 -0
- package/packages/dashboard/dist/metrics.js +193 -0
- package/packages/dashboard/dist/needs-attention.d.ts +24 -0
- package/packages/dashboard/dist/needs-attention.js +78 -0
- package/packages/dashboard/dist/server.d.ts +25 -0
- package/packages/dashboard/dist/server.js +5107 -0
- package/packages/dashboard/dist/start.d.ts +6 -0
- package/packages/dashboard/dist/start.js +13 -0
- package/packages/dashboard/dist/types/threading.d.ts +8 -0
- package/packages/dashboard/dist/types/threading.js +2 -0
- package/packages/dashboard/dist/user-bridge.d.ts +154 -0
- package/packages/dashboard/dist/user-bridge.js +372 -0
- package/packages/dashboard/package.json +72 -0
- package/packages/dashboard/ui/.next/BUILD_ID +1 -0
- package/packages/dashboard/ui/.next/app-build-manifest.json +135 -0
- package/packages/dashboard/ui/.next/app-path-routes-manifest.json +1 -0
- package/packages/dashboard/ui/.next/build-manifest.json +32 -0
- package/packages/dashboard/ui/.next/cache/config.json +7 -0
- package/packages/dashboard/ui/.next/cache/eslint/.cache_1asv1h5 +1 -0
- package/packages/dashboard/ui/.next/cache/webpack/client-production/0.pack +0 -0
- package/packages/dashboard/ui/.next/cache/webpack/client-production/index.pack +0 -0
- package/packages/dashboard/ui/.next/cache/webpack/edge-server-production/0.pack +0 -0
- package/packages/dashboard/ui/.next/cache/webpack/edge-server-production/index.pack +0 -0
- package/packages/dashboard/ui/.next/cache/webpack/server-production/0.pack +0 -0
- package/packages/dashboard/ui/.next/cache/webpack/server-production/index.pack +0 -0
- package/packages/dashboard/ui/.next/export-detail.json +1 -0
- package/packages/dashboard/ui/.next/export-marker.json +1 -0
- package/packages/dashboard/ui/.next/images-manifest.json +1 -0
- package/packages/dashboard/ui/.next/next-minimal-server.js.nft.json +1 -0
- package/packages/dashboard/ui/.next/next-server.js.nft.json +1 -0
- package/packages/dashboard/ui/.next/package.json +1 -0
- package/packages/dashboard/ui/.next/prerender-manifest.json +1 -0
- package/packages/dashboard/ui/.next/react-loadable-manifest.json +1970 -0
- package/packages/dashboard/ui/.next/required-server-files.json +1 -0
- package/packages/dashboard/ui/.next/routes-manifest.json +1 -0
- package/packages/dashboard/ui/.next/server/app/_not-found/page.js +1 -0
- package/packages/dashboard/ui/.next/server/app/_not-found/page.js.nft.json +1 -0
- package/packages/dashboard/ui/.next/server/app/_not-found/page_client-reference-manifest.js +1 -0
- package/packages/dashboard/ui/.next/server/app/_not-found.html +1 -0
- package/packages/dashboard/ui/.next/server/app/_not-found.meta +6 -0
- package/packages/dashboard/ui/.next/server/app/_not-found.rsc +9 -0
- package/packages/dashboard/ui/.next/server/app/app/onboarding/page.js +6 -0
- package/packages/dashboard/ui/.next/server/app/app/onboarding/page.js.nft.json +1 -0
- package/packages/dashboard/ui/.next/server/app/app/onboarding/page_client-reference-manifest.js +1 -0
- package/packages/dashboard/ui/.next/server/app/app/onboarding.html +1 -0
- package/packages/dashboard/ui/.next/server/app/app/onboarding.meta +5 -0
- package/packages/dashboard/ui/.next/server/app/app/onboarding.rsc +7 -0
- package/packages/dashboard/ui/.next/server/app/app/page.js +436 -0
- package/packages/dashboard/ui/.next/server/app/app/page.js.nft.json +1 -0
- package/packages/dashboard/ui/.next/server/app/app/page_client-reference-manifest.js +1 -0
- package/packages/dashboard/ui/.next/server/app/app.html +1 -0
- package/packages/dashboard/ui/.next/server/app/app.meta +5 -0
- package/packages/dashboard/ui/.next/server/app/app.rsc +7 -0
- package/packages/dashboard/ui/.next/server/app/apple-icon.png/route.js +12 -0
- package/packages/dashboard/ui/.next/server/app/apple-icon.png/route.js.nft.json +1 -0
- package/packages/dashboard/ui/.next/server/app/apple-icon.png.body +0 -0
- package/packages/dashboard/ui/.next/server/app/apple-icon.png.meta +1 -0
- package/packages/dashboard/ui/.next/server/app/cloud/link/page.js +1 -0
- package/packages/dashboard/ui/.next/server/app/cloud/link/page.js.nft.json +1 -0
- package/packages/dashboard/ui/.next/server/app/cloud/link/page_client-reference-manifest.js +1 -0
- package/packages/dashboard/ui/.next/server/app/cloud/link.html +1 -0
- package/packages/dashboard/ui/.next/server/app/cloud/link.meta +5 -0
- package/packages/dashboard/ui/.next/server/app/cloud/link.rsc +7 -0
- package/packages/dashboard/ui/.next/server/app/connect-repos/page.js +6 -0
- package/packages/dashboard/ui/.next/server/app/connect-repos/page.js.nft.json +1 -0
- package/packages/dashboard/ui/.next/server/app/connect-repos/page_client-reference-manifest.js +1 -0
- package/packages/dashboard/ui/.next/server/app/connect-repos.html +1 -0
- package/packages/dashboard/ui/.next/server/app/connect-repos.meta +5 -0
- package/packages/dashboard/ui/.next/server/app/connect-repos.rsc +7 -0
- package/packages/dashboard/ui/.next/server/app/history/page.js +1 -0
- package/packages/dashboard/ui/.next/server/app/history/page.js.nft.json +1 -0
- package/packages/dashboard/ui/.next/server/app/history/page_client-reference-manifest.js +1 -0
- package/packages/dashboard/ui/.next/server/app/history.html +1 -0
- package/packages/dashboard/ui/.next/server/app/history.meta +5 -0
- package/packages/dashboard/ui/.next/server/app/history.rsc +7 -0
- package/packages/dashboard/ui/.next/server/app/index.html +1 -0
- package/packages/dashboard/ui/.next/server/app/index.meta +5 -0
- package/packages/dashboard/ui/.next/server/app/index.rsc +7 -0
- package/packages/dashboard/ui/.next/server/app/login/page.js +6 -0
- package/packages/dashboard/ui/.next/server/app/login/page.js.nft.json +1 -0
- package/packages/dashboard/ui/.next/server/app/login/page_client-reference-manifest.js +1 -0
- package/packages/dashboard/ui/.next/server/app/login.html +5 -0
- package/packages/dashboard/ui/.next/server/app/login.meta +5 -0
- package/packages/dashboard/ui/.next/server/app/login.rsc +7 -0
- package/packages/dashboard/ui/.next/server/app/metrics/page.js +1 -0
- package/packages/dashboard/ui/.next/server/app/metrics/page.js.nft.json +1 -0
- package/packages/dashboard/ui/.next/server/app/metrics/page_client-reference-manifest.js +1 -0
- package/packages/dashboard/ui/.next/server/app/metrics.html +1 -0
- package/packages/dashboard/ui/.next/server/app/metrics.meta +5 -0
- package/packages/dashboard/ui/.next/server/app/metrics.rsc +7 -0
- package/packages/dashboard/ui/.next/server/app/page.js +1 -0
- package/packages/dashboard/ui/.next/server/app/page.js.nft.json +1 -0
- package/packages/dashboard/ui/.next/server/app/page_client-reference-manifest.js +1 -0
- package/packages/dashboard/ui/.next/server/app/pricing/page.js +5 -0
- package/packages/dashboard/ui/.next/server/app/pricing/page.js.nft.json +1 -0
- package/packages/dashboard/ui/.next/server/app/pricing/page_client-reference-manifest.js +1 -0
- package/packages/dashboard/ui/.next/server/app/pricing.html +13 -0
- package/packages/dashboard/ui/.next/server/app/pricing.meta +5 -0
- package/packages/dashboard/ui/.next/server/app/pricing.rsc +7 -0
- package/packages/dashboard/ui/.next/server/app/providers/page.js +2 -0
- package/packages/dashboard/ui/.next/server/app/providers/page.js.nft.json +1 -0
- package/packages/dashboard/ui/.next/server/app/providers/page_client-reference-manifest.js +1 -0
- package/packages/dashboard/ui/.next/server/app/providers/setup/[provider]/page.js +1 -0
- package/packages/dashboard/ui/.next/server/app/providers/setup/[provider]/page.js.nft.json +1 -0
- package/packages/dashboard/ui/.next/server/app/providers/setup/[provider]/page_client-reference-manifest.js +1 -0
- package/packages/dashboard/ui/.next/server/app/providers/setup/claude.html +1 -0
- package/packages/dashboard/ui/.next/server/app/providers/setup/claude.meta +5 -0
- package/packages/dashboard/ui/.next/server/app/providers/setup/claude.rsc +8 -0
- package/packages/dashboard/ui/.next/server/app/providers/setup/codex.html +1 -0
- package/packages/dashboard/ui/.next/server/app/providers/setup/codex.meta +5 -0
- package/packages/dashboard/ui/.next/server/app/providers/setup/codex.rsc +8 -0
- package/packages/dashboard/ui/.next/server/app/providers/setup/cursor.html +1 -0
- package/packages/dashboard/ui/.next/server/app/providers/setup/cursor.meta +5 -0
- package/packages/dashboard/ui/.next/server/app/providers/setup/cursor.rsc +8 -0
- package/packages/dashboard/ui/.next/server/app/providers.html +1 -0
- package/packages/dashboard/ui/.next/server/app/providers.meta +5 -0
- package/packages/dashboard/ui/.next/server/app/providers.rsc +7 -0
- package/packages/dashboard/ui/.next/server/app/signup/page.js +6 -0
- package/packages/dashboard/ui/.next/server/app/signup/page.js.nft.json +1 -0
- package/packages/dashboard/ui/.next/server/app/signup/page_client-reference-manifest.js +1 -0
- package/packages/dashboard/ui/.next/server/app/signup.html +6 -0
- package/packages/dashboard/ui/.next/server/app/signup.meta +5 -0
- package/packages/dashboard/ui/.next/server/app/signup.rsc +7 -0
- package/packages/dashboard/ui/.next/server/app-paths-manifest.json +16 -0
- package/packages/dashboard/ui/.next/server/chunks/190.js +1 -0
- package/packages/dashboard/ui/.next/server/chunks/205.js +1 -0
- package/packages/dashboard/ui/.next/server/chunks/251.js +9 -0
- package/packages/dashboard/ui/.next/server/chunks/288.js +1 -0
- package/packages/dashboard/ui/.next/server/chunks/434.js +1 -0
- package/packages/dashboard/ui/.next/server/chunks/471.js +2 -0
- package/packages/dashboard/ui/.next/server/chunks/621.js +1 -0
- package/packages/dashboard/ui/.next/server/chunks/680.js +5 -0
- package/packages/dashboard/ui/.next/server/chunks/682.js +6 -0
- package/packages/dashboard/ui/.next/server/chunks/684.js +1 -0
- package/packages/dashboard/ui/.next/server/chunks/71.js +196 -0
- package/packages/dashboard/ui/.next/server/chunks/711.js +1 -0
- package/packages/dashboard/ui/.next/server/chunks/90.js +17 -0
- package/packages/dashboard/ui/.next/server/chunks/948.js +2 -0
- package/packages/dashboard/ui/.next/server/chunks/font-manifest.json +1 -0
- package/packages/dashboard/ui/.next/server/font-manifest.json +1 -0
- package/packages/dashboard/ui/.next/server/functions-config-manifest.json +1 -0
- package/packages/dashboard/ui/.next/server/interception-route-rewrite-manifest.js +1 -0
- package/packages/dashboard/ui/.next/server/middleware-build-manifest.js +1 -0
- package/packages/dashboard/ui/.next/server/middleware-manifest.json +6 -0
- package/packages/dashboard/ui/.next/server/middleware-react-loadable-manifest.js +1 -0
- package/packages/dashboard/ui/.next/server/next-font-manifest.js +1 -0
- package/packages/dashboard/ui/.next/server/next-font-manifest.json +1 -0
- package/packages/dashboard/ui/.next/server/pages/404.html +1 -0
- package/packages/dashboard/ui/.next/server/pages/500.html +1 -0
- package/packages/dashboard/ui/.next/server/pages/_app.js +1 -0
- package/packages/dashboard/ui/.next/server/pages/_app.js.nft.json +1 -0
- package/packages/dashboard/ui/.next/server/pages/_document.js +1 -0
- package/packages/dashboard/ui/.next/server/pages/_document.js.nft.json +1 -0
- package/packages/dashboard/ui/.next/server/pages/_error.js +1 -0
- package/packages/dashboard/ui/.next/server/pages/_error.js.nft.json +1 -0
- package/packages/dashboard/ui/.next/server/pages-manifest.json +1 -0
- package/packages/dashboard/ui/.next/server/server-reference-manifest.js +1 -0
- package/packages/dashboard/ui/.next/server/server-reference-manifest.json +1 -0
- package/packages/dashboard/ui/.next/server/webpack-runtime.js +1 -0
- package/packages/dashboard/ui/.next/static/HR7W9z1PPVPFqUboUVZFZ/_buildManifest.js +1 -0
- package/packages/dashboard/ui/.next/static/HR7W9z1PPVPFqUboUVZFZ/_ssgManifest.js +1 -0
- package/packages/dashboard/ui/.next/static/chunks/116-a883fca163f3a5bc.js +1 -0
- package/packages/dashboard/ui/.next/static/chunks/117-c8afed19e821a35d.js +2 -0
- package/packages/dashboard/ui/.next/static/chunks/282-980c2eb8fff20123.js +1 -0
- package/packages/dashboard/ui/.next/static/chunks/532-bace199897eeab37.js +9 -0
- package/packages/dashboard/ui/.next/static/chunks/631-af51bad94027527a.js +1 -0
- package/packages/dashboard/ui/.next/static/chunks/648-acb2ff9f77cbfbd3.js +1 -0
- package/packages/dashboard/ui/.next/static/chunks/677-30e60cb0b47875b6.js +1 -0
- package/packages/dashboard/ui/.next/static/chunks/766-2aea80818f7eb0d8.js +1 -0
- package/packages/dashboard/ui/.next/static/chunks/83-4f08122d4e7e79a6.js +1 -0
- package/packages/dashboard/ui/.next/static/chunks/847-f1f467060f32afff.js +1 -0
- package/packages/dashboard/ui/.next/static/chunks/891-5cb1513eeb97a891.js +1 -0
- package/packages/dashboard/ui/.next/static/chunks/app/_not-found/page-60501fddbafba9dc.js +1 -0
- package/packages/dashboard/ui/.next/static/chunks/app/app/onboarding/page-9914652442f7e4fb.js +1 -0
- package/packages/dashboard/ui/.next/static/chunks/app/app/page-44813aa26ad19681.js +1 -0
- package/packages/dashboard/ui/.next/static/chunks/app/cloud/link/page-fa1d5842aa90e8a6.js +1 -0
- package/packages/dashboard/ui/.next/static/chunks/app/connect-repos/page-113060009ef35bc2.js +1 -0
- package/packages/dashboard/ui/.next/static/chunks/app/history/page-9965d2483011b846.js +1 -0
- package/packages/dashboard/ui/.next/static/chunks/app/layout-6b91e33784c20610.js +1 -0
- package/packages/dashboard/ui/.next/static/chunks/app/login/page-a0ca6f7ca6a100b8.js +1 -0
- package/packages/dashboard/ui/.next/static/chunks/app/metrics/page-1e37ef8e73940b40.js +1 -0
- package/packages/dashboard/ui/.next/static/chunks/app/page-7993778218818ace.js +1 -0
- package/packages/dashboard/ui/.next/static/chunks/app/pricing/page-9db3ebdfa567a7c9.js +1 -0
- package/packages/dashboard/ui/.next/static/chunks/app/providers/page-bcf46064ac4474ce.js +1 -0
- package/packages/dashboard/ui/.next/static/chunks/app/providers/setup/[provider]/page-4dbe33f0f7691b7c.js +1 -0
- package/packages/dashboard/ui/.next/static/chunks/app/signup/page-1ede2205b58649ca.js +1 -0
- package/packages/dashboard/ui/.next/static/chunks/e868780c-48e5f147c90a3a41.js +18 -0
- package/packages/dashboard/ui/.next/static/chunks/fd9d1056-609918ca7b6280bb.js +1 -0
- package/packages/dashboard/ui/.next/static/chunks/framework-f66176bb897dc684.js +1 -0
- package/packages/dashboard/ui/.next/static/chunks/main-5a40a5ae29646e1b.js +1 -0
- package/packages/dashboard/ui/.next/static/chunks/main-app-fdbeb09028f57c9f.js +1 -0
- package/packages/dashboard/ui/.next/static/chunks/pages/_app-72b849fbd24ac258.js +1 -0
- package/packages/dashboard/ui/.next/static/chunks/pages/_error-7ba65e1336b92748.js +1 -0
- package/packages/dashboard/ui/.next/static/chunks/polyfills-42372ed130431b0a.js +1 -0
- package/packages/dashboard/ui/.next/static/chunks/webpack-1cdd8ed57114d5e1.js +1 -0
- package/packages/dashboard/ui/.next/static/css/4034f236dd1a3178.css +1 -0
- package/packages/dashboard/ui/.next/static/css/99c2552394077586.css +1 -0
- package/packages/dashboard/ui/.next/trace +63 -0
- package/packages/dashboard/ui/.next/types/app/app/onboarding/page.ts +79 -0
- package/packages/dashboard/ui/.next/types/app/app/page.ts +79 -0
- package/packages/dashboard/ui/.next/types/app/cloud/link/page.ts +79 -0
- package/packages/dashboard/ui/.next/types/app/connect-repos/page.ts +79 -0
- package/packages/dashboard/ui/.next/types/app/history/page.ts +79 -0
- package/packages/dashboard/ui/.next/types/app/layout.ts +79 -0
- package/packages/dashboard/ui/.next/types/app/login/page.ts +79 -0
- package/packages/dashboard/ui/.next/types/app/metrics/page.ts +79 -0
- package/packages/dashboard/ui/.next/types/app/page.ts +79 -0
- package/packages/dashboard/ui/.next/types/app/pricing/page.ts +79 -0
- package/packages/dashboard/ui/.next/types/app/providers/page.ts +79 -0
- package/packages/dashboard/ui/.next/types/app/providers/setup/[provider]/page.ts +79 -0
- package/packages/dashboard/ui/.next/types/app/signup/page.ts +79 -0
- package/packages/dashboard/ui/.next/types/package.json +1 -0
- package/packages/dashboard/ui/app/app/onboarding/page.tsx +394 -0
- package/packages/dashboard/ui/app/app/page.tsx +667 -0
- package/packages/dashboard/ui/app/apple-icon.png +0 -0
- package/packages/dashboard/ui/app/cloud/link/page.tsx +464 -0
- package/packages/dashboard/ui/app/connect-repos/page.tsx +410 -0
- package/packages/dashboard/ui/app/favicon.png +0 -0
- package/packages/dashboard/ui/app/globals.css +59 -0
- package/packages/dashboard/ui/app/history/page.tsx +658 -0
- package/packages/dashboard/ui/app/layout.tsx +25 -0
- package/packages/dashboard/ui/app/login/page.tsx +280 -0
- package/packages/dashboard/ui/app/metrics/page.tsx +751 -0
- package/packages/dashboard/ui/app/page.tsx +59 -0
- package/packages/dashboard/ui/app/pricing/page.tsx +7 -0
- package/packages/dashboard/ui/app/providers/page.tsx +193 -0
- package/packages/dashboard/ui/app/providers/setup/[provider]/ProviderSetupClient.tsx +148 -0
- package/packages/dashboard/ui/app/providers/setup/[provider]/constants.ts +35 -0
- package/packages/dashboard/ui/app/providers/setup/[provider]/page.tsx +42 -0
- package/packages/dashboard/ui/app/signup/page.tsx +343 -0
- package/packages/dashboard/ui/index.ts +49 -0
- package/packages/dashboard/ui/landing/LandingPage.tsx +713 -0
- package/packages/dashboard/ui/landing/PricingPage.tsx +559 -0
- package/packages/dashboard/ui/landing/index.ts +6 -0
- package/packages/dashboard/ui/landing/styles.css +2850 -0
- package/packages/dashboard/ui/lib/agent-merge.ts +35 -0
- package/packages/dashboard/ui/lib/api.ts +1155 -0
- package/packages/dashboard/ui/lib/cloudApi.ts +876 -0
- package/packages/dashboard/ui/lib/colors.ts +218 -0
- package/packages/dashboard/ui/lib/hierarchy.ts +242 -0
- package/packages/dashboard/ui/lib/stuckDetection.ts +142 -0
- package/packages/dashboard/ui/next-env.d.ts +5 -0
- package/packages/dashboard/ui/next.config.js +41 -0
- package/packages/dashboard/ui/package-lock.json +2882 -0
- package/packages/dashboard/ui/package.json +33 -0
- package/packages/dashboard/ui/postcss.config.js +5 -0
- package/packages/dashboard/ui/react-components/ActivityFeed.tsx +216 -0
- package/packages/dashboard/ui/react-components/AddWorkspaceModal.tsx +170 -0
- package/packages/dashboard/ui/react-components/AgentCard.tsx +587 -0
- package/packages/dashboard/ui/react-components/AgentList.tsx +411 -0
- package/packages/dashboard/ui/react-components/AgentProfilePanel.tsx +564 -0
- package/packages/dashboard/ui/react-components/App.tsx +3447 -0
- package/packages/dashboard/ui/react-components/BillingPanel.tsx +922 -0
- package/packages/dashboard/ui/react-components/BillingResult.tsx +447 -0
- package/packages/dashboard/ui/react-components/BroadcastComposer.tsx +690 -0
- package/packages/dashboard/ui/react-components/ChannelAdminPanel.tsx +773 -0
- package/packages/dashboard/ui/react-components/ChannelBrowser.tsx +385 -0
- package/packages/dashboard/ui/react-components/ChannelChat.tsx +307 -0
- package/packages/dashboard/ui/react-components/ChannelSidebar.tsx +399 -0
- package/packages/dashboard/ui/react-components/CloudSessionProvider.tsx +130 -0
- package/packages/dashboard/ui/react-components/CommandPalette.tsx +815 -0
- package/packages/dashboard/ui/react-components/ConfirmationDialog.tsx +133 -0
- package/packages/dashboard/ui/react-components/ConversationHistory.tsx +518 -0
- package/packages/dashboard/ui/react-components/CoordinatorPanel.tsx +944 -0
- package/packages/dashboard/ui/react-components/DecisionQueue.tsx +717 -0
- package/packages/dashboard/ui/react-components/DirectMessageView.tsx +164 -0
- package/packages/dashboard/ui/react-components/FileAutocomplete.tsx +368 -0
- package/packages/dashboard/ui/react-components/FleetOverview.tsx +278 -0
- package/packages/dashboard/ui/react-components/LogViewer.tsx +310 -0
- package/packages/dashboard/ui/react-components/LogViewerPanel.tsx +482 -0
- package/packages/dashboard/ui/react-components/Logo.tsx +284 -0
- package/packages/dashboard/ui/react-components/MentionAutocomplete.tsx +384 -0
- package/packages/dashboard/ui/react-components/MessageList.tsx +649 -0
- package/packages/dashboard/ui/react-components/MessageSenderName.tsx +91 -0
- package/packages/dashboard/ui/react-components/MessageStatusIndicator.tsx +142 -0
- package/packages/dashboard/ui/react-components/NewConversationModal.tsx +400 -0
- package/packages/dashboard/ui/react-components/NotificationToast.tsx +488 -0
- package/packages/dashboard/ui/react-components/OnlineUsersIndicator.tsx +164 -0
- package/packages/dashboard/ui/react-components/Pagination.tsx +124 -0
- package/packages/dashboard/ui/react-components/PricingPlans.tsx +386 -0
- package/packages/dashboard/ui/react-components/ProjectList.tsx +625 -0
- package/packages/dashboard/ui/react-components/ProviderAuthFlow.tsx +843 -0
- package/packages/dashboard/ui/react-components/ProviderConnectionList.tsx +363 -0
- package/packages/dashboard/ui/react-components/ProvisioningProgress.tsx +730 -0
- package/packages/dashboard/ui/react-components/RepoAccessPanel.tsx +392 -0
- package/packages/dashboard/ui/react-components/ServerCard.tsx +202 -0
- package/packages/dashboard/ui/react-components/SessionExpiredModal.tsx +128 -0
- package/packages/dashboard/ui/react-components/SpawnModal.tsx +704 -0
- package/packages/dashboard/ui/react-components/TaskAssignmentUI.tsx +375 -0
- package/packages/dashboard/ui/react-components/TerminalProviderSetup.tsx +608 -0
- package/packages/dashboard/ui/react-components/ThemeProvider.tsx +325 -0
- package/packages/dashboard/ui/react-components/ThinkingIndicator.tsx +231 -0
- package/packages/dashboard/ui/react-components/ThreadList.tsx +198 -0
- package/packages/dashboard/ui/react-components/ThreadPanel.tsx +346 -0
- package/packages/dashboard/ui/react-components/TrajectoryViewer.tsx +698 -0
- package/packages/dashboard/ui/react-components/TypingIndicator.tsx +69 -0
- package/packages/dashboard/ui/react-components/UsageBanner.tsx +231 -0
- package/packages/dashboard/ui/react-components/UserProfilePanel.tsx +233 -0
- package/packages/dashboard/ui/react-components/WorkspaceContext.tsx +107 -0
- package/packages/dashboard/ui/react-components/WorkspaceSelector.tsx +234 -0
- package/packages/dashboard/ui/react-components/WorkspaceStatusIndicator.tsx +370 -0
- package/packages/dashboard/ui/react-components/XTermInteractive.tsx +510 -0
- package/packages/dashboard/ui/react-components/XTermLogViewer.tsx +719 -0
- package/packages/dashboard/ui/react-components/channels/ChannelDialogs.tsx +1411 -0
- package/packages/dashboard/ui/react-components/channels/ChannelHeader.tsx +317 -0
- package/packages/dashboard/ui/react-components/channels/ChannelMessageList.tsx +463 -0
- package/packages/dashboard/ui/react-components/channels/ChannelViewV1.tsx +146 -0
- package/packages/dashboard/ui/react-components/channels/MessageInput.tsx +288 -0
- package/packages/dashboard/ui/react-components/channels/SearchInput.tsx +172 -0
- package/packages/dashboard/ui/react-components/channels/SearchResults.tsx +336 -0
- package/packages/dashboard/ui/react-components/channels/api.ts +697 -0
- package/packages/dashboard/ui/react-components/channels/index.ts +76 -0
- package/packages/dashboard/ui/react-components/channels/mockApi.ts +344 -0
- package/packages/dashboard/ui/react-components/channels/types.ts +566 -0
- package/packages/dashboard/ui/react-components/hooks/index.ts +57 -0
- package/packages/dashboard/ui/react-components/hooks/useAgentLogs.ts +394 -0
- package/packages/dashboard/ui/react-components/hooks/useAgents.ts +127 -0
- package/packages/dashboard/ui/react-components/hooks/useBroadcastDedup.ts +86 -0
- package/packages/dashboard/ui/react-components/hooks/useChannelAdmin.ts +329 -0
- package/packages/dashboard/ui/react-components/hooks/useChannelBrowser.ts +239 -0
- package/packages/dashboard/ui/react-components/hooks/useChannelCommands.ts +138 -0
- package/packages/dashboard/ui/react-components/hooks/useChannels.ts +328 -0
- package/packages/dashboard/ui/react-components/hooks/useDebounce.ts +29 -0
- package/packages/dashboard/ui/react-components/hooks/useDirectMessage.ts +141 -0
- package/packages/dashboard/ui/react-components/hooks/useMessages.ts +309 -0
- package/packages/dashboard/ui/react-components/hooks/useOrchestrator.ts +364 -0
- package/packages/dashboard/ui/react-components/hooks/usePinnedAgents.ts +140 -0
- package/packages/dashboard/ui/react-components/hooks/usePresence.ts +340 -0
- package/packages/dashboard/ui/react-components/hooks/useRecentRepos.ts +130 -0
- package/packages/dashboard/ui/react-components/hooks/useSession.ts +209 -0
- package/packages/dashboard/ui/react-components/hooks/useTrajectory.ts +265 -0
- package/packages/dashboard/ui/react-components/hooks/useWebSocket.ts +169 -0
- package/packages/dashboard/ui/react-components/hooks/useWorkspaceMembers.ts +120 -0
- package/packages/dashboard/ui/react-components/hooks/useWorkspaceRepos.ts +73 -0
- package/packages/dashboard/ui/react-components/hooks/useWorkspaceStatus.ts +237 -0
- package/packages/dashboard/ui/react-components/index.ts +81 -0
- package/packages/dashboard/ui/react-components/layout/Header.tsx +355 -0
- package/packages/dashboard/ui/react-components/layout/RepoContextHeader.tsx +361 -0
- package/packages/dashboard/ui/react-components/layout/Sidebar.archive.test.tsx +126 -0
- package/packages/dashboard/ui/react-components/layout/Sidebar.test.tsx +691 -0
- package/packages/dashboard/ui/react-components/layout/Sidebar.tsx +930 -0
- package/packages/dashboard/ui/react-components/layout/index.ts +7 -0
- package/packages/dashboard/ui/react-components/settings/BillingSettingsPanel.tsx +564 -0
- package/packages/dashboard/ui/react-components/settings/SettingsPage.tsx +544 -0
- package/packages/dashboard/ui/react-components/settings/TeamSettingsPanel.tsx +560 -0
- package/packages/dashboard/ui/react-components/settings/WorkspaceSettingsPanel.tsx +1329 -0
- package/packages/dashboard/ui/react-components/settings/index.ts +11 -0
- package/packages/dashboard/ui/react-components/settings/types.ts +53 -0
- package/packages/dashboard/ui/react-components/utils/messageFormatting.tsx +370 -0
- package/packages/dashboard/ui/tailwind.config.js +148 -0
- package/packages/dashboard/ui/types/index.ts +304 -0
- package/packages/dashboard/ui/types/threading.ts +7 -0
- package/packages/dashboard/ui-dist/404.html +1 -0
- package/packages/dashboard/ui-dist/_next/static/HR7W9z1PPVPFqUboUVZFZ/_buildManifest.js +1 -0
- package/packages/dashboard/ui-dist/_next/static/HR7W9z1PPVPFqUboUVZFZ/_ssgManifest.js +1 -0
- package/packages/dashboard/ui-dist/_next/static/ZCFjHbkF8yDKS2md3lVgb/_buildManifest.js +1 -0
- package/packages/dashboard/ui-dist/_next/static/ZCFjHbkF8yDKS2md3lVgb/_ssgManifest.js +1 -0
- package/packages/dashboard/ui-dist/_next/static/chunks/116-a883fca163f3a5bc.js +1 -0
- package/packages/dashboard/ui-dist/_next/static/chunks/117-c8afed19e821a35d.js +2 -0
- package/packages/dashboard/ui-dist/_next/static/chunks/282-980c2eb8fff20123.js +1 -0
- package/packages/dashboard/ui-dist/_next/static/chunks/320-900169c942e31422.js +1 -0
- package/packages/dashboard/ui-dist/_next/static/chunks/532-bace199897eeab37.js +9 -0
- package/packages/dashboard/ui-dist/_next/static/chunks/631-af51bad94027527a.js +1 -0
- package/packages/dashboard/ui-dist/_next/static/chunks/648-acb2ff9f77cbfbd3.js +1 -0
- package/packages/dashboard/ui-dist/_next/static/chunks/677-30e60cb0b47875b6.js +1 -0
- package/packages/dashboard/ui-dist/_next/static/chunks/766-2aea80818f7eb0d8.js +1 -0
- package/packages/dashboard/ui-dist/_next/static/chunks/83-4f08122d4e7e79a6.js +1 -0
- package/packages/dashboard/ui-dist/_next/static/chunks/847-f1f467060f32afff.js +1 -0
- package/packages/dashboard/ui-dist/_next/static/chunks/891-5cb1513eeb97a891.js +1 -0
- package/packages/dashboard/ui-dist/_next/static/chunks/app/_not-found/page-60501fddbafba9dc.js +1 -0
- package/packages/dashboard/ui-dist/_next/static/chunks/app/app/onboarding/page-9914652442f7e4fb.js +1 -0
- package/packages/dashboard/ui-dist/_next/static/chunks/app/app/onboarding/page-f746f29e01fffc43.js +1 -0
- package/packages/dashboard/ui-dist/_next/static/chunks/app/app/page-2e525b1dcc790967.js +1 -0
- package/packages/dashboard/ui-dist/_next/static/chunks/app/app/page-44813aa26ad19681.js +1 -0
- package/packages/dashboard/ui-dist/_next/static/chunks/app/cloud/link/page-5011ae044b90449d.js +1 -0
- package/packages/dashboard/ui-dist/_next/static/chunks/app/cloud/link/page-fa1d5842aa90e8a6.js +1 -0
- package/packages/dashboard/ui-dist/_next/static/chunks/app/connect-repos/page-03ac6f35a6654ea6.js +1 -0
- package/packages/dashboard/ui-dist/_next/static/chunks/app/connect-repos/page-113060009ef35bc2.js +1 -0
- package/packages/dashboard/ui-dist/_next/static/chunks/app/history/page-9965d2483011b846.js +1 -0
- package/packages/dashboard/ui-dist/_next/static/chunks/app/history/page-b2ce7c96ed0931da.js +1 -0
- package/packages/dashboard/ui-dist/_next/static/chunks/app/layout-6b91e33784c20610.js +1 -0
- package/packages/dashboard/ui-dist/_next/static/chunks/app/layout-c0d118c0f92d969c.js +1 -0
- package/packages/dashboard/ui-dist/_next/static/chunks/app/login/page-6ec54eee75877971.js +1 -0
- package/packages/dashboard/ui-dist/_next/static/chunks/app/login/page-a0ca6f7ca6a100b8.js +1 -0
- package/packages/dashboard/ui-dist/_next/static/chunks/app/metrics/page-1e37ef8e73940b40.js +1 -0
- package/packages/dashboard/ui-dist/_next/static/chunks/app/metrics/page-bf2cb1e5915bc92d.js +1 -0
- package/packages/dashboard/ui-dist/_next/static/chunks/app/page-4e64923d73c35bc9.js +1 -0
- package/packages/dashboard/ui-dist/_next/static/chunks/app/page-7993778218818ace.js +1 -0
- package/packages/dashboard/ui-dist/_next/static/chunks/app/pricing/page-0efa024c28ba4597.js +1 -0
- package/packages/dashboard/ui-dist/_next/static/chunks/app/pricing/page-9db3ebdfa567a7c9.js +1 -0
- package/packages/dashboard/ui-dist/_next/static/chunks/app/providers/page-bcf46064ac4474ce.js +1 -0
- package/packages/dashboard/ui-dist/_next/static/chunks/app/providers/page-e65a0010da6ea5be.js +1 -0
- package/packages/dashboard/ui-dist/_next/static/chunks/app/providers/setup/[provider]/page-4dbe33f0f7691b7c.js +1 -0
- package/packages/dashboard/ui-dist/_next/static/chunks/app/providers/setup/[provider]/page-84161c802b020a1f.js +1 -0
- package/packages/dashboard/ui-dist/_next/static/chunks/app/signup/page-18a4665665f6be11.js +1 -0
- package/packages/dashboard/ui-dist/_next/static/chunks/app/signup/page-1ede2205b58649ca.js +1 -0
- package/packages/dashboard/ui-dist/_next/static/chunks/e868780c-48e5f147c90a3a41.js +18 -0
- package/packages/dashboard/ui-dist/_next/static/chunks/fd9d1056-609918ca7b6280bb.js +1 -0
- package/packages/dashboard/ui-dist/_next/static/chunks/framework-f66176bb897dc684.js +1 -0
- package/packages/dashboard/ui-dist/_next/static/chunks/main-5a40a5ae29646e1b.js +1 -0
- package/packages/dashboard/ui-dist/_next/static/chunks/main-app-6e8e8d3ef4e0192a.js +1 -0
- package/packages/dashboard/ui-dist/_next/static/chunks/main-app-fdbeb09028f57c9f.js +1 -0
- package/packages/dashboard/ui-dist/_next/static/chunks/pages/_app-72b849fbd24ac258.js +1 -0
- package/packages/dashboard/ui-dist/_next/static/chunks/pages/_error-7ba65e1336b92748.js +1 -0
- package/packages/dashboard/ui-dist/_next/static/chunks/polyfills-42372ed130431b0a.js +1 -0
- package/packages/dashboard/ui-dist/_next/static/chunks/webpack-1cdd8ed57114d5e1.js +1 -0
- package/packages/dashboard/ui-dist/_next/static/css/4034f236dd1a3178.css +1 -0
- package/packages/dashboard/ui-dist/_next/static/css/99c2552394077586.css +1 -0
- package/packages/dashboard/ui-dist/alt-logos/agent-relay-logo-128.png +0 -0
- package/packages/dashboard/ui-dist/alt-logos/agent-relay-logo-256.png +0 -0
- package/packages/dashboard/ui-dist/alt-logos/agent-relay-logo-32.png +0 -0
- package/packages/dashboard/ui-dist/alt-logos/agent-relay-logo-512.png +0 -0
- package/packages/dashboard/ui-dist/alt-logos/agent-relay-logo-64.png +0 -0
- package/packages/dashboard/ui-dist/alt-logos/agent-relay-logo.svg +45 -0
- package/packages/dashboard/ui-dist/alt-logos/logo.svg +38 -0
- package/packages/dashboard/ui-dist/alt-logos/monogram-logo-128.png +0 -0
- package/packages/dashboard/ui-dist/alt-logos/monogram-logo-256.png +0 -0
- package/packages/dashboard/ui-dist/alt-logos/monogram-logo-32.png +0 -0
- package/packages/dashboard/ui-dist/alt-logos/monogram-logo-512.png +0 -0
- package/packages/dashboard/ui-dist/alt-logos/monogram-logo-64.png +0 -0
- package/packages/dashboard/ui-dist/alt-logos/monogram-logo.svg +38 -0
- package/packages/dashboard/ui-dist/app/onboarding.html +1 -0
- package/packages/dashboard/ui-dist/app/onboarding.txt +7 -0
- package/packages/dashboard/ui-dist/app.html +1 -0
- package/packages/dashboard/ui-dist/app.txt +7 -0
- package/packages/dashboard/ui-dist/apple-icon.png +0 -0
- package/packages/dashboard/ui-dist/cloud/link.html +1 -0
- package/packages/dashboard/ui-dist/cloud/link.txt +7 -0
- package/packages/dashboard/ui-dist/connect-repos.html +1 -0
- package/packages/dashboard/ui-dist/connect-repos.txt +7 -0
- package/packages/dashboard/ui-dist/history.html +1 -0
- package/packages/dashboard/ui-dist/history.txt +7 -0
- package/packages/dashboard/ui-dist/index.html +1 -0
- package/packages/dashboard/ui-dist/index.txt +7 -0
- package/packages/dashboard/ui-dist/login.html +5 -0
- package/packages/dashboard/ui-dist/login.txt +7 -0
- package/packages/dashboard/ui-dist/metrics.html +1 -0
- package/packages/dashboard/ui-dist/metrics.txt +7 -0
- package/packages/dashboard/ui-dist/pricing.html +13 -0
- package/packages/dashboard/ui-dist/pricing.txt +7 -0
- package/packages/dashboard/ui-dist/providers/setup/claude.html +1 -0
- package/packages/dashboard/ui-dist/providers/setup/claude.txt +8 -0
- package/packages/dashboard/ui-dist/providers/setup/codex.html +1 -0
- package/packages/dashboard/ui-dist/providers/setup/codex.txt +8 -0
- package/packages/dashboard/ui-dist/providers/setup/cursor.html +1 -0
- package/packages/dashboard/ui-dist/providers/setup/cursor.txt +8 -0
- package/packages/dashboard/ui-dist/providers.html +1 -0
- package/packages/dashboard/ui-dist/providers.txt +7 -0
- package/packages/dashboard/ui-dist/signup.html +6 -0
- package/packages/dashboard/ui-dist/signup.txt +7 -0
- package/packages/dashboard-server/dist/health-worker-manager.d.ts +62 -0
- package/packages/dashboard-server/dist/health-worker-manager.js +144 -0
- package/packages/dashboard-server/dist/health-worker.d.ts +9 -0
- package/packages/dashboard-server/dist/health-worker.js +79 -0
- package/packages/dashboard-server/dist/index.d.ts +18 -0
- package/packages/dashboard-server/dist/index.js +17 -0
- package/packages/dashboard-server/dist/server.js +5099 -0
- package/packages/dashboard-server/dist/start.js +13 -0
- package/packages/dashboard-server/dist/types/threading.d.ts +8 -0
- package/packages/dashboard-server/dist/types/threading.js +2 -0
- package/packages/dashboard-server/dist/user-bridge.d.ts +154 -0
- package/packages/dashboard-server/dist/user-bridge.js +372 -0
- package/packages/dashboard-server/package.json +49 -0
- package/packages/hooks/dist/browser.d.ts +2 -0
- package/packages/hooks/dist/browser.js +3 -0
- package/packages/hooks/dist/index.d.ts +11 -0
- package/packages/hooks/dist/index.js +11 -0
- package/packages/hooks/dist/registry.js +476 -0
- package/packages/hooks/dist/trajectory-hooks.js +183 -0
- package/packages/hooks/dist/types.d.ts +285 -0
- package/packages/hooks/dist/types.js +10 -0
- package/packages/hooks/package.json +52 -0
- package/packages/mcp/LICENSE +190 -0
- package/packages/mcp/README.md +214 -0
- package/packages/mcp/SPEC.md +1922 -0
- package/packages/mcp/STAFFING_PLAN.md +294 -0
- package/packages/mcp/dist/bin.d.ts +12 -0
- package/packages/mcp/dist/bin.js +127 -0
- package/packages/mcp/dist/client.d.ts +68 -0
- package/packages/mcp/dist/client.js +115 -0
- package/packages/mcp/dist/cloud.d.ts +108 -0
- package/packages/mcp/dist/cloud.js +279 -0
- package/packages/mcp/dist/errors.d.ts +27 -0
- package/packages/mcp/dist/errors.js +48 -0
- package/packages/mcp/dist/index.d.ts +10 -0
- package/packages/mcp/dist/index.js +16 -0
- package/packages/mcp/dist/install-cli.d.ts +35 -0
- package/packages/mcp/dist/install-cli.js +157 -0
- package/packages/mcp/dist/install.d.ts +101 -0
- package/packages/mcp/dist/install.js +398 -0
- package/packages/mcp/dist/prompts/index.d.ts +2 -0
- package/packages/mcp/dist/prompts/index.js +2 -0
- package/packages/mcp/dist/prompts/protocol.d.ts +11 -0
- package/packages/mcp/dist/prompts/protocol.js +168 -0
- package/packages/mcp/dist/resources/agents.d.ts +11 -0
- package/packages/mcp/dist/resources/agents.js +17 -0
- package/packages/mcp/dist/resources/inbox.d.ts +11 -0
- package/packages/mcp/dist/resources/inbox.js +17 -0
- package/packages/mcp/dist/resources/index.d.ts +4 -0
- package/packages/mcp/dist/resources/index.js +4 -0
- package/packages/mcp/dist/resources/project.d.ts +11 -0
- package/packages/mcp/dist/resources/project.js +21 -0
- package/packages/mcp/dist/server.d.ts +19 -0
- package/packages/mcp/dist/server.js +215 -0
- package/packages/mcp/dist/simple.d.ts +173 -0
- package/packages/mcp/dist/simple.js +120 -0
- package/packages/mcp/dist/tools/index.d.ts +10 -0
- package/packages/mcp/dist/tools/index.js +10 -0
- package/packages/mcp/dist/tools/relay-health.d.ts +23 -0
- package/packages/mcp/dist/tools/relay-health.js +138 -0
- package/packages/mcp/dist/tools/relay-inbox.d.ts +26 -0
- package/packages/mcp/dist/tools/relay-inbox.js +58 -0
- package/packages/mcp/dist/tools/relay-logs.d.ts +20 -0
- package/packages/mcp/dist/tools/relay-logs.js +88 -0
- package/packages/mcp/dist/tools/relay-metrics.d.ts +20 -0
- package/packages/mcp/dist/tools/relay-metrics.js +135 -0
- package/packages/mcp/dist/tools/relay-release.d.ts +20 -0
- package/packages/mcp/dist/tools/relay-release.js +44 -0
- package/packages/mcp/dist/tools/relay-send.d.ts +29 -0
- package/packages/mcp/dist/tools/relay-send.js +71 -0
- package/packages/mcp/dist/tools/relay-spawn.d.ts +36 -0
- package/packages/mcp/dist/tools/relay-spawn.js +73 -0
- package/packages/mcp/dist/tools/relay-status.d.ts +11 -0
- package/packages/mcp/dist/tools/relay-status.js +43 -0
- package/packages/mcp/dist/tools/relay-who.d.ts +20 -0
- package/packages/mcp/dist/tools/relay-who.js +47 -0
- package/packages/mcp/package.json +69 -0
- package/packages/memory/dist/memory-hooks.d.ts +60 -0
- package/packages/memory/package.json +35 -0
- package/packages/policy/dist/agent-policy.js +665 -0
- package/packages/policy/dist/index.d.ts +12 -0
- package/packages/policy/dist/index.js +12 -0
- package/packages/policy/package.json +35 -0
- package/packages/protocol/dist/channels.d.ts +137 -0
- package/packages/protocol/dist/channels.js +154 -0
- package/packages/protocol/dist/framing.d.ts +80 -0
- package/packages/protocol/dist/framing.js +206 -0
- package/packages/protocol/dist/index.d.ts +5 -0
- package/packages/protocol/dist/index.js +5 -0
- package/packages/protocol/dist/relay-pty-schemas.d.ts +258 -0
- package/packages/protocol/dist/types.d.ts +341 -0
- package/packages/protocol/dist/types.js +8 -0
- package/packages/protocol/package.json +56 -0
- package/packages/resiliency/dist/memory-monitor.js +599 -0
- package/packages/resiliency/dist/provider-context.d.ts +100 -0
- package/packages/resiliency/package.json +33 -0
- package/packages/sdk/README.md +171 -0
- package/packages/sdk/dist/client.d.ts +181 -0
- package/packages/sdk/dist/client.js +695 -0
- package/packages/sdk/dist/index.d.ts +32 -0
- package/packages/sdk/dist/index.js +36 -0
- package/packages/sdk/dist/protocol/framing.d.ts +80 -0
- package/packages/sdk/dist/protocol/framing.js +206 -0
- package/packages/sdk/dist/protocol/index.d.ts +6 -0
- package/packages/sdk/dist/protocol/index.js +6 -0
- package/packages/sdk/dist/protocol/types.d.ts +341 -0
- package/packages/sdk/dist/protocol/types.js +8 -0
- package/packages/sdk/dist/standalone.d.ts +87 -0
- package/packages/sdk/dist/standalone.js +126 -0
- package/packages/sdk/package.json +80 -0
- package/packages/spawner/API.md +256 -0
- package/packages/spawner/dist/index.d.ts +8 -0
- package/packages/spawner/dist/index.js +8 -0
- package/packages/spawner/dist/types.d.ts +552 -0
- package/packages/spawner/dist/types.js +193 -0
- package/packages/spawner/package.json +47 -0
- package/packages/state/dist/agent-state.js +120 -0
- package/packages/state/dist/index.d.ts +8 -0
- package/packages/state/dist/index.js +8 -0
- package/packages/state/package.json +32 -0
- package/packages/storage/dist/adapter.d.ts +156 -0
- package/packages/storage/dist/batched-sqlite-adapter.d.ts +75 -0
- package/packages/storage/dist/batched-sqlite-adapter.js +189 -0
- package/packages/storage/dist/index.d.ts +5 -0
- package/packages/storage/dist/index.js +6 -0
- package/packages/storage/dist/sqlite-adapter.d.ts +113 -0
- package/packages/storage/dist/sqlite-adapter.js +752 -0
- package/packages/storage/package.json +69 -0
- package/packages/trajectory/dist/index.d.ts +2 -0
- package/packages/trajectory/dist/index.js +2 -0
- package/packages/trajectory/dist/integration.js +987 -0
- package/packages/trajectory/package.json +35 -0
- package/packages/user-directory/dist/index.d.ts +7 -0
- package/packages/user-directory/dist/index.js +7 -0
- package/packages/user-directory/dist/user-directory.d.ts +121 -0
- package/packages/user-directory/dist/user-directory.js +267 -0
- package/packages/user-directory/package.json +35 -0
- package/packages/utils/dist/command-resolver.js +80 -0
- package/packages/utils/dist/error-tracking.d.ts +103 -0
- package/packages/utils/dist/error-tracking.js +149 -0
- package/packages/utils/dist/index.d.ts +9 -0
- package/packages/utils/dist/index.js +9 -0
- package/packages/utils/dist/model-mapping.d.ts +28 -0
- package/packages/utils/dist/model-mapping.js +55 -0
- package/packages/utils/package.json +75 -0
- package/packages/wrapper/dist/__fixtures__/claude-outputs.d.ts +49 -0
- package/packages/wrapper/dist/__fixtures__/claude-outputs.js +443 -0
- package/packages/wrapper/dist/__fixtures__/codex-outputs.d.ts +9 -0
- package/packages/wrapper/dist/__fixtures__/codex-outputs.js +94 -0
- package/packages/wrapper/dist/__fixtures__/gemini-outputs.d.ts +19 -0
- package/packages/wrapper/dist/__fixtures__/gemini-outputs.js +144 -0
- package/packages/wrapper/dist/__fixtures__/index.d.ts +68 -0
- package/packages/wrapper/dist/__fixtures__/index.js +44 -0
- package/packages/wrapper/dist/base-wrapper.d.ts +225 -0
- package/packages/wrapper/dist/base-wrapper.js +572 -0
- package/packages/wrapper/dist/client.d.ts +254 -0
- package/packages/wrapper/dist/client.js +801 -0
- package/packages/wrapper/dist/id-generator.d.ts +35 -0
- package/packages/wrapper/dist/id-generator.js +60 -0
- package/packages/wrapper/dist/idle-detector.d.ts +110 -0
- package/packages/wrapper/dist/idle-detector.js +304 -0
- package/packages/wrapper/dist/index.d.ts +37 -0
- package/packages/wrapper/dist/index.js +47 -0
- package/packages/wrapper/dist/parser.d.ts +236 -0
- package/packages/wrapper/dist/parser.js +1238 -0
- package/packages/wrapper/dist/relay-pty-orchestrator.d.ts +407 -0
- package/packages/wrapper/dist/relay-pty-orchestrator.js +1885 -0
- package/packages/wrapper/dist/shared.d.ts +201 -0
- package/packages/wrapper/dist/shared.js +341 -0
- package/packages/wrapper/dist/stuck-detector.d.ts +161 -0
- package/packages/wrapper/dist/stuck-detector.js +402 -0
- package/packages/wrapper/dist/tmux-wrapper.d.ts +345 -0
- package/packages/wrapper/dist/tmux-wrapper.js +1747 -0
- package/packages/wrapper/dist/trajectory-integration.d.ts +292 -0
- package/packages/wrapper/dist/trajectory-integration.js +979 -0
- package/packages/wrapper/dist/wrapper-types.d.ts +41 -0
- package/packages/wrapper/dist/wrapper-types.js +7 -0
- package/packages/wrapper/package.json +63 -0
- package/scripts/setup-stripe-products.ts +312 -0
- package/scripts/stress-test-orchestrator-integration.mts +1366 -0
- package/scripts/stress-test-orchestrator.mjs +584 -0
- package/scripts/stress-test-relay-pty.sh +452 -0
- package/scripts/verify-schema.js +1 -1
- package/turbo.json +37 -0
- package/dist/bridge/config.d.ts +0 -41
- package/dist/bridge/config.js +0 -143
- package/dist/bridge/index.d.ts +0 -10
- package/dist/bridge/index.js +0 -10
- package/dist/bridge/multi-project-client.d.ts +0 -99
- package/dist/bridge/multi-project-client.js +0 -389
- package/dist/bridge/shadow-cli.js +0 -75
- package/dist/bridge/shadow-config.d.ts +0 -87
- package/dist/bridge/spawner.d.ts +0 -186
- package/dist/bridge/spawner.js +0 -920
- package/dist/bridge/types.d.ts +0 -129
- package/dist/bridge/utils.d.ts +0 -30
- package/dist/bridge/utils.js +0 -54
- package/dist/cli/index.js +0 -2784
- package/dist/cloud/api/admin.js +0 -225
- package/dist/cloud/api/billing.js +0 -564
- package/dist/cloud/api/cli-pty-runner.d.ts +0 -54
- package/dist/cloud/api/cli-pty-runner.js +0 -119
- package/dist/cloud/api/codex-auth-helper.js +0 -327
- package/dist/cloud/api/consensus.js +0 -259
- package/dist/cloud/api/coordinators.js +0 -749
- package/dist/cloud/api/daemons.js +0 -535
- package/dist/cloud/api/generic-webhooks.js +0 -129
- package/dist/cloud/api/github-app.js +0 -223
- package/dist/cloud/api/monitoring.js +0 -578
- package/dist/cloud/api/nango-auth.js +0 -658
- package/dist/cloud/api/onboarding.d.ts +0 -15
- package/dist/cloud/api/onboarding.js +0 -666
- package/dist/cloud/api/policy.js +0 -229
- package/dist/cloud/api/provider-env.d.ts +0 -5
- package/dist/cloud/api/provider-env.js +0 -27
- package/dist/cloud/api/providers.js +0 -511
- package/dist/cloud/api/repos.js +0 -576
- package/dist/cloud/api/teams.js +0 -279
- package/dist/cloud/api/test-helpers.js +0 -745
- package/dist/cloud/api/workspaces.js +0 -1783
- package/dist/cloud/billing/plans.js +0 -245
- package/dist/cloud/config.d.ts +0 -75
- package/dist/cloud/config.js +0 -109
- package/dist/cloud/db/drizzle.d.ts +0 -246
- package/dist/cloud/db/drizzle.js +0 -1249
- package/dist/cloud/db/schema.d.ts +0 -4854
- package/dist/cloud/db/schema.js +0 -610
- package/dist/cloud/index.d.ts +0 -11
- package/dist/cloud/index.js +0 -38
- package/dist/cloud/provisioner/index.d.ts +0 -207
- package/dist/cloud/provisioner/index.js +0 -2069
- package/dist/cloud/server.js +0 -1599
- package/dist/cloud/services/index.d.ts +0 -17
- package/dist/cloud/services/index.js +0 -25
- package/dist/cloud/services/intro-expiration.d.ts +0 -55
- package/dist/cloud/services/intro-expiration.js +0 -211
- package/dist/cloud/services/nango.d.ts +0 -199
- package/dist/cloud/services/nango.js +0 -382
- package/dist/cloud/services/persistence.d.ts +0 -131
- package/dist/config/relay-config.d.ts +0 -23
- package/dist/config/relay-config.js +0 -23
- package/dist/continuity/index.d.ts +0 -45
- package/dist/continuity/index.js +0 -48
- package/dist/continuity/types.d.ts +0 -180
- package/dist/continuity/types.js +0 -9
- package/dist/daemon/agent-manager.d.ts +0 -134
- package/dist/daemon/agent-manager.js +0 -564
- package/dist/daemon/agent-registry.js +0 -213
- package/dist/daemon/api.d.ts +0 -83
- package/dist/daemon/api.js +0 -780
- package/dist/daemon/channel-membership-store.d.ts +0 -48
- package/dist/daemon/channel-membership-store.js +0 -149
- package/dist/daemon/cli-auth.d.ts +0 -82
- package/dist/daemon/cli-auth.js +0 -700
- package/dist/daemon/cloud-sync.d.ts +0 -150
- package/dist/daemon/cloud-sync.js +0 -424
- package/dist/daemon/connection.d.ts +0 -130
- package/dist/daemon/connection.js +0 -438
- package/dist/daemon/consensus-integration.js +0 -371
- package/dist/daemon/delivery-tracker.d.ts +0 -34
- package/dist/daemon/delivery-tracker.js +0 -104
- package/dist/daemon/enhanced-features.d.ts +0 -118
- package/dist/daemon/enhanced-features.js +0 -178
- package/dist/daemon/index.d.ts +0 -14
- package/dist/daemon/index.js +0 -17
- package/dist/daemon/orchestrator.d.ts +0 -157
- package/dist/daemon/orchestrator.js +0 -792
- package/dist/daemon/repo-manager.js +0 -384
- package/dist/daemon/router.d.ts +0 -358
- package/dist/daemon/router.js +0 -1333
- package/dist/daemon/server.d.ts +0 -159
- package/dist/daemon/server.js +0 -788
- package/dist/daemon/services/browser-testing.d.ts +0 -88
- package/dist/daemon/services/browser-testing.js +0 -244
- package/dist/daemon/services/container-spawner.d.ts +0 -135
- package/dist/daemon/services/container-spawner.js +0 -313
- package/dist/daemon/sync-queue.d.ts +0 -116
- package/dist/daemon/sync-queue.js +0 -361
- package/dist/daemon/types.d.ts +0 -131
- package/dist/daemon/user-directory.d.ts +0 -111
- package/dist/daemon/user-directory.js +0 -233
- package/dist/daemon/workspace-manager.js +0 -314
- package/dist/dashboard/out/_next/static/chunks/116-eacf84a131b80db9.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/64-f4268c2ac6f4d7d4.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/766-aa7c8c9900ff5f53.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/891-a024fbe4b619cf6f.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/app/app/page-ffad986adfcc8b31.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/app/page-671037943b2f2e43.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/app/providers/page-57cbd738c6a73859.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/app/providers/setup/[provider]/page-5ab0854472b402b0.js +0 -1
- package/dist/dashboard/out/_next/static/css/8f9ed310f454e5a5.css +0 -1
- package/dist/dashboard-server/server.js +0 -4806
- package/dist/dashboard-server/start.js +0 -13
- package/dist/dashboard-server/user-bridge.d.ts +0 -138
- package/dist/dashboard-server/user-bridge.js +0 -348
- package/dist/hooks/index.d.ts +0 -10
- package/dist/hooks/index.js +0 -10
- package/dist/hooks/registry.js +0 -476
- package/dist/hooks/trajectory-hooks.js +0 -183
- package/dist/hooks/types.d.ts +0 -284
- package/dist/hooks/types.js +0 -8
- package/dist/index.d.ts +0 -13
- package/dist/index.js +0 -16
- package/dist/memory/memory-hooks.d.ts +0 -60
- package/dist/policy/agent-policy.js +0 -665
- package/dist/protocol/channels.d.ts +0 -211
- package/dist/protocol/channels.js +0 -154
- package/dist/protocol/framing.d.ts +0 -94
- package/dist/protocol/framing.js +0 -240
- package/dist/protocol/index.d.ts +0 -4
- package/dist/protocol/index.js +0 -4
- package/dist/protocol/relay-pty-schemas.d.ts +0 -209
- package/dist/protocol/types.d.ts +0 -168
- package/dist/protocol/types.js +0 -6
- package/dist/resiliency/memory-monitor.js +0 -593
- package/dist/resiliency/provider-context.d.ts +0 -100
- package/dist/shared/cli-auth-config.js +0 -320
- package/dist/state/agent-state.js +0 -120
- package/dist/storage/adapter.d.ts +0 -154
- package/dist/storage/batched-sqlite-adapter.d.ts +0 -71
- package/dist/storage/batched-sqlite-adapter.js +0 -183
- package/dist/storage/sqlite-adapter.d.ts +0 -107
- package/dist/storage/sqlite-adapter.js +0 -717
- package/dist/trajectory/config.d.ts +0 -102
- package/dist/trajectory/config.js +0 -185
- package/dist/trajectory/index.d.ts +0 -8
- package/dist/trajectory/index.js +0 -8
- package/dist/trajectory/integration.js +0 -987
- package/dist/utils/command-resolver.js +0 -76
- package/dist/utils/index.d.ts +0 -4
- package/dist/utils/index.js +0 -4
- package/dist/utils/project-namespace.d.ts +0 -70
- package/dist/utils/project-namespace.js +0 -216
- package/dist/wrapper/base-wrapper.d.ts +0 -217
- package/dist/wrapper/base-wrapper.js +0 -538
- package/dist/wrapper/client.d.ts +0 -199
- package/dist/wrapper/client.js +0 -677
- package/dist/wrapper/idle-detector.d.ts +0 -102
- package/dist/wrapper/idle-detector.js +0 -279
- package/dist/wrapper/index.d.ts +0 -4
- package/dist/wrapper/index.js +0 -7
- package/dist/wrapper/parser.d.ts +0 -230
- package/dist/wrapper/parser.js +0 -1178
- package/dist/wrapper/pty-wrapper.d.ts +0 -343
- package/dist/wrapper/pty-wrapper.js +0 -1593
- package/dist/wrapper/relay-pty-orchestrator.d.ts +0 -296
- package/dist/wrapper/relay-pty-orchestrator.js +0 -1088
- package/dist/wrapper/shared.d.ts +0 -168
- package/dist/wrapper/shared.js +0 -291
- package/dist/wrapper/stuck-detector.d.ts +0 -101
- package/dist/wrapper/stuck-detector.js +0 -228
- package/dist/wrapper/tmux-wrapper.d.ts +0 -344
- package/dist/wrapper/tmux-wrapper.js +0 -1711
- /package/dist/dashboard/out/_next/static/{BffXAqxm-_rUlj2mAnK26 → ZCFjHbkF8yDKS2md3lVgb}/_buildManifest.js +0 -0
- /package/dist/dashboard/out/_next/static/{BffXAqxm-_rUlj2mAnK26 → ZCFjHbkF8yDKS2md3lVgb}/_ssgManifest.js +0 -0
- /package/dist/dashboard/out/_next/static/chunks/app/cloud/link/{page-cfeb437f08a12ed9.js → page-5011ae044b90449d.js} +0 -0
- /package/dist/dashboard/out/_next/static/chunks/app/history/{page-240f91e8b06ba8ac.js → page-b2ce7c96ed0931da.js} +0 -0
- /package/dist/dashboard/out/_next/static/chunks/app/metrics/{page-82938ab8fcf44694.js → page-bf2cb1e5915bc92d.js} +0 -0
- /package/dist/{cli → src/cli}/index.d.ts +0 -0
- /package/dist/{health-worker-manager.d.ts → src/health-worker-manager.d.ts} +0 -0
- /package/dist/{health-worker-manager.js → src/health-worker-manager.js} +0 -0
- /package/dist/{health-worker.d.ts → src/health-worker.d.ts} +0 -0
- /package/dist/{health-worker.js → src/health-worker.js} +0 -0
- /package/{dist/bridge → packages/bridge/dist}/shadow-cli.d.ts +0 -0
- /package/{dist/bridge → packages/bridge/dist}/types.js +0 -0
- /package/{dist/cloud → packages/cloud/dist}/api/admin.d.ts +0 -0
- /package/{dist/cloud → packages/cloud/dist}/api/auth.d.ts +0 -0
- /package/{dist/cloud → packages/cloud/dist}/api/auth.js +0 -0
- /package/{dist/cloud → packages/cloud/dist}/api/billing.d.ts +0 -0
- /package/{dist/cloud → packages/cloud/dist}/api/codex-auth-helper.d.ts +0 -0
- /package/{dist/cloud → packages/cloud/dist}/api/consensus.d.ts +0 -0
- /package/{dist/cloud → packages/cloud/dist}/api/coordinators.d.ts +0 -0
- /package/{dist/cloud → packages/cloud/dist}/api/daemons.d.ts +0 -0
- /package/{dist/cloud → packages/cloud/dist}/api/generic-webhooks.d.ts +0 -0
- /package/{dist/cloud → packages/cloud/dist}/api/git.d.ts +0 -0
- /package/{dist/cloud → packages/cloud/dist}/api/git.js +0 -0
- /package/{dist/cloud → packages/cloud/dist}/api/github-app.d.ts +0 -0
- /package/{dist/cloud → packages/cloud/dist}/api/middleware/planLimits.d.ts +0 -0
- /package/{dist/cloud → packages/cloud/dist}/api/middleware/planLimits.js +0 -0
- /package/{dist/cloud → packages/cloud/dist}/api/monitoring.d.ts +0 -0
- /package/{dist/cloud → packages/cloud/dist}/api/nango-auth.d.ts +0 -0
- /package/{dist/cloud → packages/cloud/dist}/api/policy.d.ts +0 -0
- /package/{dist/cloud → packages/cloud/dist}/api/providers.d.ts +0 -0
- /package/{dist/cloud → packages/cloud/dist}/api/repos.d.ts +0 -0
- /package/{dist/cloud → packages/cloud/dist}/api/teams.d.ts +0 -0
- /package/{dist/cloud → packages/cloud/dist}/api/test-helpers.d.ts +0 -0
- /package/{dist/cloud → packages/cloud/dist}/api/usage.d.ts +0 -0
- /package/{dist/cloud → packages/cloud/dist}/api/usage.js +0 -0
- /package/{dist/cloud → packages/cloud/dist}/api/webhooks.d.ts +0 -0
- /package/{dist/cloud → packages/cloud/dist}/api/webhooks.js +0 -0
- /package/{dist/cloud → packages/cloud/dist}/api/workspaces.d.ts +0 -0
- /package/{dist/cloud → packages/cloud/dist}/billing/index.d.ts +0 -0
- /package/{dist/cloud → packages/cloud/dist}/billing/index.js +0 -0
- /package/{dist/cloud → packages/cloud/dist}/billing/plans.d.ts +0 -0
- /package/{dist/cloud → packages/cloud/dist}/billing/service.d.ts +0 -0
- /package/{dist/cloud → packages/cloud/dist}/billing/service.js +0 -0
- /package/{dist/cloud → packages/cloud/dist}/billing/types.d.ts +0 -0
- /package/{dist/cloud → packages/cloud/dist}/billing/types.js +0 -0
- /package/{dist/cloud → packages/cloud/dist}/db/bulk-ingest.d.ts +0 -0
- /package/{dist/cloud → packages/cloud/dist}/db/bulk-ingest.js +0 -0
- /package/{dist/cloud → packages/cloud/dist}/db/index.d.ts +0 -0
- /package/{dist/cloud → packages/cloud/dist}/db/index.js +0 -0
- /package/{dist/cloud → packages/cloud/dist}/server.d.ts +0 -0
- /package/{dist/cloud → packages/cloud/dist}/services/auto-scaler.d.ts +0 -0
- /package/{dist/cloud → packages/cloud/dist}/services/auto-scaler.js +0 -0
- /package/{dist/cloud → packages/cloud/dist}/services/capacity-manager.d.ts +0 -0
- /package/{dist/cloud → packages/cloud/dist}/services/capacity-manager.js +0 -0
- /package/{dist/cloud → packages/cloud/dist}/services/ci-agent-spawner.d.ts +0 -0
- /package/{dist/cloud → packages/cloud/dist}/services/ci-agent-spawner.js +0 -0
- /package/{dist/cloud → packages/cloud/dist}/services/cloud-message-bus.d.ts +0 -0
- /package/{dist/cloud → packages/cloud/dist}/services/cloud-message-bus.js +0 -0
- /package/{dist/cloud → packages/cloud/dist}/services/compute-enforcement.d.ts +0 -0
- /package/{dist/cloud → packages/cloud/dist}/services/compute-enforcement.js +0 -0
- /package/{dist/cloud → packages/cloud/dist}/services/coordinator.d.ts +0 -0
- /package/{dist/cloud → packages/cloud/dist}/services/coordinator.js +0 -0
- /package/{dist/cloud → packages/cloud/dist}/services/mention-handler.d.ts +0 -0
- /package/{dist/cloud → packages/cloud/dist}/services/mention-handler.js +0 -0
- /package/{dist/cloud → packages/cloud/dist}/services/persistence.js +0 -0
- /package/{dist/cloud → packages/cloud/dist}/services/planLimits.d.ts +0 -0
- /package/{dist/cloud → packages/cloud/dist}/services/planLimits.js +0 -0
- /package/{dist/cloud → packages/cloud/dist}/services/presence-registry.d.ts +0 -0
- /package/{dist/cloud → packages/cloud/dist}/services/presence-registry.js +0 -0
- /package/{dist/cloud → packages/cloud/dist}/services/scaling-orchestrator.d.ts +0 -0
- /package/{dist/cloud → packages/cloud/dist}/services/scaling-orchestrator.js +0 -0
- /package/{dist/cloud → packages/cloud/dist}/services/scaling-policy.d.ts +0 -0
- /package/{dist/cloud → packages/cloud/dist}/services/scaling-policy.js +0 -0
- /package/{dist/cloud → packages/cloud/dist}/services/ssh-security.d.ts +0 -0
- /package/{dist/cloud → packages/cloud/dist}/services/ssh-security.js +0 -0
- /package/{dist/cloud → packages/cloud/dist}/services/workspace-keepalive.d.ts +0 -0
- /package/{dist/cloud → packages/cloud/dist}/services/workspace-keepalive.js +0 -0
- /package/{dist/cloud → packages/cloud/dist}/webhooks/index.d.ts +0 -0
- /package/{dist/cloud → packages/cloud/dist}/webhooks/index.js +0 -0
- /package/{dist/cloud → packages/cloud/dist}/webhooks/parsers/github.d.ts +0 -0
- /package/{dist/cloud → packages/cloud/dist}/webhooks/parsers/github.js +0 -0
- /package/{dist/cloud → packages/cloud/dist}/webhooks/parsers/index.d.ts +0 -0
- /package/{dist/cloud → packages/cloud/dist}/webhooks/parsers/index.js +0 -0
- /package/{dist/cloud → packages/cloud/dist}/webhooks/parsers/linear.d.ts +0 -0
- /package/{dist/cloud → packages/cloud/dist}/webhooks/parsers/linear.js +0 -0
- /package/{dist/cloud → packages/cloud/dist}/webhooks/parsers/slack.d.ts +0 -0
- /package/{dist/cloud → packages/cloud/dist}/webhooks/parsers/slack.js +0 -0
- /package/{dist/cloud → packages/cloud/dist}/webhooks/responders/github.d.ts +0 -0
- /package/{dist/cloud → packages/cloud/dist}/webhooks/responders/github.js +0 -0
- /package/{dist/cloud → packages/cloud/dist}/webhooks/responders/index.d.ts +0 -0
- /package/{dist/cloud → packages/cloud/dist}/webhooks/responders/index.js +0 -0
- /package/{dist/cloud → packages/cloud/dist}/webhooks/responders/linear.d.ts +0 -0
- /package/{dist/cloud → packages/cloud/dist}/webhooks/responders/linear.js +0 -0
- /package/{dist/cloud → packages/cloud/dist}/webhooks/responders/slack.d.ts +0 -0
- /package/{dist/cloud → packages/cloud/dist}/webhooks/responders/slack.js +0 -0
- /package/{dist/cloud → packages/cloud/dist}/webhooks/router.d.ts +0 -0
- /package/{dist/cloud → packages/cloud/dist}/webhooks/router.js +0 -0
- /package/{dist/cloud → packages/cloud/dist}/webhooks/rules-engine.d.ts +0 -0
- /package/{dist/cloud → packages/cloud/dist}/webhooks/rules-engine.js +0 -0
- /package/{dist/cloud → packages/cloud/dist}/webhooks/types.d.ts +0 -0
- /package/{dist/cloud → packages/cloud/dist}/webhooks/types.js +0 -0
- /package/{dist/utils → packages/config/dist}/agent-config.d.ts +0 -0
- /package/{dist/utils → packages/config/dist}/agent-config.js +0 -0
- /package/{dist/shared → packages/config/dist}/cli-auth-config.d.ts +0 -0
- /package/{dist/bridge → packages/config/dist}/shadow-config.js +0 -0
- /package/{dist/bridge → packages/config/dist}/teams-config.d.ts +0 -0
- /package/{dist/bridge → packages/config/dist}/teams-config.js +0 -0
- /package/{dist/continuity → packages/continuity/dist}/formatter.d.ts +0 -0
- /package/{dist/continuity → packages/continuity/dist}/formatter.js +0 -0
- /package/{dist/continuity → packages/continuity/dist}/handoff-store.d.ts +0 -0
- /package/{dist/continuity → packages/continuity/dist}/handoff-store.js +0 -0
- /package/{dist/continuity → packages/continuity/dist}/ledger-store.d.ts +0 -0
- /package/{dist/continuity → packages/continuity/dist}/ledger-store.js +0 -0
- /package/{dist/continuity → packages/continuity/dist}/manager.d.ts +0 -0
- /package/{dist/continuity → packages/continuity/dist}/manager.js +0 -0
- /package/{dist/continuity → packages/continuity/dist}/parser.d.ts +0 -0
- /package/{dist/continuity → packages/continuity/dist}/parser.js +0 -0
- /package/{dist/daemon → packages/daemon/dist}/agent-registry.d.ts +0 -0
- /package/{dist/daemon → packages/daemon/dist}/agent-signing.d.ts +0 -0
- /package/{dist/daemon → packages/daemon/dist}/agent-signing.js +0 -0
- /package/{dist/daemon → packages/daemon/dist}/auth.d.ts +0 -0
- /package/{dist/daemon → packages/daemon/dist}/auth.js +0 -0
- /package/{dist/daemon → packages/daemon/dist}/consensus-integration.d.ts +0 -0
- /package/{dist/daemon → packages/daemon/dist}/consensus.d.ts +0 -0
- /package/{dist/daemon → packages/daemon/dist}/consensus.js +0 -0
- /package/{dist/daemon → packages/daemon/dist}/rate-limiter.d.ts +0 -0
- /package/{dist/daemon → packages/daemon/dist}/rate-limiter.js +0 -0
- /package/{dist/daemon → packages/daemon/dist}/registry.d.ts +0 -0
- /package/{dist/daemon → packages/daemon/dist}/registry.js +0 -0
- /package/{dist/daemon → packages/daemon/dist}/repo-manager.d.ts +0 -0
- /package/{dist/daemon → packages/daemon/dist}/types.js +0 -0
- /package/{dist/daemon → packages/daemon/dist}/workspace-manager.d.ts +0 -0
- /package/{dist/dashboard-server → packages/dashboard-server/dist}/metrics.d.ts +0 -0
- /package/{dist/dashboard-server → packages/dashboard-server/dist}/metrics.js +0 -0
- /package/{dist/dashboard-server → packages/dashboard-server/dist}/needs-attention.d.ts +0 -0
- /package/{dist/dashboard-server → packages/dashboard-server/dist}/needs-attention.js +0 -0
- /package/{dist/dashboard-server → packages/dashboard-server/dist}/server.d.ts +0 -0
- /package/{dist/dashboard-server → packages/dashboard-server/dist}/start.d.ts +0 -0
- /package/{dist/hooks → packages/hooks/dist}/emitter.d.ts +0 -0
- /package/{dist/hooks → packages/hooks/dist}/emitter.js +0 -0
- /package/{dist/hooks → packages/hooks/dist}/inbox-check/hook.d.ts +0 -0
- /package/{dist/hooks → packages/hooks/dist}/inbox-check/hook.js +0 -0
- /package/{dist/hooks → packages/hooks/dist}/inbox-check/index.d.ts +0 -0
- /package/{dist/hooks → packages/hooks/dist}/inbox-check/index.js +0 -0
- /package/{dist/hooks → packages/hooks/dist}/inbox-check/types.d.ts +0 -0
- /package/{dist/hooks → packages/hooks/dist}/inbox-check/types.js +0 -0
- /package/{dist/hooks → packages/hooks/dist}/inbox-check/utils.d.ts +0 -0
- /package/{dist/hooks → packages/hooks/dist}/inbox-check/utils.js +0 -0
- /package/{dist/hooks → packages/hooks/dist}/registry.d.ts +0 -0
- /package/{dist/hooks → packages/hooks/dist}/trajectory-hooks.d.ts +0 -0
- /package/{dist/memory → packages/memory/dist}/adapters/index.d.ts +0 -0
- /package/{dist/memory → packages/memory/dist}/adapters/index.js +0 -0
- /package/{dist/memory → packages/memory/dist}/adapters/inmemory.d.ts +0 -0
- /package/{dist/memory → packages/memory/dist}/adapters/inmemory.js +0 -0
- /package/{dist/memory → packages/memory/dist}/adapters/supermemory.d.ts +0 -0
- /package/{dist/memory → packages/memory/dist}/adapters/supermemory.js +0 -0
- /package/{dist/memory → packages/memory/dist}/context-compaction.d.ts +0 -0
- /package/{dist/memory → packages/memory/dist}/context-compaction.js +0 -0
- /package/{dist/memory → packages/memory/dist}/factory.d.ts +0 -0
- /package/{dist/memory → packages/memory/dist}/factory.js +0 -0
- /package/{dist/memory → packages/memory/dist}/index.d.ts +0 -0
- /package/{dist/memory → packages/memory/dist}/index.js +0 -0
- /package/{dist/memory → packages/memory/dist}/memory-hooks.js +0 -0
- /package/{dist/memory → packages/memory/dist}/service.d.ts +0 -0
- /package/{dist/memory → packages/memory/dist}/service.js +0 -0
- /package/{dist/memory → packages/memory/dist}/types.d.ts +0 -0
- /package/{dist/memory → packages/memory/dist}/types.js +0 -0
- /package/{dist/policy → packages/policy/dist}/agent-policy.d.ts +0 -0
- /package/{dist/policy → packages/policy/dist}/cloud-policy-fetcher.d.ts +0 -0
- /package/{dist/policy → packages/policy/dist}/cloud-policy-fetcher.js +0 -0
- /package/{dist/utils → packages/protocol/dist}/id-generator.d.ts +0 -0
- /package/{dist/utils → packages/protocol/dist}/id-generator.js +0 -0
- /package/{dist/protocol → packages/protocol/dist}/relay-pty-schemas.js +0 -0
- /package/{dist/resiliency → packages/resiliency/dist}/context-persistence.d.ts +0 -0
- /package/{dist/resiliency → packages/resiliency/dist}/context-persistence.js +0 -0
- /package/{dist/resiliency → packages/resiliency/dist}/crash-insights.d.ts +0 -0
- /package/{dist/resiliency → packages/resiliency/dist}/crash-insights.js +0 -0
- /package/{dist/resiliency → packages/resiliency/dist}/gossip-health.d.ts +0 -0
- /package/{dist/resiliency → packages/resiliency/dist}/gossip-health.js +0 -0
- /package/{dist/resiliency → packages/resiliency/dist}/health-monitor.d.ts +0 -0
- /package/{dist/resiliency → packages/resiliency/dist}/health-monitor.js +0 -0
- /package/{dist/resiliency → packages/resiliency/dist}/index.d.ts +0 -0
- /package/{dist/resiliency → packages/resiliency/dist}/index.js +0 -0
- /package/{dist/resiliency → packages/resiliency/dist}/leader-watchdog.d.ts +0 -0
- /package/{dist/resiliency → packages/resiliency/dist}/leader-watchdog.js +0 -0
- /package/{dist/resiliency → packages/resiliency/dist}/logger.d.ts +0 -0
- /package/{dist/resiliency → packages/resiliency/dist}/logger.js +0 -0
- /package/{dist/resiliency → packages/resiliency/dist}/memory-monitor.d.ts +0 -0
- /package/{dist/resiliency → packages/resiliency/dist}/metrics.d.ts +0 -0
- /package/{dist/resiliency → packages/resiliency/dist}/metrics.js +0 -0
- /package/{dist/resiliency → packages/resiliency/dist}/provider-context.js +0 -0
- /package/{dist/resiliency → packages/resiliency/dist}/stateless-lead.d.ts +0 -0
- /package/{dist/resiliency → packages/resiliency/dist}/stateless-lead.js +0 -0
- /package/{dist/resiliency → packages/resiliency/dist}/supervisor.d.ts +0 -0
- /package/{dist/resiliency → packages/resiliency/dist}/supervisor.js +0 -0
- /package/{dist/state → packages/state/dist}/agent-state.d.ts +0 -0
- /package/{dist/storage → packages/storage/dist}/adapter.js +0 -0
- /package/{dist/storage → packages/storage/dist}/dead-letter-queue.d.ts +0 -0
- /package/{dist/storage → packages/storage/dist}/dead-letter-queue.js +0 -0
- /package/{dist/storage → packages/storage/dist}/dlq-adapter.d.ts +0 -0
- /package/{dist/storage → packages/storage/dist}/dlq-adapter.js +0 -0
- /package/{dist/trajectory → packages/trajectory/dist}/integration.d.ts +0 -0
- /package/{dist/utils → packages/utils/dist}/command-resolver.d.ts +0 -0
- /package/{dist/utils → packages/utils/dist}/git-remote.d.ts +0 -0
- /package/{dist/utils → packages/utils/dist}/git-remote.js +0 -0
- /package/{dist/utils → packages/utils/dist}/logger.d.ts +0 -0
- /package/{dist/utils → packages/utils/dist}/logger.js +0 -0
- /package/{dist/utils → packages/utils/dist}/name-generator.d.ts +0 -0
- /package/{dist/utils → packages/utils/dist}/name-generator.js +0 -0
- /package/{dist/utils → packages/utils/dist}/precompiled-patterns.d.ts +0 -0
- /package/{dist/utils → packages/utils/dist}/precompiled-patterns.js +0 -0
- /package/{dist/utils → packages/utils/dist}/update-checker.d.ts +0 -0
- /package/{dist/utils → packages/utils/dist}/update-checker.js +0 -0
- /package/{dist/wrapper → packages/wrapper/dist}/auth-detection.d.ts +0 -0
- /package/{dist/wrapper → packages/wrapper/dist}/auth-detection.js +0 -0
- /package/{dist/wrapper → packages/wrapper/dist}/inbox.d.ts +0 -0
- /package/{dist/wrapper → packages/wrapper/dist}/inbox.js +0 -0
- /package/{dist/wrapper → packages/wrapper/dist}/prompt-composer.d.ts +0 -0
- /package/{dist/wrapper → packages/wrapper/dist}/prompt-composer.js +0 -0
- /package/{dist/utils → packages/wrapper/dist}/tmux-resolver.d.ts +0 -0
- /package/{dist/utils → packages/wrapper/dist}/tmux-resolver.js +0 -0
|
@@ -1,4806 +0,0 @@
|
|
|
1
|
-
import express from 'express';
|
|
2
|
-
import { WebSocketServer, WebSocket } from 'ws';
|
|
3
|
-
import http from 'http';
|
|
4
|
-
import path from 'path';
|
|
5
|
-
import fs from 'fs';
|
|
6
|
-
import os from 'os';
|
|
7
|
-
import crypto from 'crypto';
|
|
8
|
-
import { exec } from 'child_process';
|
|
9
|
-
import { fileURLToPath } from 'url';
|
|
10
|
-
import { SqliteStorageAdapter } from '../storage/sqlite-adapter.js';
|
|
11
|
-
import { RelayClient } from '../wrapper/client.js';
|
|
12
|
-
import { UserBridge } from './user-bridge.js';
|
|
13
|
-
import { computeNeedsAttention } from './needs-attention.js';
|
|
14
|
-
import { computeSystemMetrics, formatPrometheusMetrics } from './metrics.js';
|
|
15
|
-
import { MultiProjectClient } from '../bridge/multi-project-client.js';
|
|
16
|
-
import { AgentSpawner } from '../bridge/spawner.js';
|
|
17
|
-
import { listTrajectorySteps, getTrajectoryStatus, getTrajectoryHistory } from '../trajectory/integration.js';
|
|
18
|
-
import { loadTeamsConfig } from '../bridge/teams-config.js';
|
|
19
|
-
import { getMemoryMonitor } from '../resiliency/memory-monitor.js';
|
|
20
|
-
import { detectWorkspacePath } from '../utils/project-namespace.js';
|
|
21
|
-
import { startCLIAuth, getAuthSession, cancelAuthSession, submitAuthCode, completeAuthSession, getSupportedProviders, } from '../daemon/cli-auth.js';
|
|
22
|
-
import { HealthWorkerManager, getHealthPort } from '../health-worker-manager.js';
|
|
23
|
-
/**
|
|
24
|
-
* Initialize cloud persistence for session tracking.
|
|
25
|
-
*
|
|
26
|
-
* Activation modes:
|
|
27
|
-
* 1. Local dev: Set RELAY_CLOUD_ENABLED=true and DATABASE_URL
|
|
28
|
-
* 2. Cloud deployment: Plan-based - user must have Pro+ subscription
|
|
29
|
-
* (enforced at cloud API level when linking daemon or enabling workspace)
|
|
30
|
-
*
|
|
31
|
-
* Session persistence (Pro+ feature) enables:
|
|
32
|
-
* - [[SUMMARY]] blocks saved to PostgreSQL
|
|
33
|
-
* - [[SESSION_END]] markers for session tracking
|
|
34
|
-
* - Session recovery and agent handoff
|
|
35
|
-
*
|
|
36
|
-
* @see canUseSessionPersistence in services/planLimits.ts
|
|
37
|
-
*/
|
|
38
|
-
async function initCloudPersistence(workspaceId) {
|
|
39
|
-
// Local dev mode: simple env var check
|
|
40
|
-
// Cloud mode: plan check happens at API level (daemon linking, workspace config)
|
|
41
|
-
if (process.env.RELAY_CLOUD_ENABLED !== 'true') {
|
|
42
|
-
return null;
|
|
43
|
-
}
|
|
44
|
-
try {
|
|
45
|
-
// Dynamic import to avoid loading cloud dependencies unless enabled
|
|
46
|
-
const { getDb } = await import('../cloud/db/drizzle.js');
|
|
47
|
-
const { agentSessions, agentSummaries } = await import('../cloud/db/schema.js');
|
|
48
|
-
const { eq } = await import('drizzle-orm');
|
|
49
|
-
const db = getDb();
|
|
50
|
-
console.log('[dashboard] Cloud persistence enabled');
|
|
51
|
-
// Track active sessions per agent with timestamps for TTL cleanup
|
|
52
|
-
const SESSION_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
53
|
-
const MAX_SESSIONS = 10000;
|
|
54
|
-
const agentSessionIds = new Map();
|
|
55
|
-
// Track pending session creation to prevent race conditions
|
|
56
|
-
const pendingSessionCreation = new Map();
|
|
57
|
-
// Periodic cleanup of stale sessions (every 5 minutes)
|
|
58
|
-
const cleanupInterval = setInterval(() => {
|
|
59
|
-
const now = Date.now();
|
|
60
|
-
let evicted = 0;
|
|
61
|
-
for (const [name, { lastActivity }] of agentSessionIds.entries()) {
|
|
62
|
-
if (now - lastActivity > SESSION_TTL_MS) {
|
|
63
|
-
agentSessionIds.delete(name);
|
|
64
|
-
evicted++;
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
if (evicted > 0) {
|
|
68
|
-
console.log(`[cloud] Evicted ${evicted} stale session entries`);
|
|
69
|
-
}
|
|
70
|
-
}, 5 * 60 * 1000);
|
|
71
|
-
// Don't keep process alive just for cleanup
|
|
72
|
-
cleanupInterval.unref();
|
|
73
|
-
// Helper to get or create session with race protection
|
|
74
|
-
const getOrCreateSession = async (agentName) => {
|
|
75
|
-
// Check cache first
|
|
76
|
-
const cached = agentSessionIds.get(agentName);
|
|
77
|
-
if (cached) {
|
|
78
|
-
return cached.id;
|
|
79
|
-
}
|
|
80
|
-
// Check if creation is already in progress
|
|
81
|
-
const pending = pendingSessionCreation.get(agentName);
|
|
82
|
-
if (pending) {
|
|
83
|
-
return pending;
|
|
84
|
-
}
|
|
85
|
-
// Create session with mutex
|
|
86
|
-
const creationPromise = (async () => {
|
|
87
|
-
try {
|
|
88
|
-
// Double-check cache after acquiring "lock"
|
|
89
|
-
const rechecked = agentSessionIds.get(agentName);
|
|
90
|
-
if (rechecked) {
|
|
91
|
-
return rechecked.id;
|
|
92
|
-
}
|
|
93
|
-
// Enforce max size - evict oldest if needed
|
|
94
|
-
if (agentSessionIds.size >= MAX_SESSIONS) {
|
|
95
|
-
let oldest = null;
|
|
96
|
-
for (const [name, { lastActivity }] of agentSessionIds.entries()) {
|
|
97
|
-
if (!oldest || lastActivity < oldest.time) {
|
|
98
|
-
oldest = { name, time: lastActivity };
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
if (oldest) {
|
|
102
|
-
agentSessionIds.delete(oldest.name);
|
|
103
|
-
console.log(`[cloud] Evicted oldest session for ${oldest.name} (max sessions reached)`);
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
// Create a new session with null safety
|
|
107
|
-
const result = await db.insert(agentSessions).values({
|
|
108
|
-
workspaceId,
|
|
109
|
-
agentName,
|
|
110
|
-
status: 'active',
|
|
111
|
-
startedAt: new Date(),
|
|
112
|
-
}).returning();
|
|
113
|
-
const session = result[0];
|
|
114
|
-
if (!session) {
|
|
115
|
-
throw new Error(`Failed to create session for agent ${agentName}`);
|
|
116
|
-
}
|
|
117
|
-
// Update cache
|
|
118
|
-
agentSessionIds.set(agentName, { id: session.id, lastActivity: Date.now() });
|
|
119
|
-
return session.id;
|
|
120
|
-
}
|
|
121
|
-
finally {
|
|
122
|
-
pendingSessionCreation.delete(agentName);
|
|
123
|
-
}
|
|
124
|
-
})();
|
|
125
|
-
pendingSessionCreation.set(agentName, creationPromise);
|
|
126
|
-
return creationPromise;
|
|
127
|
-
};
|
|
128
|
-
return {
|
|
129
|
-
onSummary: async (agentName, event) => {
|
|
130
|
-
try {
|
|
131
|
-
// Get or create session with race protection
|
|
132
|
-
const sessionId = await getOrCreateSession(agentName);
|
|
133
|
-
// Update activity timestamp
|
|
134
|
-
agentSessionIds.set(agentName, { id: sessionId, lastActivity: Date.now() });
|
|
135
|
-
// Insert summary
|
|
136
|
-
await db.insert(agentSummaries).values({
|
|
137
|
-
sessionId,
|
|
138
|
-
agentName,
|
|
139
|
-
summary: event.summary,
|
|
140
|
-
createdAt: new Date(),
|
|
141
|
-
});
|
|
142
|
-
console.log(`[cloud] Saved summary for ${agentName}: ${event.summary.currentTask || 'no task'}`);
|
|
143
|
-
}
|
|
144
|
-
catch (err) {
|
|
145
|
-
console.error(`[cloud] Failed to save summary for ${agentName}:`, err);
|
|
146
|
-
}
|
|
147
|
-
},
|
|
148
|
-
onSessionEnd: async (agentName, event) => {
|
|
149
|
-
try {
|
|
150
|
-
const cached = agentSessionIds.get(agentName);
|
|
151
|
-
if (cached) {
|
|
152
|
-
// Update session as ended
|
|
153
|
-
await db.update(agentSessions)
|
|
154
|
-
.set({
|
|
155
|
-
status: 'ended',
|
|
156
|
-
endedAt: new Date(),
|
|
157
|
-
endMarker: event.marker,
|
|
158
|
-
})
|
|
159
|
-
.where(eq(agentSessions.id, cached.id));
|
|
160
|
-
agentSessionIds.delete(agentName);
|
|
161
|
-
console.log(`[cloud] Session ended for ${agentName}: ${event.marker.summary || 'no summary'}`);
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
catch (err) {
|
|
165
|
-
console.error(`[cloud] Failed to end session for ${agentName}:`, err);
|
|
166
|
-
}
|
|
167
|
-
},
|
|
168
|
-
destroy: () => {
|
|
169
|
-
clearInterval(cleanupInterval);
|
|
170
|
-
agentSessionIds.clear();
|
|
171
|
-
pendingSessionCreation.clear();
|
|
172
|
-
console.log('[cloud] Cloud persistence handler destroyed');
|
|
173
|
-
},
|
|
174
|
-
};
|
|
175
|
-
}
|
|
176
|
-
catch (err) {
|
|
177
|
-
console.warn('[dashboard] Cloud persistence not available:', err);
|
|
178
|
-
return null;
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
182
|
-
const __dirname = path.dirname(__filename);
|
|
183
|
-
/**
|
|
184
|
-
* Search for files in a directory matching a query pattern.
|
|
185
|
-
* Uses a simple recursive search with common ignore patterns.
|
|
186
|
-
*/
|
|
187
|
-
async function searchFiles(rootDir, query, limit) {
|
|
188
|
-
const results = [];
|
|
189
|
-
const queryLower = query.toLowerCase();
|
|
190
|
-
// Directories to ignore
|
|
191
|
-
const ignoreDirs = new Set([
|
|
192
|
-
'node_modules', '.git', 'dist', 'build', '.next', 'coverage',
|
|
193
|
-
'__pycache__', '.venv', 'venv', '.cache', '.turbo', '.vercel',
|
|
194
|
-
'.nuxt', '.output', 'vendor', 'target', '.idea', '.vscode'
|
|
195
|
-
]);
|
|
196
|
-
// File patterns to ignore
|
|
197
|
-
const ignorePatterns = [
|
|
198
|
-
/\.lock$/,
|
|
199
|
-
/\.log$/,
|
|
200
|
-
/\.min\.(js|css)$/,
|
|
201
|
-
/\.map$/,
|
|
202
|
-
/\.d\.ts$/,
|
|
203
|
-
/\.pyc$/,
|
|
204
|
-
];
|
|
205
|
-
const shouldIgnore = (name, isDir) => {
|
|
206
|
-
if (isDir)
|
|
207
|
-
return ignoreDirs.has(name);
|
|
208
|
-
return ignorePatterns.some(pattern => pattern.test(name));
|
|
209
|
-
};
|
|
210
|
-
const matchesQuery = (filePath, fileName) => {
|
|
211
|
-
if (!query)
|
|
212
|
-
return true;
|
|
213
|
-
const pathLower = filePath.toLowerCase();
|
|
214
|
-
const nameLower = fileName.toLowerCase();
|
|
215
|
-
// If query contains '/', match against full path
|
|
216
|
-
if (queryLower.includes('/')) {
|
|
217
|
-
return pathLower.includes(queryLower);
|
|
218
|
-
}
|
|
219
|
-
// Otherwise match against file name or path segments
|
|
220
|
-
return nameLower.includes(queryLower) || pathLower.includes(queryLower);
|
|
221
|
-
};
|
|
222
|
-
const searchDir = async (dir, relativePath = '') => {
|
|
223
|
-
if (results.length >= limit)
|
|
224
|
-
return;
|
|
225
|
-
try {
|
|
226
|
-
const entries = await fs.promises.readdir(dir, { withFileTypes: true });
|
|
227
|
-
// Sort: directories first, then alphabetically
|
|
228
|
-
entries.sort((a, b) => {
|
|
229
|
-
if (a.isDirectory() !== b.isDirectory()) {
|
|
230
|
-
return a.isDirectory() ? -1 : 1;
|
|
231
|
-
}
|
|
232
|
-
return a.name.localeCompare(b.name);
|
|
233
|
-
});
|
|
234
|
-
for (const entry of entries) {
|
|
235
|
-
if (results.length >= limit)
|
|
236
|
-
break;
|
|
237
|
-
const entryPath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
|
|
238
|
-
const fullPath = path.join(dir, entry.name);
|
|
239
|
-
if (shouldIgnore(entry.name, entry.isDirectory()))
|
|
240
|
-
continue;
|
|
241
|
-
if (matchesQuery(entryPath, entry.name)) {
|
|
242
|
-
results.push({
|
|
243
|
-
path: entryPath,
|
|
244
|
-
name: entry.name,
|
|
245
|
-
isDirectory: entry.isDirectory(),
|
|
246
|
-
});
|
|
247
|
-
}
|
|
248
|
-
// Recurse into directories
|
|
249
|
-
if (entry.isDirectory() && results.length < limit) {
|
|
250
|
-
await searchDir(fullPath, entryPath);
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
catch (err) {
|
|
255
|
-
// Ignore permission errors, etc.
|
|
256
|
-
console.warn(`[searchFiles] Error reading ${dir}:`, err);
|
|
257
|
-
}
|
|
258
|
-
};
|
|
259
|
-
await searchDir(rootDir);
|
|
260
|
-
return results;
|
|
261
|
-
}
|
|
262
|
-
export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPathArg) {
|
|
263
|
-
// Handle overloaded signatures
|
|
264
|
-
const options = typeof portOrOptions === 'number'
|
|
265
|
-
? { port: portOrOptions, dataDir: dataDirArg, teamDir: teamDirArg, dbPath: dbPathArg }
|
|
266
|
-
: portOrOptions;
|
|
267
|
-
const { port, dataDir, teamDir, dbPath, enableSpawner, projectRoot, tmuxSession, onMarkSpawning, onClearSpawning } = options;
|
|
268
|
-
console.log('Starting dashboard...');
|
|
269
|
-
const disableStorage = process.env.RELAY_DISABLE_STORAGE === 'true';
|
|
270
|
-
const storage = disableStorage
|
|
271
|
-
? undefined
|
|
272
|
-
: new SqliteStorageAdapter({
|
|
273
|
-
dbPath: dbPath ?? path.join(dataDir, 'dashboard.db'),
|
|
274
|
-
});
|
|
275
|
-
const defaultWorkspaceId = process.env.RELAY_WORKSPACE_ID ?? process.env.AGENT_RELAY_WORKSPACE_ID;
|
|
276
|
-
const resolveWorkspaceId = (req) => {
|
|
277
|
-
const fromQuery = req.query.workspaceId;
|
|
278
|
-
const fromBody = req.body?.workspaceId;
|
|
279
|
-
const fromHeader = req.headers['x-workspace-id'];
|
|
280
|
-
return fromQuery || fromBody || fromHeader || defaultWorkspaceId;
|
|
281
|
-
};
|
|
282
|
-
const loadChannelRecords = async (workspaceId) => {
|
|
283
|
-
const map = new Map();
|
|
284
|
-
if (!storage) {
|
|
285
|
-
return map;
|
|
286
|
-
}
|
|
287
|
-
const stored = await storage.getMessages({ order: 'asc' });
|
|
288
|
-
const ensureRecord = (id) => {
|
|
289
|
-
let record = map.get(id);
|
|
290
|
-
if (!record) {
|
|
291
|
-
record = {
|
|
292
|
-
id,
|
|
293
|
-
visibility: 'public',
|
|
294
|
-
status: 'active',
|
|
295
|
-
lastActivityAt: 0,
|
|
296
|
-
members: new Set(),
|
|
297
|
-
};
|
|
298
|
-
if (id.startsWith('dm:')) {
|
|
299
|
-
const participants = id.split(':').slice(1).filter(Boolean);
|
|
300
|
-
if (participants.length > 0) {
|
|
301
|
-
participants.forEach((participant) => record.members.add(participant));
|
|
302
|
-
record.dmParticipants = participants;
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
map.set(id, record);
|
|
306
|
-
}
|
|
307
|
-
return record;
|
|
308
|
-
};
|
|
309
|
-
const addMember = (record, member) => {
|
|
310
|
-
if (!member)
|
|
311
|
-
return;
|
|
312
|
-
record.members.add(member);
|
|
313
|
-
};
|
|
314
|
-
for (const msg of stored) {
|
|
315
|
-
const target = msg.to;
|
|
316
|
-
if (!target || (!target.startsWith('#') && !target.startsWith('dm:'))) {
|
|
317
|
-
continue;
|
|
318
|
-
}
|
|
319
|
-
const data = msg.data;
|
|
320
|
-
const messageWorkspaceId = typeof data?._workspaceId === 'string' ? data._workspaceId : undefined;
|
|
321
|
-
if (workspaceId && messageWorkspaceId && messageWorkspaceId !== workspaceId) {
|
|
322
|
-
continue;
|
|
323
|
-
}
|
|
324
|
-
const record = ensureRecord(target);
|
|
325
|
-
const timestamp = typeof msg.ts === 'number' ? msg.ts : Date.now();
|
|
326
|
-
const channelCreate = data?._channelCreate;
|
|
327
|
-
if (channelCreate) {
|
|
328
|
-
record.createdAt = record.createdAt ?? timestamp;
|
|
329
|
-
record.createdBy = channelCreate.createdBy ?? record.createdBy;
|
|
330
|
-
if (channelCreate.description) {
|
|
331
|
-
record.description = String(channelCreate.description);
|
|
332
|
-
}
|
|
333
|
-
record.visibility = channelCreate.isPrivate ? 'private' : 'public';
|
|
334
|
-
}
|
|
335
|
-
const stateChange = data?._channelState;
|
|
336
|
-
if (stateChange) {
|
|
337
|
-
record.status = stateChange === 'archived' ? 'archived' : 'active';
|
|
338
|
-
record.lastActivityAt = Math.max(record.lastActivityAt, timestamp);
|
|
339
|
-
}
|
|
340
|
-
const membership = data?._channelMembership;
|
|
341
|
-
if (membership?.member) {
|
|
342
|
-
if (membership.action === 'leave') {
|
|
343
|
-
record.members.delete(membership.member);
|
|
344
|
-
}
|
|
345
|
-
else {
|
|
346
|
-
addMember(record, membership.member);
|
|
347
|
-
}
|
|
348
|
-
record.lastActivityAt = Math.max(record.lastActivityAt, timestamp);
|
|
349
|
-
}
|
|
350
|
-
const isChannelMessage = Boolean(data?._isChannelMessage);
|
|
351
|
-
if (isChannelMessage) {
|
|
352
|
-
addMember(record, msg.from);
|
|
353
|
-
record.lastActivityAt = Math.max(record.lastActivityAt, timestamp);
|
|
354
|
-
record.lastMessage = {
|
|
355
|
-
content: msg.body,
|
|
356
|
-
from: msg.from || '__system__',
|
|
357
|
-
timestamp: new Date(timestamp).toISOString(),
|
|
358
|
-
};
|
|
359
|
-
if (target.startsWith('dm:') && !record.dmParticipants) {
|
|
360
|
-
const participants = target.split(':').slice(1).filter(Boolean);
|
|
361
|
-
if (participants.length > 0) {
|
|
362
|
-
participants.forEach((participant) => record.members.add(participant));
|
|
363
|
-
record.dmParticipants = participants;
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
return map;
|
|
369
|
-
};
|
|
370
|
-
const loadPersistedChannelsForUser = async (username, workspaceId) => {
|
|
371
|
-
const channelMap = await loadChannelRecords(workspaceId);
|
|
372
|
-
const result = [];
|
|
373
|
-
for (const record of channelMap.values()) {
|
|
374
|
-
if (record.status === 'archived') {
|
|
375
|
-
continue;
|
|
376
|
-
}
|
|
377
|
-
if (record.members.has(username)) {
|
|
378
|
-
result.push(record.id);
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
if (!result.includes('#general')) {
|
|
382
|
-
result.unshift('#general');
|
|
383
|
-
}
|
|
384
|
-
return result;
|
|
385
|
-
};
|
|
386
|
-
const persistChannelMembershipEvent = async (channel, member, action, options) => {
|
|
387
|
-
if (!storage)
|
|
388
|
-
return;
|
|
389
|
-
const data = {
|
|
390
|
-
_channelMembership: {
|
|
391
|
-
member,
|
|
392
|
-
action,
|
|
393
|
-
invitedBy: options?.invitedBy,
|
|
394
|
-
},
|
|
395
|
-
};
|
|
396
|
-
const workspaceToStore = options?.workspaceId ?? defaultWorkspaceId;
|
|
397
|
-
if (workspaceToStore) {
|
|
398
|
-
data._workspaceId = workspaceToStore;
|
|
399
|
-
}
|
|
400
|
-
await storage.saveMessage({
|
|
401
|
-
id: `channel-membership-${crypto.randomUUID()}`,
|
|
402
|
-
ts: Date.now(),
|
|
403
|
-
from: '__system__',
|
|
404
|
-
to: channel,
|
|
405
|
-
topic: undefined,
|
|
406
|
-
kind: 'state',
|
|
407
|
-
body: `${action}:${member}`,
|
|
408
|
-
data,
|
|
409
|
-
status: 'read',
|
|
410
|
-
is_urgent: false,
|
|
411
|
-
is_broadcast: true,
|
|
412
|
-
}).catch((err) => {
|
|
413
|
-
console.error('[channels] Failed to persist membership event', err);
|
|
414
|
-
});
|
|
415
|
-
await notifyDaemonOfMembershipUpdate(channel, member, action, workspaceToStore).catch((err) => {
|
|
416
|
-
console.error('[channels] Failed to notify daemon of membership update', err);
|
|
417
|
-
});
|
|
418
|
-
};
|
|
419
|
-
// Initialize spawner if enabled
|
|
420
|
-
// Use detectWorkspacePath to find the actual repo directory in cloud workspaces
|
|
421
|
-
const workspacePath = detectWorkspacePath(projectRoot || dataDir);
|
|
422
|
-
console.log(`[dashboard] Workspace path: ${workspacePath}`);
|
|
423
|
-
// Pass dashboard port to spawner so spawned agents can call spawn/release APIs for nested spawning
|
|
424
|
-
// Also pass spawn tracking callbacks so messages can be queued before HELLO completes
|
|
425
|
-
const spawner = enableSpawner
|
|
426
|
-
? new AgentSpawner({
|
|
427
|
-
projectRoot: workspacePath,
|
|
428
|
-
tmuxSession,
|
|
429
|
-
dashboardPort: port,
|
|
430
|
-
onMarkSpawning,
|
|
431
|
-
onClearSpawning,
|
|
432
|
-
})
|
|
433
|
-
: undefined;
|
|
434
|
-
// Initialize cloud persistence and memory monitoring if enabled (RELAY_CLOUD_ENABLED=true)
|
|
435
|
-
if (spawner) {
|
|
436
|
-
// Use workspace ID from env or generate from project root
|
|
437
|
-
const workspaceId = process.env.RELAY_WORKSPACE_ID ||
|
|
438
|
-
crypto.createHash('sha256').update(projectRoot || dataDir).digest('hex').slice(0, 36);
|
|
439
|
-
initCloudPersistence(workspaceId).then((cloudHandler) => {
|
|
440
|
-
if (cloudHandler) {
|
|
441
|
-
spawner.setCloudPersistence(cloudHandler);
|
|
442
|
-
}
|
|
443
|
-
}).catch((err) => {
|
|
444
|
-
console.warn('[dashboard] Failed to initialize cloud persistence:', err);
|
|
445
|
-
});
|
|
446
|
-
// Initialize memory monitoring for cloud deployments
|
|
447
|
-
// Memory monitoring is enabled by default when cloud is enabled
|
|
448
|
-
if (process.env.RELAY_CLOUD_ENABLED === 'true' || process.env.RELAY_MEMORY_MONITORING === 'true') {
|
|
449
|
-
try {
|
|
450
|
-
const memoryMonitor = getMemoryMonitor({
|
|
451
|
-
checkIntervalMs: 10000, // Check every 10 seconds
|
|
452
|
-
enableTrendAnalysis: true,
|
|
453
|
-
enableProactiveAlerts: true,
|
|
454
|
-
});
|
|
455
|
-
memoryMonitor.start();
|
|
456
|
-
console.log('[dashboard] Memory monitoring enabled');
|
|
457
|
-
// Register existing workers with memory monitor
|
|
458
|
-
const workers = spawner.getActiveWorkers();
|
|
459
|
-
for (const worker of workers) {
|
|
460
|
-
if (worker.pid) {
|
|
461
|
-
memoryMonitor.register(worker.name, worker.pid);
|
|
462
|
-
}
|
|
463
|
-
}
|
|
464
|
-
}
|
|
465
|
-
catch (err) {
|
|
466
|
-
console.warn('[dashboard] Failed to initialize memory monitoring:', err);
|
|
467
|
-
}
|
|
468
|
-
}
|
|
469
|
-
}
|
|
470
|
-
process.on('uncaughtException', (err) => {
|
|
471
|
-
console.error('Uncaught Exception:', err);
|
|
472
|
-
});
|
|
473
|
-
process.on('unhandledRejection', (reason, promise) => {
|
|
474
|
-
console.error('Unhandled Rejection at:', promise, 'reason:', reason);
|
|
475
|
-
});
|
|
476
|
-
const app = express();
|
|
477
|
-
const server = http.createServer(app);
|
|
478
|
-
// Use noServer mode to manually route upgrade requests
|
|
479
|
-
// This prevents the bug where multiple WebSocketServers attached to the same
|
|
480
|
-
// HTTP server cause conflicts - each one's upgrade handler fires and the ones
|
|
481
|
-
// that don't match the path call abortHandshake(400), writing raw HTTP to the socket
|
|
482
|
-
const wss = new WebSocketServer({
|
|
483
|
-
noServer: true,
|
|
484
|
-
perMessageDeflate: false,
|
|
485
|
-
skipUTF8Validation: true,
|
|
486
|
-
maxPayload: 100 * 1024 * 1024 // 100MB
|
|
487
|
-
});
|
|
488
|
-
const wssBridge = new WebSocketServer({
|
|
489
|
-
noServer: true,
|
|
490
|
-
perMessageDeflate: false,
|
|
491
|
-
skipUTF8Validation: true,
|
|
492
|
-
maxPayload: 100 * 1024 * 1024
|
|
493
|
-
});
|
|
494
|
-
const wssLogs = new WebSocketServer({
|
|
495
|
-
noServer: true,
|
|
496
|
-
perMessageDeflate: false,
|
|
497
|
-
skipUTF8Validation: true,
|
|
498
|
-
maxPayload: 100 * 1024 * 1024
|
|
499
|
-
});
|
|
500
|
-
const wssPresence = new WebSocketServer({
|
|
501
|
-
noServer: true,
|
|
502
|
-
perMessageDeflate: false,
|
|
503
|
-
skipUTF8Validation: true,
|
|
504
|
-
maxPayload: 1024 * 1024 // 1MB - presence messages are small
|
|
505
|
-
});
|
|
506
|
-
// Track log subscriptions: agentName -> Set of WebSocket clients
|
|
507
|
-
const logSubscriptions = new Map();
|
|
508
|
-
// Track alive status for ping/pong keepalive on main dashboard connections
|
|
509
|
-
// This prevents TCP/proxy timeouts from killing idle workspace connections
|
|
510
|
-
const mainClientAlive = new WeakMap();
|
|
511
|
-
// Track alive status for ping/pong keepalive on bridge connections
|
|
512
|
-
const bridgeClientAlive = new WeakMap();
|
|
513
|
-
// Ping interval for main dashboard WebSocket connections (30 seconds)
|
|
514
|
-
// Aligns with heartbeat timeout (5s heartbeat * 6 multiplier = 30s)
|
|
515
|
-
const MAIN_PING_INTERVAL_MS = 30000;
|
|
516
|
-
const mainPingInterval = setInterval(() => {
|
|
517
|
-
wss.clients.forEach((ws) => {
|
|
518
|
-
if (mainClientAlive.get(ws) === false) {
|
|
519
|
-
// Client didn't respond to last ping - close gracefully
|
|
520
|
-
console.log('[dashboard] Main WebSocket client unresponsive, closing gracefully');
|
|
521
|
-
ws.close(1000, 'unresponsive');
|
|
522
|
-
return;
|
|
523
|
-
}
|
|
524
|
-
// Mark as not alive until we get a pong
|
|
525
|
-
mainClientAlive.set(ws, false);
|
|
526
|
-
ws.ping();
|
|
527
|
-
});
|
|
528
|
-
}, MAIN_PING_INTERVAL_MS);
|
|
529
|
-
// Ping interval for bridge WebSocket connections (30 seconds)
|
|
530
|
-
const BRIDGE_PING_INTERVAL_MS = 30000;
|
|
531
|
-
const bridgePingInterval = setInterval(() => {
|
|
532
|
-
wssBridge.clients.forEach((ws) => {
|
|
533
|
-
if (bridgeClientAlive.get(ws) === false) {
|
|
534
|
-
console.log('[dashboard] Bridge WebSocket client unresponsive, closing gracefully');
|
|
535
|
-
ws.close(1000, 'unresponsive');
|
|
536
|
-
return;
|
|
537
|
-
}
|
|
538
|
-
bridgeClientAlive.set(ws, false);
|
|
539
|
-
ws.ping();
|
|
540
|
-
});
|
|
541
|
-
}, BRIDGE_PING_INTERVAL_MS);
|
|
542
|
-
// Clean up ping intervals on server close
|
|
543
|
-
wss.on('close', () => {
|
|
544
|
-
clearInterval(mainPingInterval);
|
|
545
|
-
});
|
|
546
|
-
wssBridge.on('close', () => {
|
|
547
|
-
clearInterval(bridgePingInterval);
|
|
548
|
-
});
|
|
549
|
-
const onlineUsers = new Map();
|
|
550
|
-
// Validation helpers for presence
|
|
551
|
-
const isValidUsername = (username) => {
|
|
552
|
-
if (typeof username !== 'string')
|
|
553
|
-
return false;
|
|
554
|
-
// Username should be 1-39 chars, alphanumeric with hyphens (GitHub username rules)
|
|
555
|
-
return /^[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,37}[a-zA-Z0-9])?$/.test(username);
|
|
556
|
-
};
|
|
557
|
-
const isValidAvatarUrl = (url) => {
|
|
558
|
-
if (url === undefined || url === null)
|
|
559
|
-
return true;
|
|
560
|
-
if (typeof url !== 'string')
|
|
561
|
-
return false;
|
|
562
|
-
// Must be a valid HTTPS URL from GitHub or similar known providers
|
|
563
|
-
try {
|
|
564
|
-
const parsed = new URL(url);
|
|
565
|
-
return parsed.protocol === 'https:' &&
|
|
566
|
-
(parsed.hostname === 'avatars.githubusercontent.com' ||
|
|
567
|
-
parsed.hostname === 'github.com' ||
|
|
568
|
-
parsed.hostname.endsWith('.githubusercontent.com'));
|
|
569
|
-
}
|
|
570
|
-
catch {
|
|
571
|
-
return false;
|
|
572
|
-
}
|
|
573
|
-
};
|
|
574
|
-
// Manually handle upgrade requests and route to correct WebSocketServer
|
|
575
|
-
server.on('upgrade', (request, socket, head) => {
|
|
576
|
-
const pathname = new URL(request.url || '', `http://${request.headers.host}`).pathname;
|
|
577
|
-
if (pathname === '/ws') {
|
|
578
|
-
wss.handleUpgrade(request, socket, head, (ws) => {
|
|
579
|
-
wss.emit('connection', ws, request);
|
|
580
|
-
});
|
|
581
|
-
}
|
|
582
|
-
else if (pathname === '/ws/bridge') {
|
|
583
|
-
wssBridge.handleUpgrade(request, socket, head, (ws) => {
|
|
584
|
-
wssBridge.emit('connection', ws, request);
|
|
585
|
-
});
|
|
586
|
-
}
|
|
587
|
-
else if (pathname === '/ws/logs' || pathname.startsWith('/ws/logs/')) {
|
|
588
|
-
wssLogs.handleUpgrade(request, socket, head, (ws) => {
|
|
589
|
-
wssLogs.emit('connection', ws, request);
|
|
590
|
-
});
|
|
591
|
-
}
|
|
592
|
-
else if (pathname === '/ws/presence') {
|
|
593
|
-
wssPresence.handleUpgrade(request, socket, head, (ws) => {
|
|
594
|
-
wssPresence.emit('connection', ws, request);
|
|
595
|
-
});
|
|
596
|
-
}
|
|
597
|
-
else {
|
|
598
|
-
// Unknown path - destroy socket
|
|
599
|
-
socket.destroy();
|
|
600
|
-
}
|
|
601
|
-
});
|
|
602
|
-
// Server-level error handlers
|
|
603
|
-
wss.on('error', (err) => {
|
|
604
|
-
console.error('[dashboard] WebSocket server error:', err);
|
|
605
|
-
});
|
|
606
|
-
wssBridge.on('error', (err) => {
|
|
607
|
-
console.error('[dashboard] Bridge WebSocket server error:', err);
|
|
608
|
-
});
|
|
609
|
-
wssLogs.on('error', (err) => {
|
|
610
|
-
console.error('[dashboard] Logs WebSocket server error:', err);
|
|
611
|
-
});
|
|
612
|
-
wssPresence.on('error', (err) => {
|
|
613
|
-
console.error('[dashboard] Presence WebSocket server error:', err);
|
|
614
|
-
});
|
|
615
|
-
if (storage) {
|
|
616
|
-
await storage.init();
|
|
617
|
-
}
|
|
618
|
-
// Request logger for debugging
|
|
619
|
-
app.use((req, res, next) => {
|
|
620
|
-
if (req.path.startsWith('/api/channels')) {
|
|
621
|
-
console.log(`[dashboard] ${req.method} ${req.path} - incoming request`);
|
|
622
|
-
}
|
|
623
|
-
next();
|
|
624
|
-
});
|
|
625
|
-
// Increase JSON body limit for base64 image uploads (10MB)
|
|
626
|
-
app.use(express.json({ limit: '10mb' }));
|
|
627
|
-
// Create attachments directory in user's home directory (~/.relay/attachments)
|
|
628
|
-
// This keeps attachments out of source control while still accessible to agents
|
|
629
|
-
const attachmentsDir = path.join(os.homedir(), '.relay', 'attachments');
|
|
630
|
-
if (!fs.existsSync(attachmentsDir)) {
|
|
631
|
-
fs.mkdirSync(attachmentsDir, { recursive: true });
|
|
632
|
-
}
|
|
633
|
-
// Also keep uploads dir for backwards compatibility (URL-based serving)
|
|
634
|
-
const uploadsDir = path.join(dataDir, 'uploads');
|
|
635
|
-
if (!fs.existsSync(uploadsDir)) {
|
|
636
|
-
fs.mkdirSync(uploadsDir, { recursive: true });
|
|
637
|
-
}
|
|
638
|
-
// Auto-evict old attachments (older than 7 days)
|
|
639
|
-
const ATTACHMENT_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000; // 7 days
|
|
640
|
-
const evictOldAttachments = async () => {
|
|
641
|
-
try {
|
|
642
|
-
const files = await fs.promises.readdir(attachmentsDir);
|
|
643
|
-
const now = Date.now();
|
|
644
|
-
let evictedCount = 0;
|
|
645
|
-
for (const file of files) {
|
|
646
|
-
const filePath = path.join(attachmentsDir, file);
|
|
647
|
-
try {
|
|
648
|
-
const stat = await fs.promises.stat(filePath);
|
|
649
|
-
if (stat.isFile() && (now - stat.mtimeMs) > ATTACHMENT_MAX_AGE_MS) {
|
|
650
|
-
await fs.promises.unlink(filePath);
|
|
651
|
-
evictedCount++;
|
|
652
|
-
}
|
|
653
|
-
}
|
|
654
|
-
catch (_err) {
|
|
655
|
-
// Ignore errors for individual files (may have been deleted)
|
|
656
|
-
}
|
|
657
|
-
}
|
|
658
|
-
if (evictedCount > 0) {
|
|
659
|
-
console.log(`[dashboard] Evicted ${evictedCount} old attachment(s)`);
|
|
660
|
-
}
|
|
661
|
-
}
|
|
662
|
-
catch (err) {
|
|
663
|
-
console.error('[dashboard] Failed to evict old attachments:', err);
|
|
664
|
-
}
|
|
665
|
-
};
|
|
666
|
-
// Run eviction on startup and every hour
|
|
667
|
-
evictOldAttachments();
|
|
668
|
-
const evictionInterval = setInterval(evictOldAttachments, 60 * 60 * 1000); // 1 hour
|
|
669
|
-
// Clean up interval on process exit
|
|
670
|
-
process.on('beforeExit', () => {
|
|
671
|
-
clearInterval(evictionInterval);
|
|
672
|
-
});
|
|
673
|
-
// Serve uploaded files statically
|
|
674
|
-
app.use('/uploads', express.static(uploadsDir));
|
|
675
|
-
// Serve attachments from ~/.relay/attachments
|
|
676
|
-
app.use('/attachments', express.static(attachmentsDir));
|
|
677
|
-
// In-memory attachment registry (for current session)
|
|
678
|
-
// Attachments are also stored on disk, so this is just for quick lookups
|
|
679
|
-
const attachmentRegistry = new Map();
|
|
680
|
-
// Serve dashboard static files at root (built with `next build` in src/dashboard)
|
|
681
|
-
// __dirname is dist/dashboard-server, dashboard is at ../dashboard/out (relative to dist)
|
|
682
|
-
// But in source it's at ../dashboard/out (relative to src/dashboard-server)
|
|
683
|
-
const dashboardDistDir = path.join(__dirname, '..', 'dashboard', 'out');
|
|
684
|
-
const dashboardSourceDir = path.join(__dirname, '..', '..', 'src', 'dashboard', 'out');
|
|
685
|
-
// Check which path exists (dist or src)
|
|
686
|
-
const dashboardDir = fs.existsSync(dashboardDistDir) ? dashboardDistDir : dashboardSourceDir;
|
|
687
|
-
if (fs.existsSync(dashboardDir)) {
|
|
688
|
-
console.log(`[dashboard] Serving from: ${dashboardDir}`);
|
|
689
|
-
// Serve Next.js static export with .html extension handling
|
|
690
|
-
app.use(express.static(dashboardDir, { extensions: ['html'] }));
|
|
691
|
-
// Fallback for Next.js pages (e.g., /metrics -> /metrics.html)
|
|
692
|
-
// These are needed when a route exists as both a directory and .html file
|
|
693
|
-
app.get('/metrics', (req, res) => {
|
|
694
|
-
res.sendFile(path.join(dashboardDir, 'metrics.html'));
|
|
695
|
-
});
|
|
696
|
-
app.get('/app', (req, res) => {
|
|
697
|
-
res.sendFile(path.join(dashboardDir, 'app.html'));
|
|
698
|
-
});
|
|
699
|
-
}
|
|
700
|
-
else {
|
|
701
|
-
console.error('[dashboard] Dashboard not found at:', dashboardDistDir, 'or', dashboardSourceDir);
|
|
702
|
-
}
|
|
703
|
-
// Relay clients for sending messages from dashboard
|
|
704
|
-
// Map of senderName -> RelayClient for per-user connections
|
|
705
|
-
const socketPath = path.join(dataDir, 'relay.sock');
|
|
706
|
-
const relayClients = new Map();
|
|
707
|
-
const notifyDaemonOfMembershipUpdate = async (channel, member, action, workspaceId) => {
|
|
708
|
-
const client = await getRelayClient('Dashboard');
|
|
709
|
-
if (!client || client.state !== 'READY') {
|
|
710
|
-
return;
|
|
711
|
-
}
|
|
712
|
-
client.sendMessage('_router', '', 'state', {
|
|
713
|
-
_channelMembershipUpdate: {
|
|
714
|
-
channel,
|
|
715
|
-
member,
|
|
716
|
-
action,
|
|
717
|
-
workspaceId,
|
|
718
|
-
},
|
|
719
|
-
});
|
|
720
|
-
};
|
|
721
|
-
// Track pending client connections to prevent race conditions
|
|
722
|
-
const pendingConnections = new Map();
|
|
723
|
-
// Get or create a relay client for a specific sender
|
|
724
|
-
const getRelayClient = async (senderName = 'Dashboard', entityType) => {
|
|
725
|
-
// Check if we already have a connected client for this sender
|
|
726
|
-
const existing = relayClients.get(senderName);
|
|
727
|
-
if (existing && existing.state === 'READY') {
|
|
728
|
-
return existing;
|
|
729
|
-
}
|
|
730
|
-
// Check if there's already a pending connection for this sender
|
|
731
|
-
const pending = pendingConnections.get(senderName);
|
|
732
|
-
if (pending) {
|
|
733
|
-
return pending;
|
|
734
|
-
}
|
|
735
|
-
// Only attempt connection if socket exists (daemon is running)
|
|
736
|
-
if (!fs.existsSync(socketPath)) {
|
|
737
|
-
console.log('[dashboard] Relay socket not found, messaging disabled');
|
|
738
|
-
return undefined;
|
|
739
|
-
}
|
|
740
|
-
// Create connection promise to prevent race conditions
|
|
741
|
-
const connectionPromise = (async () => {
|
|
742
|
-
// Create new client for this sender
|
|
743
|
-
// Default to 'user' entityType for non-Dashboard senders (human users)
|
|
744
|
-
const resolvedEntityType = entityType ?? (senderName === 'Dashboard' ? undefined : 'user');
|
|
745
|
-
const client = new RelayClient({
|
|
746
|
-
socketPath,
|
|
747
|
-
agentName: senderName,
|
|
748
|
-
entityType: resolvedEntityType,
|
|
749
|
-
cli: 'dashboard',
|
|
750
|
-
reconnect: true,
|
|
751
|
-
maxReconnectAttempts: 5,
|
|
752
|
-
});
|
|
753
|
-
client.onError = (err) => {
|
|
754
|
-
console.error(`[dashboard] Relay client error for ${senderName}:`, err.message);
|
|
755
|
-
};
|
|
756
|
-
client.onStateChange = (state) => {
|
|
757
|
-
console.log(`[dashboard] Relay client for ${senderName} state: ${state}`);
|
|
758
|
-
// Clean up disconnected clients
|
|
759
|
-
if (state === 'DISCONNECTED') {
|
|
760
|
-
relayClients.delete(senderName);
|
|
761
|
-
}
|
|
762
|
-
};
|
|
763
|
-
// Set up channel message handler to forward messages to presence WebSocket
|
|
764
|
-
// This enables cloud users to receive channel messages via the presence bridge
|
|
765
|
-
client.onChannelMessage = (from, channel, body, envelope) => {
|
|
766
|
-
console.log(`[dashboard] *** CHANNEL MESSAGE RECEIVED *** for ${senderName}: ${from} -> ${channel}`);
|
|
767
|
-
// Broadcast to presence WebSocket clients so cloud can forward to its users
|
|
768
|
-
// Include the target user so cloud knows who to forward to
|
|
769
|
-
broadcastChannelMessage({
|
|
770
|
-
type: 'channel_message',
|
|
771
|
-
targetUser: senderName,
|
|
772
|
-
channel,
|
|
773
|
-
from,
|
|
774
|
-
body,
|
|
775
|
-
thread: envelope?.payload?.thread,
|
|
776
|
-
mentions: envelope?.payload?.mentions,
|
|
777
|
-
timestamp: new Date().toISOString(),
|
|
778
|
-
});
|
|
779
|
-
};
|
|
780
|
-
try {
|
|
781
|
-
await client.connect();
|
|
782
|
-
relayClients.set(senderName, client);
|
|
783
|
-
console.log(`[dashboard] Connected to relay daemon as ${senderName}`);
|
|
784
|
-
return client;
|
|
785
|
-
}
|
|
786
|
-
catch (err) {
|
|
787
|
-
console.error(`[dashboard] Failed to connect to relay daemon as ${senderName}:`, err);
|
|
788
|
-
return undefined;
|
|
789
|
-
}
|
|
790
|
-
finally {
|
|
791
|
-
// Clean up pending connection
|
|
792
|
-
pendingConnections.delete(senderName);
|
|
793
|
-
}
|
|
794
|
-
})();
|
|
795
|
-
// Store the pending connection
|
|
796
|
-
pendingConnections.set(senderName, connectionPromise);
|
|
797
|
-
return connectionPromise;
|
|
798
|
-
};
|
|
799
|
-
// Start default relay client connection (non-blocking)
|
|
800
|
-
// Use '_DashboardUI' to avoid conflicts with agents named 'Dashboard'
|
|
801
|
-
getRelayClient('_DashboardUI').catch(() => { });
|
|
802
|
-
// User bridge for human-to-human and human-to-agent messaging
|
|
803
|
-
const userBridge = new UserBridge({
|
|
804
|
-
socketPath,
|
|
805
|
-
createRelayClient: async (options) => {
|
|
806
|
-
const client = new RelayClient({
|
|
807
|
-
socketPath: options.socketPath,
|
|
808
|
-
agentName: options.agentName,
|
|
809
|
-
entityType: options.entityType,
|
|
810
|
-
displayName: options.displayName,
|
|
811
|
-
avatarUrl: options.avatarUrl,
|
|
812
|
-
cli: 'dashboard',
|
|
813
|
-
reconnect: true,
|
|
814
|
-
maxReconnectAttempts: 5,
|
|
815
|
-
});
|
|
816
|
-
client.onError = (err) => {
|
|
817
|
-
console.error(`[user-bridge] Relay client error for ${options.agentName}:`, err.message);
|
|
818
|
-
};
|
|
819
|
-
await client.connect();
|
|
820
|
-
return client;
|
|
821
|
-
},
|
|
822
|
-
loadPersistedChannels: (username) => loadPersistedChannelsForUser(username, defaultWorkspaceId),
|
|
823
|
-
});
|
|
824
|
-
// Bridge client for cross-project messaging
|
|
825
|
-
let bridgeClient;
|
|
826
|
-
let bridgeClientConnecting = false;
|
|
827
|
-
const connectBridgeClient = async () => {
|
|
828
|
-
if (bridgeClient || bridgeClientConnecting)
|
|
829
|
-
return;
|
|
830
|
-
// Check if bridge-state.json exists and has projects
|
|
831
|
-
const bridgeStatePath = path.join(dataDir, 'bridge-state.json');
|
|
832
|
-
if (!fs.existsSync(bridgeStatePath)) {
|
|
833
|
-
return;
|
|
834
|
-
}
|
|
835
|
-
try {
|
|
836
|
-
const bridgeState = JSON.parse(fs.readFileSync(bridgeStatePath, 'utf-8'));
|
|
837
|
-
if (!bridgeState.connected || !bridgeState.projects?.length) {
|
|
838
|
-
return;
|
|
839
|
-
}
|
|
840
|
-
bridgeClientConnecting = true;
|
|
841
|
-
// Build project configs from bridge state
|
|
842
|
-
const projectConfigs = bridgeState.projects.map((p) => {
|
|
843
|
-
// Compute socket path for each project
|
|
844
|
-
const projectHash = crypto.createHash('sha256').update(p.path).digest('hex').slice(0, 12);
|
|
845
|
-
const projectDataDir = path.join(path.dirname(dataDir), projectHash);
|
|
846
|
-
const socketPath = path.join(projectDataDir, 'relay.sock');
|
|
847
|
-
return {
|
|
848
|
-
id: p.id,
|
|
849
|
-
path: p.path,
|
|
850
|
-
socketPath,
|
|
851
|
-
leadName: p.lead?.name || 'Lead',
|
|
852
|
-
cli: 'dashboard-bridge',
|
|
853
|
-
};
|
|
854
|
-
});
|
|
855
|
-
// Filter to projects with existing sockets
|
|
856
|
-
const validConfigs = projectConfigs.filter((p) => fs.existsSync(p.socketPath));
|
|
857
|
-
if (validConfigs.length === 0) {
|
|
858
|
-
bridgeClientConnecting = false;
|
|
859
|
-
return;
|
|
860
|
-
}
|
|
861
|
-
bridgeClient = new MultiProjectClient(validConfigs, {
|
|
862
|
-
agentName: '__DashboardBridge__', // Unique name to avoid conflict with CLI bridge
|
|
863
|
-
reconnect: true,
|
|
864
|
-
});
|
|
865
|
-
bridgeClient.onProjectStateChange = (projectId, connected) => {
|
|
866
|
-
console.log(`[dashboard-bridge] Project ${projectId} ${connected ? 'connected' : 'disconnected'}`);
|
|
867
|
-
};
|
|
868
|
-
await bridgeClient.connect();
|
|
869
|
-
console.log('[dashboard] Bridge client connected to', validConfigs.length, 'project(s)');
|
|
870
|
-
bridgeClientConnecting = false;
|
|
871
|
-
}
|
|
872
|
-
catch (err) {
|
|
873
|
-
console.error('[dashboard] Failed to connect bridge client:', err);
|
|
874
|
-
bridgeClient = undefined;
|
|
875
|
-
bridgeClientConnecting = false;
|
|
876
|
-
}
|
|
877
|
-
};
|
|
878
|
-
// Start bridge client connection (non-blocking)
|
|
879
|
-
connectBridgeClient().catch(() => { });
|
|
880
|
-
// Helper to check if an agent is online (seen within heartbeat timeout window)
|
|
881
|
-
// Uses 30 second threshold to align with heartbeat timeout (5s * 6 multiplier)
|
|
882
|
-
const isAgentOnline = (agentName) => {
|
|
883
|
-
if (agentName === '*')
|
|
884
|
-
return true; // Broadcast always allowed
|
|
885
|
-
const agentsPath = path.join(teamDir, 'agents.json');
|
|
886
|
-
if (!fs.existsSync(agentsPath))
|
|
887
|
-
return false;
|
|
888
|
-
try {
|
|
889
|
-
const data = JSON.parse(fs.readFileSync(agentsPath, 'utf-8'));
|
|
890
|
-
const agent = data.agents?.find((a) => a.name === agentName);
|
|
891
|
-
if (!agent || !agent.lastSeen)
|
|
892
|
-
return false;
|
|
893
|
-
const thirtySecondsAgo = Date.now() - 30 * 1000;
|
|
894
|
-
return new Date(agent.lastSeen).getTime() > thirtySecondsAgo;
|
|
895
|
-
}
|
|
896
|
-
catch {
|
|
897
|
-
return false;
|
|
898
|
-
}
|
|
899
|
-
};
|
|
900
|
-
const isUserOnline = (username) => {
|
|
901
|
-
if (username === '*')
|
|
902
|
-
return true;
|
|
903
|
-
return onlineUsers.has(username) || userBridge.isUserRegistered(username);
|
|
904
|
-
};
|
|
905
|
-
const isRecipientOnline = (name) => (isAgentOnline(name) || isUserOnline(name));
|
|
906
|
-
// Helper to get team members from teams.json, agents.json, and spawner's active workers
|
|
907
|
-
const getTeamMembers = (teamName) => {
|
|
908
|
-
const members = new Set();
|
|
909
|
-
// Check teams.json first - this is the source of truth for team definitions
|
|
910
|
-
const teamsConfig = loadTeamsConfig(projectRoot || dataDir);
|
|
911
|
-
if (teamsConfig && teamsConfig.team === teamName) {
|
|
912
|
-
for (const agent of teamsConfig.agents) {
|
|
913
|
-
members.add(agent.name);
|
|
914
|
-
}
|
|
915
|
-
}
|
|
916
|
-
// Check spawner's active workers (they have accurate team info for spawned agents)
|
|
917
|
-
if (spawner) {
|
|
918
|
-
const activeWorkers = spawner.getActiveWorkers();
|
|
919
|
-
for (const worker of activeWorkers) {
|
|
920
|
-
if (worker.team === teamName) {
|
|
921
|
-
members.add(worker.name);
|
|
922
|
-
}
|
|
923
|
-
}
|
|
924
|
-
}
|
|
925
|
-
// Also check agents.json for persisted team info
|
|
926
|
-
const agentsPath = path.join(teamDir, 'agents.json');
|
|
927
|
-
if (fs.existsSync(agentsPath)) {
|
|
928
|
-
try {
|
|
929
|
-
const data = JSON.parse(fs.readFileSync(agentsPath, 'utf-8'));
|
|
930
|
-
for (const agent of (data.agents || [])) {
|
|
931
|
-
if (agent.team === teamName) {
|
|
932
|
-
members.add(agent.name);
|
|
933
|
-
}
|
|
934
|
-
}
|
|
935
|
-
}
|
|
936
|
-
catch {
|
|
937
|
-
// Ignore parse errors
|
|
938
|
-
}
|
|
939
|
-
}
|
|
940
|
-
return Array.from(members);
|
|
941
|
-
};
|
|
942
|
-
// API endpoint to send messages
|
|
943
|
-
app.post('/api/send', async (req, res) => {
|
|
944
|
-
const { to, message, thread, attachments: attachmentIds, from: senderName } = req.body;
|
|
945
|
-
// DEBUG: Trace message routing through /api/send
|
|
946
|
-
console.log(`[api/send] === MESSAGE TRACE ===`);
|
|
947
|
-
console.log(`[api/send] to=${to}, from=${senderName || 'not-specified'}`);
|
|
948
|
-
console.log(`[api/send] message length=${message?.length}, thread=${thread || 'none'}`);
|
|
949
|
-
console.log(`[api/send] message preview: ${message?.substring(0, 100)}...`);
|
|
950
|
-
if (!to || !message) {
|
|
951
|
-
return res.status(400).json({ error: 'Missing "to" or "message" field' });
|
|
952
|
-
}
|
|
953
|
-
// Check if this is a team mention (team:teamName)
|
|
954
|
-
const teamMatch = to.match(/^team:(.+)$/);
|
|
955
|
-
let targets;
|
|
956
|
-
if (teamMatch) {
|
|
957
|
-
const teamName = teamMatch[1];
|
|
958
|
-
const members = getTeamMembers(teamName);
|
|
959
|
-
if (members.length === 0) {
|
|
960
|
-
return res.status(404).json({ error: `No agents found in team "${teamName}"` });
|
|
961
|
-
}
|
|
962
|
-
// Filter to only online members
|
|
963
|
-
targets = members.filter(isAgentOnline);
|
|
964
|
-
if (targets.length === 0) {
|
|
965
|
-
return res.status(404).json({ error: `No online agents in team "${teamName}"` });
|
|
966
|
-
}
|
|
967
|
-
}
|
|
968
|
-
else {
|
|
969
|
-
// Fail fast if target agent is offline (except broadcasts)
|
|
970
|
-
if (to !== '*' && !isRecipientOnline(to)) {
|
|
971
|
-
return res.status(404).json({ error: `Recipient "${to}" is not online` });
|
|
972
|
-
}
|
|
973
|
-
targets = [to];
|
|
974
|
-
}
|
|
975
|
-
// Always use '_DashboardUI' client to avoid name conflicts with user agents
|
|
976
|
-
// (underscore prefix indicates system client, prevents collision if user names an agent "Dashboard")
|
|
977
|
-
// The sender name is preserved in message history/logs but not used for the relay connection
|
|
978
|
-
const relayClient = await getRelayClient('_DashboardUI');
|
|
979
|
-
if (!relayClient || relayClient.state !== 'READY') {
|
|
980
|
-
return res.status(503).json({ error: 'Relay daemon not connected' });
|
|
981
|
-
}
|
|
982
|
-
try {
|
|
983
|
-
// Resolve attachments if provided
|
|
984
|
-
let attachments;
|
|
985
|
-
if (attachmentIds && Array.isArray(attachmentIds) && attachmentIds.length > 0) {
|
|
986
|
-
attachments = [];
|
|
987
|
-
for (const id of attachmentIds) {
|
|
988
|
-
const attachment = attachmentRegistry.get(id);
|
|
989
|
-
if (attachment) {
|
|
990
|
-
attachments.push(attachment);
|
|
991
|
-
}
|
|
992
|
-
}
|
|
993
|
-
}
|
|
994
|
-
// Include attachments, channel context, and sender info in the message data field
|
|
995
|
-
// For broadcasts (to='*'), include channel: 'general' so replies can be routed back
|
|
996
|
-
// For dashboard messages, include senderName so frontend can display actual user instead of '_DashboardUI'
|
|
997
|
-
const isBroadcast = targets.length === 1 && targets[0] === '*';
|
|
998
|
-
const messageData = {};
|
|
999
|
-
if (attachments && attachments.length > 0) {
|
|
1000
|
-
messageData.attachments = attachments;
|
|
1001
|
-
}
|
|
1002
|
-
if (isBroadcast) {
|
|
1003
|
-
messageData.channel = 'general';
|
|
1004
|
-
}
|
|
1005
|
-
// Include actual sender name for dashboard messages (relay client uses '_DashboardUI' but
|
|
1006
|
-
// we want the real user's name displayed in message history)
|
|
1007
|
-
if (senderName) {
|
|
1008
|
-
messageData.senderName = senderName;
|
|
1009
|
-
}
|
|
1010
|
-
const hasMessageData = Object.keys(messageData).length > 0;
|
|
1011
|
-
// Send to all targets (single agent, team members, or broadcast)
|
|
1012
|
-
let allSent = true;
|
|
1013
|
-
for (const target of targets) {
|
|
1014
|
-
const sent = relayClient.sendMessage(target, message, 'message', hasMessageData ? messageData : undefined, thread);
|
|
1015
|
-
if (!sent) {
|
|
1016
|
-
allSent = false;
|
|
1017
|
-
console.error(`[dashboard] Failed to send message to ${target}`);
|
|
1018
|
-
}
|
|
1019
|
-
}
|
|
1020
|
-
if (allSent) {
|
|
1021
|
-
// Broadcast updated data to all connected clients so they see the sent message
|
|
1022
|
-
broadcastData().catch((err) => console.error('[dashboard] Failed to broadcast after send:', err));
|
|
1023
|
-
res.json({ success: true, sentTo: targets.length > 1 ? targets : targets[0] });
|
|
1024
|
-
}
|
|
1025
|
-
else {
|
|
1026
|
-
res.status(500).json({ error: 'Failed to send message to some recipients' });
|
|
1027
|
-
}
|
|
1028
|
-
}
|
|
1029
|
-
catch (err) {
|
|
1030
|
-
console.error('[dashboard] Failed to send message:', err);
|
|
1031
|
-
res.status(500).json({ error: 'Failed to send message' });
|
|
1032
|
-
}
|
|
1033
|
-
});
|
|
1034
|
-
// API endpoint to send messages via bridge (cross-project)
|
|
1035
|
-
app.post('/api/bridge/send', async (req, res) => {
|
|
1036
|
-
const { projectId, to, message } = req.body;
|
|
1037
|
-
if (!projectId || !to || !message) {
|
|
1038
|
-
return res.status(400).json({ error: 'Missing "projectId", "to", or "message" field' });
|
|
1039
|
-
}
|
|
1040
|
-
// Try to connect bridge client if not connected
|
|
1041
|
-
if (!bridgeClient) {
|
|
1042
|
-
await connectBridgeClient();
|
|
1043
|
-
if (!bridgeClient) {
|
|
1044
|
-
return res.status(503).json({ error: 'Bridge not connected. Is the bridge command running?' });
|
|
1045
|
-
}
|
|
1046
|
-
}
|
|
1047
|
-
try {
|
|
1048
|
-
const sent = bridgeClient.sendToProject(projectId, to, message);
|
|
1049
|
-
if (sent) {
|
|
1050
|
-
res.json({ success: true });
|
|
1051
|
-
}
|
|
1052
|
-
else {
|
|
1053
|
-
res.status(500).json({ error: `Failed to send message to ${projectId}:${to}` });
|
|
1054
|
-
}
|
|
1055
|
-
}
|
|
1056
|
-
catch (err) {
|
|
1057
|
-
console.error('[dashboard] Failed to send bridge message:', err);
|
|
1058
|
-
res.status(500).json({ error: 'Failed to send bridge message' });
|
|
1059
|
-
}
|
|
1060
|
-
});
|
|
1061
|
-
// API endpoint to upload attachments (images/screenshots)
|
|
1062
|
-
app.post('/api/upload', async (req, res) => {
|
|
1063
|
-
const { filename, mimeType, data } = req.body;
|
|
1064
|
-
// Validate required fields
|
|
1065
|
-
if (!filename || !mimeType || !data) {
|
|
1066
|
-
return res.status(400).json({
|
|
1067
|
-
success: false,
|
|
1068
|
-
error: 'Missing required fields: filename, mimeType, data',
|
|
1069
|
-
});
|
|
1070
|
-
}
|
|
1071
|
-
// Validate mime type (only allow images for now)
|
|
1072
|
-
const allowedTypes = ['image/png', 'image/jpeg', 'image/gif', 'image/webp', 'image/svg+xml'];
|
|
1073
|
-
if (!allowedTypes.includes(mimeType)) {
|
|
1074
|
-
return res.status(400).json({
|
|
1075
|
-
success: false,
|
|
1076
|
-
error: `Invalid file type. Allowed types: ${allowedTypes.join(', ')}`,
|
|
1077
|
-
});
|
|
1078
|
-
}
|
|
1079
|
-
try {
|
|
1080
|
-
// Decode base64 data
|
|
1081
|
-
const base64Data = data.replace(/^data:[^;]+;base64,/, '');
|
|
1082
|
-
const buffer = Buffer.from(base64Data, 'base64');
|
|
1083
|
-
// Generate unique ID and filename for the attachment
|
|
1084
|
-
const attachmentId = crypto.randomUUID();
|
|
1085
|
-
const timestamp = Date.now();
|
|
1086
|
-
const ext = mimeType.split('/')[1].replace('svg+xml', 'svg');
|
|
1087
|
-
// Use format: {messageId}-{timestamp}.{ext} for unique, identifiable filenames
|
|
1088
|
-
const safeFilename = `${attachmentId.substring(0, 8)}-${timestamp}.${ext}`;
|
|
1089
|
-
// Save to ~/.relay/attachments/ directory for agents to access
|
|
1090
|
-
const attachmentFilePath = path.join(attachmentsDir, safeFilename);
|
|
1091
|
-
fs.writeFileSync(attachmentFilePath, buffer);
|
|
1092
|
-
// Create attachment record with file path for agents
|
|
1093
|
-
const attachment = {
|
|
1094
|
-
id: attachmentId,
|
|
1095
|
-
filename: filename,
|
|
1096
|
-
mimeType: mimeType,
|
|
1097
|
-
size: buffer.length,
|
|
1098
|
-
url: `/attachments/${safeFilename}`,
|
|
1099
|
-
// Include absolute file path so agents can read the file directly
|
|
1100
|
-
filePath: attachmentFilePath,
|
|
1101
|
-
// Include base64 data for agents that can't access the file
|
|
1102
|
-
data: data,
|
|
1103
|
-
};
|
|
1104
|
-
// Store in registry for lookup when sending messages
|
|
1105
|
-
attachmentRegistry.set(attachmentId, attachment);
|
|
1106
|
-
console.log(`[dashboard] Uploaded attachment: ${filename} (${buffer.length} bytes) -> ${attachmentFilePath}`);
|
|
1107
|
-
res.json({
|
|
1108
|
-
success: true,
|
|
1109
|
-
attachment: {
|
|
1110
|
-
id: attachment.id,
|
|
1111
|
-
filename: attachment.filename,
|
|
1112
|
-
mimeType: attachment.mimeType,
|
|
1113
|
-
size: attachment.size,
|
|
1114
|
-
url: attachment.url,
|
|
1115
|
-
filePath: attachment.filePath,
|
|
1116
|
-
},
|
|
1117
|
-
});
|
|
1118
|
-
}
|
|
1119
|
-
catch (err) {
|
|
1120
|
-
console.error('[dashboard] Upload failed:', err);
|
|
1121
|
-
res.status(500).json({
|
|
1122
|
-
success: false,
|
|
1123
|
-
error: 'Failed to upload file',
|
|
1124
|
-
});
|
|
1125
|
-
}
|
|
1126
|
-
});
|
|
1127
|
-
// API endpoint to get attachment by ID
|
|
1128
|
-
app.get('/api/attachment/:id', (req, res) => {
|
|
1129
|
-
const { id } = req.params;
|
|
1130
|
-
const attachment = attachmentRegistry.get(id);
|
|
1131
|
-
if (!attachment) {
|
|
1132
|
-
return res.status(404).json({ error: 'Attachment not found' });
|
|
1133
|
-
}
|
|
1134
|
-
res.json({
|
|
1135
|
-
success: true,
|
|
1136
|
-
attachment: {
|
|
1137
|
-
id: attachment.id,
|
|
1138
|
-
filename: attachment.filename,
|
|
1139
|
-
mimeType: attachment.mimeType,
|
|
1140
|
-
size: attachment.size,
|
|
1141
|
-
url: attachment.url,
|
|
1142
|
-
filePath: attachment.filePath,
|
|
1143
|
-
},
|
|
1144
|
-
});
|
|
1145
|
-
});
|
|
1146
|
-
const getTeamData = () => {
|
|
1147
|
-
// Try team.json first (file-based team mode)
|
|
1148
|
-
const teamPath = path.join(teamDir, 'team.json');
|
|
1149
|
-
if (fs.existsSync(teamPath)) {
|
|
1150
|
-
try {
|
|
1151
|
-
return JSON.parse(fs.readFileSync(teamPath, 'utf-8'));
|
|
1152
|
-
}
|
|
1153
|
-
catch (e) {
|
|
1154
|
-
console.error('Failed to read team.json', e);
|
|
1155
|
-
}
|
|
1156
|
-
}
|
|
1157
|
-
// Fall back to agents.json (daemon mode - live connected agents)
|
|
1158
|
-
const agentsPath = path.join(teamDir, 'agents.json');
|
|
1159
|
-
if (fs.existsSync(agentsPath)) {
|
|
1160
|
-
try {
|
|
1161
|
-
const data = JSON.parse(fs.readFileSync(agentsPath, 'utf-8'));
|
|
1162
|
-
// Convert agents.json format to team.json format
|
|
1163
|
-
return {
|
|
1164
|
-
agents: data.agents.map((a) => ({
|
|
1165
|
-
name: a.name,
|
|
1166
|
-
role: 'Agent',
|
|
1167
|
-
cli: a.cli ?? 'Unknown',
|
|
1168
|
-
lastSeen: a.lastSeen ?? a.connectedAt,
|
|
1169
|
-
lastActive: a.lastSeen ?? a.connectedAt,
|
|
1170
|
-
team: a.team,
|
|
1171
|
-
})),
|
|
1172
|
-
};
|
|
1173
|
-
}
|
|
1174
|
-
catch (e) {
|
|
1175
|
-
console.error('Failed to read agents.json', e);
|
|
1176
|
-
}
|
|
1177
|
-
}
|
|
1178
|
-
return null;
|
|
1179
|
-
};
|
|
1180
|
-
const parseInbox = (agentName) => {
|
|
1181
|
-
const inboxPath = path.join(dataDir, agentName, 'inbox.md');
|
|
1182
|
-
if (!fs.existsSync(inboxPath))
|
|
1183
|
-
return [];
|
|
1184
|
-
try {
|
|
1185
|
-
const content = fs.readFileSync(inboxPath, 'utf-8');
|
|
1186
|
-
const messages = [];
|
|
1187
|
-
// Split by "## Message from "
|
|
1188
|
-
const parts = content.split('## Message from ');
|
|
1189
|
-
parts.forEach((part, index) => {
|
|
1190
|
-
if (!part.trim())
|
|
1191
|
-
return;
|
|
1192
|
-
const firstLineEnd = part.indexOf('\n');
|
|
1193
|
-
if (firstLineEnd === -1)
|
|
1194
|
-
return;
|
|
1195
|
-
const header = part.substring(0, firstLineEnd).trim(); // "Sender | Timestamp" or just "Sender"
|
|
1196
|
-
const body = part.substring(firstLineEnd).trim();
|
|
1197
|
-
// Handle potential " | " in header
|
|
1198
|
-
let sender = header;
|
|
1199
|
-
let timestamp = new Date().toISOString();
|
|
1200
|
-
if (header.includes('|')) {
|
|
1201
|
-
const split = header.split('|');
|
|
1202
|
-
sender = split[0].trim();
|
|
1203
|
-
timestamp = split.slice(1).join('|').trim();
|
|
1204
|
-
}
|
|
1205
|
-
messages.push({
|
|
1206
|
-
from: sender,
|
|
1207
|
-
to: agentName,
|
|
1208
|
-
content: body,
|
|
1209
|
-
timestamp: timestamp,
|
|
1210
|
-
id: `${agentName}-${index}-${Date.now()}`
|
|
1211
|
-
});
|
|
1212
|
-
});
|
|
1213
|
-
return messages;
|
|
1214
|
-
}
|
|
1215
|
-
catch (e) {
|
|
1216
|
-
console.error(`Failed to read inbox for ${agentName}`, e);
|
|
1217
|
-
return [];
|
|
1218
|
-
}
|
|
1219
|
-
};
|
|
1220
|
-
// Helper to check if an agent name is internal/system (should be hidden from UI)
|
|
1221
|
-
// Convention: agent names starting with __ are internal (e.g., __spawner__, __DashboardBridge__)
|
|
1222
|
-
const isInternalAgent = (name) => {
|
|
1223
|
-
return name.startsWith('__');
|
|
1224
|
-
};
|
|
1225
|
-
const mapStoredMessages = (rows) => rows
|
|
1226
|
-
// Filter out messages from/to internal system agents (e.g., __spawner__)
|
|
1227
|
-
.filter((row) => !isInternalAgent(row.from) && !isInternalAgent(row.to))
|
|
1228
|
-
.map((row) => {
|
|
1229
|
-
// Extract attachments, channel, and senderName from the data field if present
|
|
1230
|
-
let attachments;
|
|
1231
|
-
let channel;
|
|
1232
|
-
let effectiveFrom = row.from;
|
|
1233
|
-
if (row.data && typeof row.data === 'object') {
|
|
1234
|
-
if ('attachments' in row.data) {
|
|
1235
|
-
attachments = row.data.attachments;
|
|
1236
|
-
}
|
|
1237
|
-
if ('channel' in row.data) {
|
|
1238
|
-
channel = row.data.channel;
|
|
1239
|
-
}
|
|
1240
|
-
// For dashboard messages sent via _DashboardUI, use the actual sender name
|
|
1241
|
-
// This provides proper attribution in message history
|
|
1242
|
-
if ('senderName' in row.data && row.from === '_DashboardUI') {
|
|
1243
|
-
effectiveFrom = row.data.senderName;
|
|
1244
|
-
}
|
|
1245
|
-
}
|
|
1246
|
-
return {
|
|
1247
|
-
from: effectiveFrom,
|
|
1248
|
-
to: row.to,
|
|
1249
|
-
content: row.body,
|
|
1250
|
-
timestamp: new Date(row.ts).toISOString(),
|
|
1251
|
-
id: row.id,
|
|
1252
|
-
thread: row.thread,
|
|
1253
|
-
isBroadcast: row.is_broadcast,
|
|
1254
|
-
replyCount: row.replyCount,
|
|
1255
|
-
status: row.status,
|
|
1256
|
-
attachments,
|
|
1257
|
-
channel,
|
|
1258
|
-
};
|
|
1259
|
-
});
|
|
1260
|
-
const getMessages = async (agents) => {
|
|
1261
|
-
if (storage) {
|
|
1262
|
-
const rows = await storage.getMessages({ limit: 100, order: 'desc' });
|
|
1263
|
-
// Dashboard expects oldest first
|
|
1264
|
-
return mapStoredMessages(rows).reverse();
|
|
1265
|
-
}
|
|
1266
|
-
// Fallback to file-based inbox parsing
|
|
1267
|
-
let allMessages = [];
|
|
1268
|
-
agents.forEach((a) => {
|
|
1269
|
-
const msgs = parseInbox(a.name);
|
|
1270
|
-
allMessages = [...allMessages, ...msgs];
|
|
1271
|
-
});
|
|
1272
|
-
return allMessages;
|
|
1273
|
-
};
|
|
1274
|
-
const formatDuration = (startMs, endMs) => {
|
|
1275
|
-
const end = endMs ?? Date.now();
|
|
1276
|
-
const durationMs = end - startMs;
|
|
1277
|
-
const minutes = Math.floor(durationMs / 60000);
|
|
1278
|
-
const hours = Math.floor(minutes / 60);
|
|
1279
|
-
if (hours > 0) {
|
|
1280
|
-
return `${hours}h ${minutes % 60}m`;
|
|
1281
|
-
}
|
|
1282
|
-
return `${minutes}m`;
|
|
1283
|
-
};
|
|
1284
|
-
const getRecentSessions = async () => {
|
|
1285
|
-
if (storage && storage instanceof SqliteStorageAdapter) {
|
|
1286
|
-
const sessions = await storage.getRecentSessions(20);
|
|
1287
|
-
return sessions.map(s => ({
|
|
1288
|
-
id: s.id,
|
|
1289
|
-
agentName: s.agentName,
|
|
1290
|
-
cli: s.cli,
|
|
1291
|
-
startedAt: new Date(s.startedAt).toISOString(),
|
|
1292
|
-
endedAt: s.endedAt ? new Date(s.endedAt).toISOString() : undefined,
|
|
1293
|
-
duration: formatDuration(s.startedAt, s.endedAt),
|
|
1294
|
-
messageCount: s.messageCount,
|
|
1295
|
-
summary: s.summary,
|
|
1296
|
-
isActive: !s.endedAt, // Active if no end time
|
|
1297
|
-
closedBy: s.closedBy,
|
|
1298
|
-
}));
|
|
1299
|
-
}
|
|
1300
|
-
return [];
|
|
1301
|
-
};
|
|
1302
|
-
const getAgentSummaries = async () => {
|
|
1303
|
-
if (storage && storage instanceof SqliteStorageAdapter) {
|
|
1304
|
-
const summaries = await storage.getAllAgentSummaries();
|
|
1305
|
-
return summaries.map(s => ({
|
|
1306
|
-
agentName: s.agentName,
|
|
1307
|
-
lastUpdated: new Date(s.lastUpdated).toISOString(),
|
|
1308
|
-
currentTask: s.currentTask,
|
|
1309
|
-
completedTasks: s.completedTasks,
|
|
1310
|
-
context: s.context,
|
|
1311
|
-
}));
|
|
1312
|
-
}
|
|
1313
|
-
return [];
|
|
1314
|
-
};
|
|
1315
|
-
const getAllData = async () => {
|
|
1316
|
-
const team = getTeamData();
|
|
1317
|
-
if (!team)
|
|
1318
|
-
return { agents: [], messages: [], activity: [], sessions: [], summaries: [] };
|
|
1319
|
-
const agentsMap = new Map();
|
|
1320
|
-
const allMessages = await getMessages(team.agents);
|
|
1321
|
-
// Initialize agents from config
|
|
1322
|
-
team.agents.forEach((a) => {
|
|
1323
|
-
agentsMap.set(a.name, {
|
|
1324
|
-
name: a.name,
|
|
1325
|
-
role: a.role,
|
|
1326
|
-
cli: a.cli ?? 'Unknown',
|
|
1327
|
-
messageCount: 0,
|
|
1328
|
-
status: 'Idle',
|
|
1329
|
-
lastSeen: a.lastSeen,
|
|
1330
|
-
lastActive: a.lastActive,
|
|
1331
|
-
needsAttention: false,
|
|
1332
|
-
team: a.team,
|
|
1333
|
-
});
|
|
1334
|
-
});
|
|
1335
|
-
// Inject online human users (connected via dashboard WebSocket) into agentsMap
|
|
1336
|
-
// These users are tracked in onlineUsers for presence but need to appear in the agents list
|
|
1337
|
-
// with cli: 'dashboard' so they show up in the sidebar for DM conversations
|
|
1338
|
-
for (const [username, state] of onlineUsers) {
|
|
1339
|
-
const existing = agentsMap.get(username);
|
|
1340
|
-
if (existing) {
|
|
1341
|
-
// Update existing entry to ensure cli: 'dashboard' for proper human/agent separation
|
|
1342
|
-
// This fixes the bug where users appear as both human AND agent if they have a stale
|
|
1343
|
-
// entry in agents.json with a different cli value
|
|
1344
|
-
existing.cli = 'dashboard';
|
|
1345
|
-
existing.status = 'online';
|
|
1346
|
-
existing.avatarUrl = state.info.avatarUrl || existing.avatarUrl;
|
|
1347
|
-
}
|
|
1348
|
-
else {
|
|
1349
|
-
agentsMap.set(username, {
|
|
1350
|
-
name: username,
|
|
1351
|
-
role: 'User',
|
|
1352
|
-
cli: 'dashboard',
|
|
1353
|
-
messageCount: 0,
|
|
1354
|
-
status: 'online',
|
|
1355
|
-
lastSeen: state.info.lastSeen,
|
|
1356
|
-
lastActive: state.info.lastSeen,
|
|
1357
|
-
needsAttention: false,
|
|
1358
|
-
avatarUrl: state.info.avatarUrl,
|
|
1359
|
-
});
|
|
1360
|
-
}
|
|
1361
|
-
}
|
|
1362
|
-
// Update inbox counts if fallback mode; if storage, count messages addressed to agent
|
|
1363
|
-
if (storage) {
|
|
1364
|
-
for (const msg of allMessages) {
|
|
1365
|
-
const agent = agentsMap.get(msg.to);
|
|
1366
|
-
if (agent) {
|
|
1367
|
-
agent.messageCount = (agent.messageCount ?? 0) + 1;
|
|
1368
|
-
}
|
|
1369
|
-
}
|
|
1370
|
-
}
|
|
1371
|
-
else {
|
|
1372
|
-
// Sort by timestamp
|
|
1373
|
-
allMessages.sort((a, b) => {
|
|
1374
|
-
return new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime();
|
|
1375
|
-
});
|
|
1376
|
-
}
|
|
1377
|
-
// Derive status from messages sent BY agents
|
|
1378
|
-
// We scan all messages; if M is from A, we check if it is a STATUS message
|
|
1379
|
-
// Note: lastActive is updated from messages, but lastSeen comes from the registry
|
|
1380
|
-
// (heartbeat-based) and should NOT be overwritten by message timestamps
|
|
1381
|
-
allMessages.forEach(m => {
|
|
1382
|
-
const agent = agentsMap.get(m.from);
|
|
1383
|
-
if (agent) {
|
|
1384
|
-
agent.lastActive = m.timestamp;
|
|
1385
|
-
// Don't overwrite lastSeen - it comes from registry (heartbeat/connection tracking)
|
|
1386
|
-
if (m.content.startsWith('STATUS:')) {
|
|
1387
|
-
agent.status = m.content.substring(7).trim(); // remove "STATUS:"
|
|
1388
|
-
}
|
|
1389
|
-
}
|
|
1390
|
-
});
|
|
1391
|
-
// Detect agents with unanswered inbound messages (needs attention)
|
|
1392
|
-
const needsAttentionAgents = computeNeedsAttention(allMessages.map((m) => ({
|
|
1393
|
-
from: m.from,
|
|
1394
|
-
to: m.to,
|
|
1395
|
-
timestamp: m.timestamp,
|
|
1396
|
-
thread: m.thread,
|
|
1397
|
-
isBroadcast: m.isBroadcast,
|
|
1398
|
-
})));
|
|
1399
|
-
needsAttentionAgents.forEach((agentName) => {
|
|
1400
|
-
const agent = agentsMap.get(agentName);
|
|
1401
|
-
if (agent) {
|
|
1402
|
-
agent.needsAttention = true;
|
|
1403
|
-
}
|
|
1404
|
-
});
|
|
1405
|
-
// Read processing state from daemon
|
|
1406
|
-
const processingStatePath = path.join(teamDir, 'processing-state.json');
|
|
1407
|
-
if (fs.existsSync(processingStatePath)) {
|
|
1408
|
-
try {
|
|
1409
|
-
const processingData = JSON.parse(fs.readFileSync(processingStatePath, 'utf-8'));
|
|
1410
|
-
const processingAgents = processingData.processingAgents || {};
|
|
1411
|
-
for (const [agentName, state] of Object.entries(processingAgents)) {
|
|
1412
|
-
const agent = agentsMap.get(agentName);
|
|
1413
|
-
if (agent && state && typeof state === 'object') {
|
|
1414
|
-
agent.isProcessing = true;
|
|
1415
|
-
agent.processingStartedAt = state.startedAt;
|
|
1416
|
-
}
|
|
1417
|
-
}
|
|
1418
|
-
}
|
|
1419
|
-
catch (_err) {
|
|
1420
|
-
// Ignore errors reading processing state - it's optional
|
|
1421
|
-
}
|
|
1422
|
-
}
|
|
1423
|
-
// Mark spawned agents with isSpawned flag and team
|
|
1424
|
-
if (spawner) {
|
|
1425
|
-
const activeWorkers = spawner.getActiveWorkers();
|
|
1426
|
-
for (const worker of activeWorkers) {
|
|
1427
|
-
const agent = agentsMap.get(worker.name);
|
|
1428
|
-
if (agent) {
|
|
1429
|
-
agent.isSpawned = true;
|
|
1430
|
-
if (worker.team) {
|
|
1431
|
-
agent.team = worker.team;
|
|
1432
|
-
}
|
|
1433
|
-
}
|
|
1434
|
-
}
|
|
1435
|
-
}
|
|
1436
|
-
// Set team from teams.json for agents that don't have a team yet
|
|
1437
|
-
// This ensures agents defined in teams.json are associated with their team
|
|
1438
|
-
// even if they weren't spawned via auto-spawn
|
|
1439
|
-
const teamsConfig = loadTeamsConfig(projectRoot || dataDir);
|
|
1440
|
-
if (teamsConfig) {
|
|
1441
|
-
for (const teamAgent of teamsConfig.agents) {
|
|
1442
|
-
const agent = agentsMap.get(teamAgent.name);
|
|
1443
|
-
if (agent && !agent.team) {
|
|
1444
|
-
agent.team = teamsConfig.team;
|
|
1445
|
-
}
|
|
1446
|
-
}
|
|
1447
|
-
}
|
|
1448
|
-
// Fetch sessions and summaries in parallel
|
|
1449
|
-
const [sessions, summaries] = await Promise.all([
|
|
1450
|
-
getRecentSessions(),
|
|
1451
|
-
getAgentSummaries(),
|
|
1452
|
-
]);
|
|
1453
|
-
// Filter and separate agents from human users:
|
|
1454
|
-
// 1. Exclude "Dashboard" (internal agent, not a real team member)
|
|
1455
|
-
// 2. Exclude offline agents (no lastSeen or lastSeen > threshold)
|
|
1456
|
-
// 3. Exclude agents without a known CLI (these are improperly registered or stale)
|
|
1457
|
-
// 4. Separate human users (cli === 'dashboard') from AI agents
|
|
1458
|
-
const now = Date.now();
|
|
1459
|
-
// 30 seconds - aligns with heartbeat timeout (5s heartbeat * 6 multiplier = 30s)
|
|
1460
|
-
// This ensures agents disappear quickly after they stop responding to heartbeats
|
|
1461
|
-
const OFFLINE_THRESHOLD_MS = 30 * 1000;
|
|
1462
|
-
// First pass: filter out invalid/offline entries
|
|
1463
|
-
const validEntries = Array.from(agentsMap.values())
|
|
1464
|
-
.filter(agent => {
|
|
1465
|
-
// Exclude Dashboard
|
|
1466
|
-
if (agent.name === 'Dashboard')
|
|
1467
|
-
return false;
|
|
1468
|
-
// Exclude agents starting with __ (internal/system agents)
|
|
1469
|
-
if (agent.name.startsWith('__'))
|
|
1470
|
-
return false;
|
|
1471
|
-
// Exclude _DashboardUI (system client for sending dashboard messages)
|
|
1472
|
-
if (agent.name === '_DashboardUI')
|
|
1473
|
-
return false;
|
|
1474
|
-
// Exclude agents without a proper CLI (improperly registered or stale)
|
|
1475
|
-
if (!agent.cli || agent.cli === 'Unknown')
|
|
1476
|
-
return false;
|
|
1477
|
-
// Exclude offline agents (no lastSeen or too old)
|
|
1478
|
-
if (!agent.lastSeen)
|
|
1479
|
-
return false;
|
|
1480
|
-
const lastSeenTime = new Date(agent.lastSeen).getTime();
|
|
1481
|
-
if (now - lastSeenTime > OFFLINE_THRESHOLD_MS)
|
|
1482
|
-
return false;
|
|
1483
|
-
return true;
|
|
1484
|
-
});
|
|
1485
|
-
// Separate AI agents from human users
|
|
1486
|
-
const filteredAgents = validEntries
|
|
1487
|
-
.filter(agent => agent.cli !== 'dashboard')
|
|
1488
|
-
.map(agent => ({
|
|
1489
|
-
...agent,
|
|
1490
|
-
isHuman: false,
|
|
1491
|
-
}));
|
|
1492
|
-
const humanUsers = validEntries
|
|
1493
|
-
.filter(agent => agent.cli === 'dashboard')
|
|
1494
|
-
.map(agent => ({
|
|
1495
|
-
...agent,
|
|
1496
|
-
isHuman: true,
|
|
1497
|
-
}));
|
|
1498
|
-
return {
|
|
1499
|
-
agents: filteredAgents,
|
|
1500
|
-
users: humanUsers,
|
|
1501
|
-
messages: allMessages,
|
|
1502
|
-
activity: allMessages, // For now, activity log is just the message log
|
|
1503
|
-
sessions,
|
|
1504
|
-
summaries,
|
|
1505
|
-
};
|
|
1506
|
-
};
|
|
1507
|
-
// Track clients that are still initializing (haven't received first data yet)
|
|
1508
|
-
// This prevents race conditions where broadcastData sends before initial data is sent
|
|
1509
|
-
const initializingClients = new WeakSet();
|
|
1510
|
-
const broadcastData = async () => {
|
|
1511
|
-
try {
|
|
1512
|
-
const data = await getAllData();
|
|
1513
|
-
const payload = JSON.stringify(data);
|
|
1514
|
-
// Guard against empty/invalid payloads
|
|
1515
|
-
if (!payload || payload.length === 0) {
|
|
1516
|
-
console.warn('[dashboard] Skipping broadcast - empty payload');
|
|
1517
|
-
return;
|
|
1518
|
-
}
|
|
1519
|
-
wss.clients.forEach(client => {
|
|
1520
|
-
// Skip clients that are still being initialized by the connection handler
|
|
1521
|
-
if (initializingClients.has(client)) {
|
|
1522
|
-
return;
|
|
1523
|
-
}
|
|
1524
|
-
if (client.readyState === WebSocket.OPEN) {
|
|
1525
|
-
try {
|
|
1526
|
-
client.send(payload);
|
|
1527
|
-
}
|
|
1528
|
-
catch (err) {
|
|
1529
|
-
console.error('[dashboard] Failed to send to client:', err);
|
|
1530
|
-
}
|
|
1531
|
-
}
|
|
1532
|
-
});
|
|
1533
|
-
}
|
|
1534
|
-
catch (err) {
|
|
1535
|
-
console.error('[dashboard] Failed to broadcast data:', err);
|
|
1536
|
-
}
|
|
1537
|
-
};
|
|
1538
|
-
// Bridge data functions - defined before connection handlers
|
|
1539
|
-
const getBridgeData = async () => {
|
|
1540
|
-
const bridgeStatePath = path.join(dataDir, 'bridge-state.json');
|
|
1541
|
-
if (fs.existsSync(bridgeStatePath)) {
|
|
1542
|
-
try {
|
|
1543
|
-
const bridgeState = JSON.parse(fs.readFileSync(bridgeStatePath, 'utf-8'));
|
|
1544
|
-
// Enrich each project with actual agent data from their team directories
|
|
1545
|
-
if (bridgeState.projects && Array.isArray(bridgeState.projects)) {
|
|
1546
|
-
for (const project of bridgeState.projects) {
|
|
1547
|
-
if (project.path) {
|
|
1548
|
-
// Get project's data directory
|
|
1549
|
-
const crypto = await import('crypto');
|
|
1550
|
-
const projectHash = crypto.createHash('sha256').update(project.path).digest('hex').slice(0, 12);
|
|
1551
|
-
const projectDataDir = path.join(path.dirname(dataDir), projectHash);
|
|
1552
|
-
const projectTeamDir = path.join(projectDataDir, 'team');
|
|
1553
|
-
const agentsPath = path.join(projectTeamDir, 'agents.json');
|
|
1554
|
-
// Read actual connected agents
|
|
1555
|
-
if (fs.existsSync(agentsPath)) {
|
|
1556
|
-
try {
|
|
1557
|
-
const agentsData = JSON.parse(fs.readFileSync(agentsPath, 'utf-8'));
|
|
1558
|
-
if (agentsData.agents && Array.isArray(agentsData.agents)) {
|
|
1559
|
-
// Filter to only show online agents (seen within 30 seconds - aligns with heartbeat timeout)
|
|
1560
|
-
const thirtySecondsAgo = Date.now() - 30 * 1000;
|
|
1561
|
-
project.agents = agentsData.agents
|
|
1562
|
-
.filter((a) => {
|
|
1563
|
-
if (!a.lastSeen)
|
|
1564
|
-
return false;
|
|
1565
|
-
return new Date(a.lastSeen).getTime() > thirtySecondsAgo;
|
|
1566
|
-
})
|
|
1567
|
-
.map((a) => ({
|
|
1568
|
-
name: a.name,
|
|
1569
|
-
status: 'active',
|
|
1570
|
-
cli: a.cli,
|
|
1571
|
-
lastSeen: a.lastSeen,
|
|
1572
|
-
}));
|
|
1573
|
-
// Update lead status based on actual agents
|
|
1574
|
-
if (project.lead) {
|
|
1575
|
-
const leadAgent = project.agents.find((a) => a.name.toLowerCase() === project.lead.name.toLowerCase());
|
|
1576
|
-
project.lead.connected = !!leadAgent;
|
|
1577
|
-
}
|
|
1578
|
-
}
|
|
1579
|
-
}
|
|
1580
|
-
catch (e) {
|
|
1581
|
-
console.error(`Failed to read agents for ${project.path}:`, e);
|
|
1582
|
-
}
|
|
1583
|
-
}
|
|
1584
|
-
}
|
|
1585
|
-
}
|
|
1586
|
-
}
|
|
1587
|
-
return bridgeState;
|
|
1588
|
-
}
|
|
1589
|
-
catch {
|
|
1590
|
-
return { projects: [], messages: [], connected: false };
|
|
1591
|
-
}
|
|
1592
|
-
}
|
|
1593
|
-
return { projects: [], messages: [], connected: false };
|
|
1594
|
-
};
|
|
1595
|
-
const broadcastBridgeData = async () => {
|
|
1596
|
-
try {
|
|
1597
|
-
const data = await getBridgeData();
|
|
1598
|
-
const payload = JSON.stringify(data);
|
|
1599
|
-
// Guard against empty/invalid payloads
|
|
1600
|
-
if (!payload || payload.length === 0) {
|
|
1601
|
-
console.warn('[dashboard] Skipping bridge broadcast - empty payload');
|
|
1602
|
-
return;
|
|
1603
|
-
}
|
|
1604
|
-
wssBridge.clients.forEach(client => {
|
|
1605
|
-
if (client.readyState === WebSocket.OPEN) {
|
|
1606
|
-
try {
|
|
1607
|
-
client.send(payload);
|
|
1608
|
-
}
|
|
1609
|
-
catch (err) {
|
|
1610
|
-
console.error('[dashboard] Failed to send to bridge client:', err);
|
|
1611
|
-
}
|
|
1612
|
-
}
|
|
1613
|
-
});
|
|
1614
|
-
}
|
|
1615
|
-
catch (err) {
|
|
1616
|
-
console.error('[dashboard] Failed to broadcast bridge data:', err);
|
|
1617
|
-
}
|
|
1618
|
-
};
|
|
1619
|
-
// Handle new WebSocket connections - send initial data immediately
|
|
1620
|
-
wss.on('connection', async (ws, req) => {
|
|
1621
|
-
console.log('[dashboard] WebSocket client connected from:', req.socket.remoteAddress);
|
|
1622
|
-
// Mark client as alive initially for ping/pong keepalive
|
|
1623
|
-
mainClientAlive.set(ws, true);
|
|
1624
|
-
// Handle pong responses (keep connection alive)
|
|
1625
|
-
ws.on('pong', () => {
|
|
1626
|
-
mainClientAlive.set(ws, true);
|
|
1627
|
-
});
|
|
1628
|
-
// Mark as initializing to prevent broadcastData from sending before we do
|
|
1629
|
-
initializingClients.add(ws);
|
|
1630
|
-
try {
|
|
1631
|
-
const data = await getAllData();
|
|
1632
|
-
const payload = JSON.stringify(data);
|
|
1633
|
-
// Guard against empty/invalid payloads
|
|
1634
|
-
if (!payload || payload.length === 0) {
|
|
1635
|
-
console.warn('[dashboard] Skipping initial send - empty payload');
|
|
1636
|
-
return;
|
|
1637
|
-
}
|
|
1638
|
-
if (ws.readyState === WebSocket.OPEN) {
|
|
1639
|
-
console.log('[dashboard] Sending initial data, size:', payload.length, 'first 200 chars:', payload.substring(0, 200));
|
|
1640
|
-
ws.send(payload);
|
|
1641
|
-
console.log('[dashboard] Initial data sent successfully');
|
|
1642
|
-
}
|
|
1643
|
-
else {
|
|
1644
|
-
console.warn('[dashboard] WebSocket not open, state:', ws.readyState);
|
|
1645
|
-
}
|
|
1646
|
-
}
|
|
1647
|
-
catch (err) {
|
|
1648
|
-
console.error('[dashboard] Failed to send initial data:', err);
|
|
1649
|
-
}
|
|
1650
|
-
finally {
|
|
1651
|
-
// Now allow broadcastData to send to this client
|
|
1652
|
-
initializingClients.delete(ws);
|
|
1653
|
-
}
|
|
1654
|
-
ws.on('error', (err) => {
|
|
1655
|
-
console.error('[dashboard] WebSocket client error:', err);
|
|
1656
|
-
});
|
|
1657
|
-
ws.on('close', (code, reason) => {
|
|
1658
|
-
console.log('[dashboard] WebSocket client disconnected, code:', code, 'reason:', reason?.toString() || 'none');
|
|
1659
|
-
});
|
|
1660
|
-
});
|
|
1661
|
-
// Handle bridge WebSocket connections
|
|
1662
|
-
wssBridge.on('connection', async (ws) => {
|
|
1663
|
-
console.log('[dashboard] Bridge WebSocket client connected');
|
|
1664
|
-
// Mark client as alive initially for ping/pong keepalive
|
|
1665
|
-
bridgeClientAlive.set(ws, true);
|
|
1666
|
-
// Handle pong responses (keep connection alive)
|
|
1667
|
-
ws.on('pong', () => {
|
|
1668
|
-
bridgeClientAlive.set(ws, true);
|
|
1669
|
-
});
|
|
1670
|
-
try {
|
|
1671
|
-
const data = await getBridgeData();
|
|
1672
|
-
const payload = JSON.stringify(data);
|
|
1673
|
-
if (ws.readyState === WebSocket.OPEN) {
|
|
1674
|
-
ws.send(payload);
|
|
1675
|
-
}
|
|
1676
|
-
}
|
|
1677
|
-
catch (err) {
|
|
1678
|
-
console.error('[dashboard] Failed to send initial bridge data:', err);
|
|
1679
|
-
}
|
|
1680
|
-
ws.on('error', (err) => {
|
|
1681
|
-
console.error('[dashboard] Bridge WebSocket client error:', err);
|
|
1682
|
-
});
|
|
1683
|
-
ws.on('close', (code, reason) => {
|
|
1684
|
-
console.log('[dashboard] Bridge WebSocket client disconnected, code:', code, 'reason:', reason?.toString() || 'none');
|
|
1685
|
-
});
|
|
1686
|
-
});
|
|
1687
|
-
// Track alive status for ping/pong keepalive on log connections
|
|
1688
|
-
const logClientAlive = new WeakMap();
|
|
1689
|
-
// Ping interval for log WebSocket connections (30 seconds)
|
|
1690
|
-
// This prevents TCP/proxy timeouts from killing idle connections
|
|
1691
|
-
const LOG_PING_INTERVAL_MS = 30000;
|
|
1692
|
-
const logPingInterval = setInterval(() => {
|
|
1693
|
-
wssLogs.clients.forEach((ws) => {
|
|
1694
|
-
if (logClientAlive.get(ws) === false) {
|
|
1695
|
-
// Client didn't respond to last ping - close gracefully
|
|
1696
|
-
console.log('[dashboard] Logs WebSocket client unresponsive, closing gracefully');
|
|
1697
|
-
ws.close(1000, 'unresponsive');
|
|
1698
|
-
return;
|
|
1699
|
-
}
|
|
1700
|
-
// Mark as not alive until we get a pong
|
|
1701
|
-
logClientAlive.set(ws, false);
|
|
1702
|
-
ws.ping();
|
|
1703
|
-
});
|
|
1704
|
-
}, LOG_PING_INTERVAL_MS);
|
|
1705
|
-
// Clean up ping interval on server close
|
|
1706
|
-
wssLogs.on('close', () => {
|
|
1707
|
-
clearInterval(logPingInterval);
|
|
1708
|
-
});
|
|
1709
|
-
// Handle logs WebSocket connections for live log streaming
|
|
1710
|
-
wssLogs.on('connection', (ws, req) => {
|
|
1711
|
-
console.log('[dashboard] Logs WebSocket client connected');
|
|
1712
|
-
const clientSubscriptions = new Set();
|
|
1713
|
-
// Mark client as alive initially
|
|
1714
|
-
logClientAlive.set(ws, true);
|
|
1715
|
-
// Handle pong responses (keep connection alive)
|
|
1716
|
-
ws.on('pong', () => {
|
|
1717
|
-
logClientAlive.set(ws, true);
|
|
1718
|
-
});
|
|
1719
|
-
// Helper to check if agent is daemon-connected (from agents.json)
|
|
1720
|
-
const isDaemonConnected = (agentName) => {
|
|
1721
|
-
const agentsPath = path.join(teamDir, 'agents.json');
|
|
1722
|
-
if (!fs.existsSync(agentsPath))
|
|
1723
|
-
return false;
|
|
1724
|
-
try {
|
|
1725
|
-
const data = JSON.parse(fs.readFileSync(agentsPath, 'utf-8'));
|
|
1726
|
-
return data.agents?.some((a) => a.name === agentName) ?? false;
|
|
1727
|
-
}
|
|
1728
|
-
catch {
|
|
1729
|
-
return false;
|
|
1730
|
-
}
|
|
1731
|
-
};
|
|
1732
|
-
// Helper to subscribe to an agent
|
|
1733
|
-
const subscribeToAgent = (agentName) => {
|
|
1734
|
-
const isSpawned = spawner?.hasWorker(agentName) ?? false;
|
|
1735
|
-
const isDaemon = isDaemonConnected(agentName);
|
|
1736
|
-
// Check if agent exists (either spawned or daemon-connected)
|
|
1737
|
-
if (!isSpawned && !isDaemon) {
|
|
1738
|
-
ws.send(JSON.stringify({
|
|
1739
|
-
type: 'error',
|
|
1740
|
-
agent: agentName,
|
|
1741
|
-
error: `Agent ${agentName} not found`,
|
|
1742
|
-
}));
|
|
1743
|
-
// Close with custom code 4404 to signal "agent not found" - client should not reconnect
|
|
1744
|
-
ws.close(4404, 'Agent not found');
|
|
1745
|
-
return false;
|
|
1746
|
-
}
|
|
1747
|
-
// Add to subscriptions
|
|
1748
|
-
clientSubscriptions.add(agentName);
|
|
1749
|
-
if (!logSubscriptions.has(agentName)) {
|
|
1750
|
-
logSubscriptions.set(agentName, new Set());
|
|
1751
|
-
}
|
|
1752
|
-
logSubscriptions.get(agentName).add(ws);
|
|
1753
|
-
console.log(`[dashboard] Client subscribed to logs for: ${agentName} (spawned: ${isSpawned}, daemon: ${isDaemon})`);
|
|
1754
|
-
if (isSpawned && spawner) {
|
|
1755
|
-
// Send initial log history for spawned agents (5000 lines to match xterm scrollback capacity)
|
|
1756
|
-
const lines = spawner.getWorkerOutput(agentName, 5000);
|
|
1757
|
-
ws.send(JSON.stringify({
|
|
1758
|
-
type: 'history',
|
|
1759
|
-
agent: agentName,
|
|
1760
|
-
lines: lines || [],
|
|
1761
|
-
}));
|
|
1762
|
-
}
|
|
1763
|
-
else {
|
|
1764
|
-
// For daemon-connected agents, explain that PTY output isn't available
|
|
1765
|
-
ws.send(JSON.stringify({
|
|
1766
|
-
type: 'history',
|
|
1767
|
-
agent: agentName,
|
|
1768
|
-
lines: [`[${agentName} is a daemon-connected agent - PTY output not available. Showing relay messages only.]`],
|
|
1769
|
-
}));
|
|
1770
|
-
}
|
|
1771
|
-
ws.send(JSON.stringify({
|
|
1772
|
-
type: 'subscribed',
|
|
1773
|
-
agent: agentName,
|
|
1774
|
-
}));
|
|
1775
|
-
return true;
|
|
1776
|
-
};
|
|
1777
|
-
// Check if agent name is in URL path: /ws/logs/:agentName
|
|
1778
|
-
const pathname = new URL(req.url || '', `http://${req.headers.host}`).pathname;
|
|
1779
|
-
const pathMatch = pathname.match(/^\/ws\/logs\/(.+)$/);
|
|
1780
|
-
if (pathMatch) {
|
|
1781
|
-
const agentName = decodeURIComponent(pathMatch[1]);
|
|
1782
|
-
subscribeToAgent(agentName);
|
|
1783
|
-
}
|
|
1784
|
-
ws.on('message', (data) => {
|
|
1785
|
-
try {
|
|
1786
|
-
const msg = JSON.parse(data.toString());
|
|
1787
|
-
// Subscribe to agent logs
|
|
1788
|
-
if (msg.subscribe && typeof msg.subscribe === 'string') {
|
|
1789
|
-
subscribeToAgent(msg.subscribe);
|
|
1790
|
-
}
|
|
1791
|
-
// Unsubscribe from agent logs
|
|
1792
|
-
if (msg.unsubscribe && typeof msg.unsubscribe === 'string') {
|
|
1793
|
-
const agentName = msg.unsubscribe;
|
|
1794
|
-
clientSubscriptions.delete(agentName);
|
|
1795
|
-
logSubscriptions.get(agentName)?.delete(ws);
|
|
1796
|
-
console.log(`[dashboard] Client unsubscribed from logs for: ${agentName}`);
|
|
1797
|
-
ws.send(JSON.stringify({
|
|
1798
|
-
type: 'unsubscribed',
|
|
1799
|
-
agent: agentName,
|
|
1800
|
-
}));
|
|
1801
|
-
}
|
|
1802
|
-
// Handle interactive terminal input
|
|
1803
|
-
if (msg.type === 'input' && typeof msg.data === 'string') {
|
|
1804
|
-
// Get agent name from message or use first subscribed agent
|
|
1805
|
-
const agentName = msg.agent || [...clientSubscriptions][0];
|
|
1806
|
-
if (!agentName) {
|
|
1807
|
-
ws.send(JSON.stringify({
|
|
1808
|
-
type: 'error',
|
|
1809
|
-
error: 'No agent subscribed for input',
|
|
1810
|
-
}));
|
|
1811
|
-
return;
|
|
1812
|
-
}
|
|
1813
|
-
// Check if this is a spawned agent (we can only send input to spawned agents)
|
|
1814
|
-
if (spawner?.hasWorker(agentName)) {
|
|
1815
|
-
const success = spawner.sendWorkerInput(agentName, msg.data);
|
|
1816
|
-
if (!success) {
|
|
1817
|
-
console.warn(`[dashboard] Failed to send input to agent ${agentName}`);
|
|
1818
|
-
}
|
|
1819
|
-
}
|
|
1820
|
-
else {
|
|
1821
|
-
// Daemon-connected agents don't support direct input
|
|
1822
|
-
ws.send(JSON.stringify({
|
|
1823
|
-
type: 'error',
|
|
1824
|
-
agent: agentName,
|
|
1825
|
-
error: 'Interactive input not supported for daemon-connected agents',
|
|
1826
|
-
}));
|
|
1827
|
-
}
|
|
1828
|
-
}
|
|
1829
|
-
}
|
|
1830
|
-
catch (err) {
|
|
1831
|
-
console.error('[dashboard] Invalid logs WebSocket message:', err);
|
|
1832
|
-
}
|
|
1833
|
-
});
|
|
1834
|
-
ws.on('error', (err) => {
|
|
1835
|
-
console.error('[dashboard] Logs WebSocket client error:', err);
|
|
1836
|
-
});
|
|
1837
|
-
ws.on('close', (code, reason) => {
|
|
1838
|
-
// Clean up subscriptions on disconnect
|
|
1839
|
-
for (const agentName of clientSubscriptions) {
|
|
1840
|
-
logSubscriptions.get(agentName)?.delete(ws);
|
|
1841
|
-
}
|
|
1842
|
-
const reasonStr = reason?.toString() || 'no reason';
|
|
1843
|
-
console.log(`[dashboard] Logs WebSocket client disconnected (code: ${code}, reason: ${reasonStr})`);
|
|
1844
|
-
});
|
|
1845
|
-
});
|
|
1846
|
-
// Deduplication for log output - prevent same content from being broadcast multiple times
|
|
1847
|
-
// Key: agentName -> Set of recent content hashes (rolling window)
|
|
1848
|
-
const recentLogHashes = new Map();
|
|
1849
|
-
const MAX_LOG_HASH_WINDOW = 50; // Keep last 50 hashes per agent
|
|
1850
|
-
// Simple hash function for log dedup
|
|
1851
|
-
const hashLogContent = (content) => {
|
|
1852
|
-
// Normalize whitespace and create a simple hash
|
|
1853
|
-
const normalized = content.replace(/\s+/g, ' ').trim().slice(0, 200);
|
|
1854
|
-
let hash = 0;
|
|
1855
|
-
for (let i = 0; i < normalized.length; i++) {
|
|
1856
|
-
const char = normalized.charCodeAt(i);
|
|
1857
|
-
hash = ((hash << 5) - hash) + char;
|
|
1858
|
-
hash = hash & hash;
|
|
1859
|
-
}
|
|
1860
|
-
return hash.toString(36);
|
|
1861
|
-
};
|
|
1862
|
-
// Function to broadcast log output to subscribed clients
|
|
1863
|
-
const broadcastLogOutput = (agentName, output) => {
|
|
1864
|
-
const clients = logSubscriptions.get(agentName);
|
|
1865
|
-
if (!clients || clients.size === 0)
|
|
1866
|
-
return;
|
|
1867
|
-
// Skip empty or whitespace-only output
|
|
1868
|
-
const trimmed = output.trim();
|
|
1869
|
-
if (!trimmed)
|
|
1870
|
-
return;
|
|
1871
|
-
// Dedup: Check if we've recently broadcast this content
|
|
1872
|
-
const hash = hashLogContent(output);
|
|
1873
|
-
let agentHashes = recentLogHashes.get(agentName);
|
|
1874
|
-
if (!agentHashes) {
|
|
1875
|
-
agentHashes = new Set();
|
|
1876
|
-
recentLogHashes.set(agentName, agentHashes);
|
|
1877
|
-
}
|
|
1878
|
-
if (agentHashes.has(hash)) {
|
|
1879
|
-
// Already broadcast this content recently, skip
|
|
1880
|
-
return;
|
|
1881
|
-
}
|
|
1882
|
-
// Add to rolling window
|
|
1883
|
-
agentHashes.add(hash);
|
|
1884
|
-
if (agentHashes.size > MAX_LOG_HASH_WINDOW) {
|
|
1885
|
-
// Remove oldest entry (first in Set iteration order)
|
|
1886
|
-
const oldest = agentHashes.values().next().value;
|
|
1887
|
-
if (oldest !== undefined) {
|
|
1888
|
-
agentHashes.delete(oldest);
|
|
1889
|
-
}
|
|
1890
|
-
}
|
|
1891
|
-
const payload = JSON.stringify({
|
|
1892
|
-
type: 'output',
|
|
1893
|
-
agent: agentName,
|
|
1894
|
-
data: output,
|
|
1895
|
-
timestamp: new Date().toISOString(),
|
|
1896
|
-
});
|
|
1897
|
-
for (const client of clients) {
|
|
1898
|
-
if (client.readyState === WebSocket.OPEN) {
|
|
1899
|
-
client.send(payload);
|
|
1900
|
-
}
|
|
1901
|
-
}
|
|
1902
|
-
};
|
|
1903
|
-
// Expose broadcastLogOutput for PTY wrappers to call
|
|
1904
|
-
global.__broadcastLogOutput = broadcastLogOutput;
|
|
1905
|
-
// ===== Presence WebSocket Handler =====
|
|
1906
|
-
// Helper to broadcast to all presence clients
|
|
1907
|
-
const broadcastPresence = (message, exclude) => {
|
|
1908
|
-
const payload = JSON.stringify(message);
|
|
1909
|
-
wssPresence.clients.forEach((client) => {
|
|
1910
|
-
if (client !== exclude && client.readyState === WebSocket.OPEN) {
|
|
1911
|
-
client.send(payload);
|
|
1912
|
-
}
|
|
1913
|
-
});
|
|
1914
|
-
};
|
|
1915
|
-
// Helper to broadcast channel messages to all presence clients
|
|
1916
|
-
// This is used by fallback relay clients to forward messages to cloud-connected users
|
|
1917
|
-
const broadcastChannelMessage = (message) => {
|
|
1918
|
-
const payload = JSON.stringify(message);
|
|
1919
|
-
wssPresence.clients.forEach((client) => {
|
|
1920
|
-
if (client.readyState === WebSocket.OPEN) {
|
|
1921
|
-
client.send(payload);
|
|
1922
|
-
}
|
|
1923
|
-
});
|
|
1924
|
-
};
|
|
1925
|
-
// Helper to get online users list (without ws references)
|
|
1926
|
-
const getOnlineUsersList = () => {
|
|
1927
|
-
return Array.from(onlineUsers.values()).map((state) => state.info);
|
|
1928
|
-
};
|
|
1929
|
-
// Heartbeat to detect dead connections (30 seconds)
|
|
1930
|
-
const PRESENCE_HEARTBEAT_INTERVAL = 30000;
|
|
1931
|
-
const presenceHealth = new WeakMap();
|
|
1932
|
-
const presenceHeartbeat = setInterval(() => {
|
|
1933
|
-
wssPresence.clients.forEach((ws) => {
|
|
1934
|
-
const health = presenceHealth.get(ws);
|
|
1935
|
-
if (!health) {
|
|
1936
|
-
presenceHealth.set(ws, { isAlive: true });
|
|
1937
|
-
return;
|
|
1938
|
-
}
|
|
1939
|
-
if (!health.isAlive) {
|
|
1940
|
-
ws.terminate();
|
|
1941
|
-
return;
|
|
1942
|
-
}
|
|
1943
|
-
health.isAlive = false;
|
|
1944
|
-
ws.ping();
|
|
1945
|
-
});
|
|
1946
|
-
}, PRESENCE_HEARTBEAT_INTERVAL);
|
|
1947
|
-
wssPresence.on('close', () => {
|
|
1948
|
-
clearInterval(presenceHeartbeat);
|
|
1949
|
-
});
|
|
1950
|
-
wssPresence.on('connection', (ws) => {
|
|
1951
|
-
// Initialize health tracking (no log - too noisy)
|
|
1952
|
-
presenceHealth.set(ws, { isAlive: true });
|
|
1953
|
-
ws.on('pong', () => {
|
|
1954
|
-
const health = presenceHealth.get(ws);
|
|
1955
|
-
if (health)
|
|
1956
|
-
health.isAlive = true;
|
|
1957
|
-
});
|
|
1958
|
-
let clientUsername;
|
|
1959
|
-
ws.on('message', (data) => {
|
|
1960
|
-
try {
|
|
1961
|
-
const msg = JSON.parse(data.toString());
|
|
1962
|
-
if (msg.type === 'presence') {
|
|
1963
|
-
if (msg.action === 'join' && msg.user?.username) {
|
|
1964
|
-
const username = msg.user.username;
|
|
1965
|
-
const avatarUrl = msg.user.avatarUrl;
|
|
1966
|
-
// Validate inputs
|
|
1967
|
-
if (!isValidUsername(username)) {
|
|
1968
|
-
console.warn(`[dashboard] Invalid username rejected: ${username}`);
|
|
1969
|
-
return;
|
|
1970
|
-
}
|
|
1971
|
-
if (!isValidAvatarUrl(avatarUrl)) {
|
|
1972
|
-
console.warn(`[dashboard] Invalid avatar URL rejected for user ${username}`);
|
|
1973
|
-
return;
|
|
1974
|
-
}
|
|
1975
|
-
clientUsername = username;
|
|
1976
|
-
const now = new Date().toISOString();
|
|
1977
|
-
// Check if user already has connections (multi-tab support)
|
|
1978
|
-
const existing = onlineUsers.get(username);
|
|
1979
|
-
if (existing) {
|
|
1980
|
-
// Add this connection to existing user
|
|
1981
|
-
existing.connections.add(ws);
|
|
1982
|
-
existing.info.lastSeen = now;
|
|
1983
|
-
// Update userBridge to use the new WebSocket for message delivery
|
|
1984
|
-
// This ensures messages are sent to an active connection, not a stale one
|
|
1985
|
-
userBridge.updateWebSocket(username, ws);
|
|
1986
|
-
// Only log at milestones to reduce noise
|
|
1987
|
-
const count = existing.connections.size;
|
|
1988
|
-
if (count === 2 || count === 5 || count === 10 || count % 50 === 0) {
|
|
1989
|
-
console.log(`[dashboard] User ${username} has ${count} connections`);
|
|
1990
|
-
}
|
|
1991
|
-
}
|
|
1992
|
-
else {
|
|
1993
|
-
// New user - create presence state
|
|
1994
|
-
onlineUsers.set(username, {
|
|
1995
|
-
info: {
|
|
1996
|
-
username,
|
|
1997
|
-
avatarUrl,
|
|
1998
|
-
connectedAt: now,
|
|
1999
|
-
lastSeen: now,
|
|
2000
|
-
},
|
|
2001
|
-
connections: new Set([ws]),
|
|
2002
|
-
});
|
|
2003
|
-
console.log(`[dashboard] User ${username} came online`);
|
|
2004
|
-
// Register user with relay daemon for messaging
|
|
2005
|
-
userBridge.registerUser(username, ws, { avatarUrl }).catch((err) => {
|
|
2006
|
-
console.error(`[dashboard] Failed to register user ${username} with relay:`, err);
|
|
2007
|
-
});
|
|
2008
|
-
// Broadcast join to all other clients (only for truly new users)
|
|
2009
|
-
broadcastPresence({
|
|
2010
|
-
type: 'presence_join',
|
|
2011
|
-
user: {
|
|
2012
|
-
username,
|
|
2013
|
-
avatarUrl,
|
|
2014
|
-
connectedAt: now,
|
|
2015
|
-
lastSeen: now,
|
|
2016
|
-
},
|
|
2017
|
-
}, ws);
|
|
2018
|
-
}
|
|
2019
|
-
// Send current online users list to the new client
|
|
2020
|
-
ws.send(JSON.stringify({
|
|
2021
|
-
type: 'presence_list',
|
|
2022
|
-
users: getOnlineUsersList(),
|
|
2023
|
-
}));
|
|
2024
|
-
}
|
|
2025
|
-
else if (msg.action === 'leave') {
|
|
2026
|
-
// Security: Only allow leaving your own username
|
|
2027
|
-
// Must have authenticated first
|
|
2028
|
-
if (!clientUsername) {
|
|
2029
|
-
console.warn(`[dashboard] Security: Unauthenticated leave attempt`);
|
|
2030
|
-
return;
|
|
2031
|
-
}
|
|
2032
|
-
if (msg.username !== clientUsername) {
|
|
2033
|
-
console.warn(`[dashboard] Security: User ${clientUsername} tried to remove ${msg.username}`);
|
|
2034
|
-
return;
|
|
2035
|
-
}
|
|
2036
|
-
// Remove this connection from the user's set
|
|
2037
|
-
const username = clientUsername; // Narrow type for TypeScript
|
|
2038
|
-
const userState = onlineUsers.get(username);
|
|
2039
|
-
if (userState) {
|
|
2040
|
-
userState.connections.delete(ws);
|
|
2041
|
-
// Only broadcast leave if no more connections
|
|
2042
|
-
if (userState.connections.size === 0) {
|
|
2043
|
-
onlineUsers.delete(username);
|
|
2044
|
-
console.log(`[dashboard] User ${username} went offline`);
|
|
2045
|
-
broadcastPresence({
|
|
2046
|
-
type: 'presence_leave',
|
|
2047
|
-
username,
|
|
2048
|
-
});
|
|
2049
|
-
}
|
|
2050
|
-
else {
|
|
2051
|
-
console.log(`[dashboard] User ${username} closed tab (${userState.connections.size} remaining)`);
|
|
2052
|
-
}
|
|
2053
|
-
}
|
|
2054
|
-
}
|
|
2055
|
-
}
|
|
2056
|
-
else if (msg.type === 'typing') {
|
|
2057
|
-
// Must have authenticated first
|
|
2058
|
-
if (!clientUsername) {
|
|
2059
|
-
console.warn(`[dashboard] Security: Unauthenticated typing attempt`);
|
|
2060
|
-
return;
|
|
2061
|
-
}
|
|
2062
|
-
// Validate typing message comes from authenticated user
|
|
2063
|
-
if (msg.username !== clientUsername) {
|
|
2064
|
-
console.warn(`[dashboard] Security: Typing message username mismatch`);
|
|
2065
|
-
return;
|
|
2066
|
-
}
|
|
2067
|
-
// Update last seen
|
|
2068
|
-
const username = clientUsername; // Narrow type for TypeScript
|
|
2069
|
-
const userState = onlineUsers.get(username);
|
|
2070
|
-
if (userState) {
|
|
2071
|
-
userState.info.lastSeen = new Date().toISOString();
|
|
2072
|
-
}
|
|
2073
|
-
// Broadcast typing indicator to all other clients
|
|
2074
|
-
broadcastPresence({
|
|
2075
|
-
type: 'typing',
|
|
2076
|
-
username,
|
|
2077
|
-
avatarUrl: userState?.info.avatarUrl,
|
|
2078
|
-
isTyping: msg.isTyping,
|
|
2079
|
-
}, ws);
|
|
2080
|
-
}
|
|
2081
|
-
else if (msg.type === 'channel_join') {
|
|
2082
|
-
// Join a channel
|
|
2083
|
-
if (!clientUsername) {
|
|
2084
|
-
console.warn(`[dashboard] Security: Unauthenticated channel_join attempt`);
|
|
2085
|
-
return;
|
|
2086
|
-
}
|
|
2087
|
-
if (!msg.channel || typeof msg.channel !== 'string') {
|
|
2088
|
-
console.warn(`[dashboard] Invalid channel_join: missing channel`);
|
|
2089
|
-
return;
|
|
2090
|
-
}
|
|
2091
|
-
userBridge.joinChannel(clientUsername, msg.channel).then((success) => {
|
|
2092
|
-
ws.send(JSON.stringify({
|
|
2093
|
-
type: 'channel_joined',
|
|
2094
|
-
channel: msg.channel,
|
|
2095
|
-
success,
|
|
2096
|
-
}));
|
|
2097
|
-
}).catch((err) => {
|
|
2098
|
-
console.error(`[dashboard] Channel join error:`, err);
|
|
2099
|
-
ws.send(JSON.stringify({
|
|
2100
|
-
type: 'channel_joined',
|
|
2101
|
-
channel: msg.channel,
|
|
2102
|
-
success: false,
|
|
2103
|
-
error: err.message,
|
|
2104
|
-
}));
|
|
2105
|
-
});
|
|
2106
|
-
}
|
|
2107
|
-
else if (msg.type === 'channel_leave') {
|
|
2108
|
-
// Leave a channel
|
|
2109
|
-
if (!clientUsername) {
|
|
2110
|
-
console.warn(`[dashboard] Security: Unauthenticated channel_leave attempt`);
|
|
2111
|
-
return;
|
|
2112
|
-
}
|
|
2113
|
-
if (!msg.channel || typeof msg.channel !== 'string') {
|
|
2114
|
-
console.warn(`[dashboard] Invalid channel_leave: missing channel`);
|
|
2115
|
-
return;
|
|
2116
|
-
}
|
|
2117
|
-
userBridge.leaveChannel(clientUsername, msg.channel).then((success) => {
|
|
2118
|
-
ws.send(JSON.stringify({
|
|
2119
|
-
type: 'channel_left',
|
|
2120
|
-
channel: msg.channel,
|
|
2121
|
-
success,
|
|
2122
|
-
}));
|
|
2123
|
-
}).catch((err) => {
|
|
2124
|
-
console.error(`[dashboard] Channel leave error:`, err);
|
|
2125
|
-
});
|
|
2126
|
-
}
|
|
2127
|
-
else if (msg.type === 'channel_message') {
|
|
2128
|
-
// Send message to channel
|
|
2129
|
-
if (!clientUsername) {
|
|
2130
|
-
console.warn(`[dashboard] Security: Unauthenticated channel_message attempt`);
|
|
2131
|
-
return;
|
|
2132
|
-
}
|
|
2133
|
-
if (!msg.channel || typeof msg.channel !== 'string') {
|
|
2134
|
-
console.warn(`[dashboard] Invalid channel_message: missing channel`);
|
|
2135
|
-
return;
|
|
2136
|
-
}
|
|
2137
|
-
if (!msg.body || typeof msg.body !== 'string') {
|
|
2138
|
-
console.warn(`[dashboard] Invalid channel_message: missing body`);
|
|
2139
|
-
return;
|
|
2140
|
-
}
|
|
2141
|
-
userBridge.sendChannelMessage(clientUsername, msg.channel, msg.body, {
|
|
2142
|
-
thread: msg.thread,
|
|
2143
|
-
}).catch((err) => {
|
|
2144
|
-
console.error(`[dashboard] Channel message error:`, err);
|
|
2145
|
-
});
|
|
2146
|
-
}
|
|
2147
|
-
else if (msg.type === 'direct_message') {
|
|
2148
|
-
// Send direct message to user or agent
|
|
2149
|
-
if (!clientUsername) {
|
|
2150
|
-
console.warn(`[dashboard] Security: Unauthenticated direct_message attempt`);
|
|
2151
|
-
return;
|
|
2152
|
-
}
|
|
2153
|
-
if (!msg.to || typeof msg.to !== 'string') {
|
|
2154
|
-
console.warn(`[dashboard] Invalid direct_message: missing 'to'`);
|
|
2155
|
-
return;
|
|
2156
|
-
}
|
|
2157
|
-
if (!msg.body || typeof msg.body !== 'string') {
|
|
2158
|
-
console.warn(`[dashboard] Invalid direct_message: missing body`);
|
|
2159
|
-
return;
|
|
2160
|
-
}
|
|
2161
|
-
userBridge.sendDirectMessage(clientUsername, msg.to, msg.body, {
|
|
2162
|
-
thread: msg.thread,
|
|
2163
|
-
}).catch((err) => {
|
|
2164
|
-
console.error(`[dashboard] Direct message error:`, err);
|
|
2165
|
-
});
|
|
2166
|
-
}
|
|
2167
|
-
}
|
|
2168
|
-
catch (err) {
|
|
2169
|
-
console.error('[dashboard] Invalid presence message:', err);
|
|
2170
|
-
}
|
|
2171
|
-
});
|
|
2172
|
-
ws.on('error', (err) => {
|
|
2173
|
-
console.error('[dashboard] Presence WebSocket client error:', err);
|
|
2174
|
-
});
|
|
2175
|
-
ws.on('close', () => {
|
|
2176
|
-
// Clean up on disconnect with multi-tab support
|
|
2177
|
-
if (clientUsername) {
|
|
2178
|
-
const userState = onlineUsers.get(clientUsername);
|
|
2179
|
-
if (userState) {
|
|
2180
|
-
userState.connections.delete(ws);
|
|
2181
|
-
// Only broadcast leave if no more connections
|
|
2182
|
-
if (userState.connections.size === 0) {
|
|
2183
|
-
onlineUsers.delete(clientUsername);
|
|
2184
|
-
console.log(`[dashboard] User ${clientUsername} disconnected`);
|
|
2185
|
-
// Unregister from relay daemon
|
|
2186
|
-
userBridge.unregisterUser(clientUsername);
|
|
2187
|
-
broadcastPresence({
|
|
2188
|
-
type: 'presence_leave',
|
|
2189
|
-
username: clientUsername,
|
|
2190
|
-
});
|
|
2191
|
-
}
|
|
2192
|
-
else {
|
|
2193
|
-
console.log(`[dashboard] User ${clientUsername} closed connection (${userState.connections.size} remaining)`);
|
|
2194
|
-
}
|
|
2195
|
-
}
|
|
2196
|
-
}
|
|
2197
|
-
});
|
|
2198
|
-
});
|
|
2199
|
-
app.get('/api/data', (req, res) => {
|
|
2200
|
-
getAllData().then((data) => res.json(data)).catch((err) => {
|
|
2201
|
-
console.error('Failed to fetch dashboard data', err);
|
|
2202
|
-
res.status(500).json({ error: 'Failed to load data' });
|
|
2203
|
-
});
|
|
2204
|
-
});
|
|
2205
|
-
// ===== Channel API =====
|
|
2206
|
-
/**
|
|
2207
|
-
* GET /api/channels - Get list of channels the user has joined
|
|
2208
|
-
*/
|
|
2209
|
-
app.get('/api/channels', async (req, res) => {
|
|
2210
|
-
const username = req.query.username;
|
|
2211
|
-
const workspaceId = resolveWorkspaceId(req);
|
|
2212
|
-
if (!storage) {
|
|
2213
|
-
if (!username) {
|
|
2214
|
-
return res.status(400).json({ error: 'username query param required' });
|
|
2215
|
-
}
|
|
2216
|
-
const channels = userBridge.getUserChannels(username);
|
|
2217
|
-
return res.json({
|
|
2218
|
-
channels: channels.map((id) => ({
|
|
2219
|
-
id,
|
|
2220
|
-
name: id.startsWith('#') ? id.slice(1) : id,
|
|
2221
|
-
visibility: 'public',
|
|
2222
|
-
status: 'active',
|
|
2223
|
-
createdAt: new Date().toISOString(),
|
|
2224
|
-
createdBy: username,
|
|
2225
|
-
memberCount: 0,
|
|
2226
|
-
unreadCount: 0,
|
|
2227
|
-
hasMentions: false,
|
|
2228
|
-
isDm: id.startsWith('dm:'),
|
|
2229
|
-
})),
|
|
2230
|
-
archivedChannels: [],
|
|
2231
|
-
});
|
|
2232
|
-
}
|
|
2233
|
-
try {
|
|
2234
|
-
const channelMap = await loadChannelRecords(workspaceId);
|
|
2235
|
-
const activeChannels = [];
|
|
2236
|
-
const archivedChannels = [];
|
|
2237
|
-
for (const record of channelMap.values()) {
|
|
2238
|
-
const isMember = !username || record.members.has(username) || record.id === '#general';
|
|
2239
|
-
if (!isMember) {
|
|
2240
|
-
continue;
|
|
2241
|
-
}
|
|
2242
|
-
const channel = {
|
|
2243
|
-
id: record.id,
|
|
2244
|
-
name: record.id.startsWith('#') ? record.id.slice(1) : record.id,
|
|
2245
|
-
description: record.description,
|
|
2246
|
-
visibility: record.visibility,
|
|
2247
|
-
status: record.status,
|
|
2248
|
-
createdAt: record.createdAt ? new Date(record.createdAt).toISOString() : new Date(record.lastActivityAt || Date.now()).toISOString(),
|
|
2249
|
-
createdBy: record.createdBy || '__system__',
|
|
2250
|
-
lastActivityAt: record.lastActivityAt ? new Date(record.lastActivityAt).toISOString() : undefined,
|
|
2251
|
-
memberCount: record.members.size,
|
|
2252
|
-
unreadCount: 0,
|
|
2253
|
-
hasMentions: false,
|
|
2254
|
-
lastMessage: record.lastMessage,
|
|
2255
|
-
isDm: record.id.startsWith('dm:'),
|
|
2256
|
-
dmParticipants: record.dmParticipants,
|
|
2257
|
-
};
|
|
2258
|
-
if (record.status === 'archived') {
|
|
2259
|
-
archivedChannels.push(channel);
|
|
2260
|
-
}
|
|
2261
|
-
else {
|
|
2262
|
-
activeChannels.push(channel);
|
|
2263
|
-
}
|
|
2264
|
-
}
|
|
2265
|
-
return res.json({
|
|
2266
|
-
channels: activeChannels,
|
|
2267
|
-
archivedChannels,
|
|
2268
|
-
});
|
|
2269
|
-
}
|
|
2270
|
-
catch (err) {
|
|
2271
|
-
console.error('[channels] Failed to load channels', err);
|
|
2272
|
-
return res.status(500).json({ error: 'Failed to load channels' });
|
|
2273
|
-
}
|
|
2274
|
-
});
|
|
2275
|
-
/**
|
|
2276
|
-
* POST /api/channels - Create a new channel
|
|
2277
|
-
*/
|
|
2278
|
-
app.post('/api/channels', express.json(), async (req, res) => {
|
|
2279
|
-
const { name, description, isPrivate, invites } = req.body;
|
|
2280
|
-
const workspaceId = resolveWorkspaceId(req);
|
|
2281
|
-
const username = req.query.username || req.body.username || 'Dashboard';
|
|
2282
|
-
if (!name) {
|
|
2283
|
-
return res.status(400).json({ error: 'name is required' });
|
|
2284
|
-
}
|
|
2285
|
-
// Normalize channel name
|
|
2286
|
-
const channelId = name.startsWith('#') ? name : `#${name}`;
|
|
2287
|
-
try {
|
|
2288
|
-
// Join the creator to the channel
|
|
2289
|
-
// Note: userBridge.joinChannel triggers router's persistChannelMembership via protocol
|
|
2290
|
-
// We only persist here for dashboard-initiated creates (no daemon connection)
|
|
2291
|
-
await userBridge.joinChannel(username, channelId);
|
|
2292
|
-
await persistChannelMembershipEvent(channelId, username, 'join', { workspaceId });
|
|
2293
|
-
// Handle invites if provided
|
|
2294
|
-
if (invites) {
|
|
2295
|
-
const inviteList = invites.split(',').map((s) => s.trim()).filter(Boolean);
|
|
2296
|
-
for (const invitee of inviteList) {
|
|
2297
|
-
// userBridge.joinChannel handles persistence via protocol
|
|
2298
|
-
await userBridge.joinChannel(invitee, channelId);
|
|
2299
|
-
await persistChannelMembershipEvent(channelId, invitee, 'invite', { invitedBy: username, workspaceId });
|
|
2300
|
-
}
|
|
2301
|
-
}
|
|
2302
|
-
// Persist channel creation as a system message
|
|
2303
|
-
if (storage) {
|
|
2304
|
-
await storage.saveMessage({
|
|
2305
|
-
id: `channel-create-${crypto.randomUUID()}`,
|
|
2306
|
-
ts: Date.now(),
|
|
2307
|
-
from: '__system__',
|
|
2308
|
-
to: channelId,
|
|
2309
|
-
topic: undefined,
|
|
2310
|
-
kind: 'state', // channel creation stored as state
|
|
2311
|
-
body: `Channel created by ${username}`,
|
|
2312
|
-
data: {
|
|
2313
|
-
_channelCreate: {
|
|
2314
|
-
createdBy: username,
|
|
2315
|
-
description,
|
|
2316
|
-
isPrivate: isPrivate ?? false,
|
|
2317
|
-
},
|
|
2318
|
-
...(workspaceId ? { _workspaceId: workspaceId } : {}),
|
|
2319
|
-
},
|
|
2320
|
-
status: 'read',
|
|
2321
|
-
is_urgent: false,
|
|
2322
|
-
is_broadcast: true,
|
|
2323
|
-
});
|
|
2324
|
-
}
|
|
2325
|
-
res.json({
|
|
2326
|
-
channel: {
|
|
2327
|
-
id: channelId,
|
|
2328
|
-
name: name.startsWith('#') ? name.slice(1) : name,
|
|
2329
|
-
description,
|
|
2330
|
-
isPrivate: isPrivate ?? false,
|
|
2331
|
-
createdBy: username,
|
|
2332
|
-
},
|
|
2333
|
-
});
|
|
2334
|
-
}
|
|
2335
|
-
catch (err) {
|
|
2336
|
-
console.error('[channels] Failed to create channel:', err);
|
|
2337
|
-
res.status(500).json({ error: err.message || 'Failed to create channel' });
|
|
2338
|
-
}
|
|
2339
|
-
});
|
|
2340
|
-
/**
|
|
2341
|
-
* POST /api/channels/invite - Invite members to a channel
|
|
2342
|
-
*/
|
|
2343
|
-
app.post('/api/channels/invite', express.json(), async (req, res) => {
|
|
2344
|
-
const { channel, invites, invitedBy } = req.body;
|
|
2345
|
-
const workspaceId = resolveWorkspaceId(req);
|
|
2346
|
-
if (!channel || !invites) {
|
|
2347
|
-
return res.status(400).json({ error: 'channel and invites are required' });
|
|
2348
|
-
}
|
|
2349
|
-
// Don't add '#' prefix to DM channels (they use 'dm:' prefix)
|
|
2350
|
-
const channelId = channel.startsWith('dm:')
|
|
2351
|
-
? channel
|
|
2352
|
-
: (channel.startsWith('#') ? channel : `#${channel}`);
|
|
2353
|
-
const inviteList = invites.split(',').map((s) => s.trim()).filter(Boolean);
|
|
2354
|
-
try {
|
|
2355
|
-
const results = [];
|
|
2356
|
-
for (const invitee of inviteList) {
|
|
2357
|
-
let success = false;
|
|
2358
|
-
let reason;
|
|
2359
|
-
if (userBridge.isUserRegistered(invitee)) {
|
|
2360
|
-
success = await userBridge.joinChannel(invitee, channelId);
|
|
2361
|
-
if (!success) {
|
|
2362
|
-
reason = 'join_failed';
|
|
2363
|
-
}
|
|
2364
|
-
}
|
|
2365
|
-
else {
|
|
2366
|
-
success = true;
|
|
2367
|
-
reason = 'pending';
|
|
2368
|
-
}
|
|
2369
|
-
await persistChannelMembershipEvent(channelId, invitee, 'invite', {
|
|
2370
|
-
invitedBy,
|
|
2371
|
-
workspaceId,
|
|
2372
|
-
});
|
|
2373
|
-
results.push({ username: invitee, success, reason });
|
|
2374
|
-
}
|
|
2375
|
-
res.json({ channel: channelId, invited: results });
|
|
2376
|
-
}
|
|
2377
|
-
catch (err) {
|
|
2378
|
-
console.error('[channels] Failed to invite to channel:', err);
|
|
2379
|
-
res.status(500).json({ error: err.message || 'Failed to invite members' });
|
|
2380
|
-
}
|
|
2381
|
-
});
|
|
2382
|
-
/**
|
|
2383
|
-
* GET /api/channels/users - Get list of registered users
|
|
2384
|
-
*/
|
|
2385
|
-
app.get('/api/channels/users', (_req, res) => {
|
|
2386
|
-
const users = userBridge.getRegisteredUsers();
|
|
2387
|
-
res.json({ users });
|
|
2388
|
-
});
|
|
2389
|
-
/**
|
|
2390
|
-
* POST /api/channels/join - Join a channel
|
|
2391
|
-
*/
|
|
2392
|
-
app.post('/api/channels/join', express.json(), async (req, res) => {
|
|
2393
|
-
console.log('[channels] POST /api/channels/join received:', req.body);
|
|
2394
|
-
const { username, channel } = req.body;
|
|
2395
|
-
if (!username || !channel) {
|
|
2396
|
-
console.log('[channels] Join: missing username or channel');
|
|
2397
|
-
return res.status(400).json({ error: 'username and channel required' });
|
|
2398
|
-
}
|
|
2399
|
-
const workspaceId = resolveWorkspaceId(req);
|
|
2400
|
-
// Don't add '#' prefix to DM channels (they use 'dm:' prefix)
|
|
2401
|
-
const channelId = channel.startsWith('dm:')
|
|
2402
|
-
? channel
|
|
2403
|
-
: (channel.startsWith('#') ? channel : `#${channel}`);
|
|
2404
|
-
let success = false;
|
|
2405
|
-
// Step 1: Try userBridge (for users connected via local WebSocket)
|
|
2406
|
-
const isLocalUser = userBridge.isUserRegistered(username);
|
|
2407
|
-
console.log(`[channels] Join: isLocalUser=${isLocalUser}`);
|
|
2408
|
-
if (isLocalUser) {
|
|
2409
|
-
console.log(`[channels] Calling userBridge.joinChannel(${username}, ${channelId})`);
|
|
2410
|
-
success = await userBridge.joinChannel(username, channelId);
|
|
2411
|
-
console.log(`[channels] userBridge.joinChannel returned: ${success}`);
|
|
2412
|
-
}
|
|
2413
|
-
// Step 2: If not local or userBridge failed, use relay client fallback
|
|
2414
|
-
if (!success) {
|
|
2415
|
-
console.log('[channels] Using relay client fallback for join');
|
|
2416
|
-
try {
|
|
2417
|
-
const client = await getRelayClient(username);
|
|
2418
|
-
console.log(`[channels] Got relay client: ${client ? `state=${client.state}` : 'null'}`);
|
|
2419
|
-
if (client && client.state === 'READY') {
|
|
2420
|
-
success = client.joinChannel(channelId, username);
|
|
2421
|
-
console.log(`[channels] relay client joinChannel returned: ${success}`);
|
|
2422
|
-
}
|
|
2423
|
-
else {
|
|
2424
|
-
console.log('[channels] Relay client not ready or null');
|
|
2425
|
-
}
|
|
2426
|
-
}
|
|
2427
|
-
catch (err) {
|
|
2428
|
-
console.log(`[channels] Relay client error: ${err.message}`);
|
|
2429
|
-
}
|
|
2430
|
-
}
|
|
2431
|
-
if (success) {
|
|
2432
|
-
await persistChannelMembershipEvent(channelId, username, 'join', { workspaceId });
|
|
2433
|
-
}
|
|
2434
|
-
console.log(`[channels] Join final result: success=${success}`);
|
|
2435
|
-
res.json({ success, channel: channelId });
|
|
2436
|
-
});
|
|
2437
|
-
/**
|
|
2438
|
-
* POST /api/channels/leave - Leave a channel
|
|
2439
|
-
*/
|
|
2440
|
-
app.post('/api/channels/leave', express.json(), async (req, res) => {
|
|
2441
|
-
const { username, channel } = req.body;
|
|
2442
|
-
if (!username || !channel) {
|
|
2443
|
-
return res.status(400).json({ error: 'username and channel required' });
|
|
2444
|
-
}
|
|
2445
|
-
const workspaceId = resolveWorkspaceId(req);
|
|
2446
|
-
try {
|
|
2447
|
-
const success = await userBridge.leaveChannel(username, channel);
|
|
2448
|
-
if (success) {
|
|
2449
|
-
await persistChannelMembershipEvent(channel, username, 'leave', { workspaceId });
|
|
2450
|
-
}
|
|
2451
|
-
res.json({ success, channel });
|
|
2452
|
-
}
|
|
2453
|
-
catch (err) {
|
|
2454
|
-
res.status(500).json({ error: err.message });
|
|
2455
|
-
}
|
|
2456
|
-
});
|
|
2457
|
-
/**
|
|
2458
|
-
* POST /api/channels/admin-join - Add a member to a channel (admin operation)
|
|
2459
|
-
* Used by cloud server to sync channel memberships for agents
|
|
2460
|
-
*/
|
|
2461
|
-
app.post('/api/channels/admin-join', express.json(), async (req, res) => {
|
|
2462
|
-
const { channel, member } = req.body;
|
|
2463
|
-
if (!channel || !member) {
|
|
2464
|
-
return res.status(400).json({ error: 'channel and member required' });
|
|
2465
|
-
}
|
|
2466
|
-
const workspaceId = resolveWorkspaceId(req);
|
|
2467
|
-
try {
|
|
2468
|
-
console.log(`[channels] Admin join: ${member} -> ${channel}`);
|
|
2469
|
-
const success = await userBridge.adminJoinChannel(channel, member);
|
|
2470
|
-
if (success) {
|
|
2471
|
-
await persistChannelMembershipEvent(channel, member, 'join', { workspaceId });
|
|
2472
|
-
}
|
|
2473
|
-
// Check if member is connected (warning for unconnected members)
|
|
2474
|
-
let warning;
|
|
2475
|
-
const connectedAgentsPath = path.join(teamDir, 'connected-agents.json');
|
|
2476
|
-
try {
|
|
2477
|
-
if (fs.existsSync(connectedAgentsPath)) {
|
|
2478
|
-
const data = JSON.parse(fs.readFileSync(connectedAgentsPath, 'utf-8'));
|
|
2479
|
-
const connectedAgents = data.agents || [];
|
|
2480
|
-
const connectedUsers = data.users || [];
|
|
2481
|
-
const allConnected = [...connectedAgents, ...connectedUsers];
|
|
2482
|
-
// Case-insensitive check
|
|
2483
|
-
const isConnected = allConnected.some((name) => name.toLowerCase() === member.toLowerCase());
|
|
2484
|
-
if (!isConnected) {
|
|
2485
|
-
warning = `Member "${member}" is not currently connected to the daemon. Messages sent to this channel will not be delivered until the agent connects.`;
|
|
2486
|
-
console.log(`[channels] Warning: ${member} added to ${channel} but not connected`);
|
|
2487
|
-
}
|
|
2488
|
-
}
|
|
2489
|
-
}
|
|
2490
|
-
catch {
|
|
2491
|
-
// Ignore errors reading connected-agents.json
|
|
2492
|
-
}
|
|
2493
|
-
res.json({ success, channel, member, warning });
|
|
2494
|
-
}
|
|
2495
|
-
catch (err) {
|
|
2496
|
-
console.error('[channels] Admin join failed:', err.message);
|
|
2497
|
-
res.status(500).json({ error: err.message });
|
|
2498
|
-
}
|
|
2499
|
-
});
|
|
2500
|
-
/**
|
|
2501
|
-
* POST /api/channels/admin-remove - Remove a member from a channel (admin operation)
|
|
2502
|
-
* Used by dashboard to remove members from channels
|
|
2503
|
-
*/
|
|
2504
|
-
app.post('/api/channels/admin-remove', express.json(), async (req, res) => {
|
|
2505
|
-
const { channel, member } = req.body;
|
|
2506
|
-
if (!channel || !member) {
|
|
2507
|
-
return res.status(400).json({ error: 'channel and member required' });
|
|
2508
|
-
}
|
|
2509
|
-
const workspaceId = resolveWorkspaceId(req);
|
|
2510
|
-
try {
|
|
2511
|
-
console.log(`[channels] Admin remove: ${member} <- ${channel}`);
|
|
2512
|
-
const success = await userBridge.adminRemoveMember(channel, member);
|
|
2513
|
-
if (success) {
|
|
2514
|
-
await persistChannelMembershipEvent(channel, member, 'leave', { workspaceId });
|
|
2515
|
-
}
|
|
2516
|
-
res.json({ success, channel, member });
|
|
2517
|
-
}
|
|
2518
|
-
catch (err) {
|
|
2519
|
-
console.error('[channels] Admin remove failed:', err.message);
|
|
2520
|
-
res.status(500).json({ error: err.message });
|
|
2521
|
-
}
|
|
2522
|
-
});
|
|
2523
|
-
/**
|
|
2524
|
-
* GET /api/channels/:channel/members - Get members of a channel
|
|
2525
|
-
*/
|
|
2526
|
-
app.get('/api/channels/:channel/members', async (req, res) => {
|
|
2527
|
-
const channelId = req.params.channel.startsWith('#') ? req.params.channel : `#${req.params.channel}`;
|
|
2528
|
-
const workspaceId = resolveWorkspaceId(req);
|
|
2529
|
-
try {
|
|
2530
|
-
// Get persisted members from storage
|
|
2531
|
-
const channelMap = await loadChannelRecords(workspaceId);
|
|
2532
|
-
const record = channelMap.get(channelId);
|
|
2533
|
-
// Get online agents from agents.json
|
|
2534
|
-
const agentsPath = path.join(teamDir, 'agents.json');
|
|
2535
|
-
const onlineAgents = [];
|
|
2536
|
-
const thirtySecondsAgo = Date.now() - 30 * 1000;
|
|
2537
|
-
if (fs.existsSync(agentsPath)) {
|
|
2538
|
-
try {
|
|
2539
|
-
const data = JSON.parse(fs.readFileSync(agentsPath, 'utf-8'));
|
|
2540
|
-
for (const agent of (data.agents || [])) {
|
|
2541
|
-
if (agent.lastSeen && new Date(agent.lastSeen).getTime() > thirtySecondsAgo) {
|
|
2542
|
-
onlineAgents.push(agent.name);
|
|
2543
|
-
}
|
|
2544
|
-
}
|
|
2545
|
-
}
|
|
2546
|
-
catch {
|
|
2547
|
-
// Ignore parse errors
|
|
2548
|
-
}
|
|
2549
|
-
}
|
|
2550
|
-
// Get connected users from userBridge
|
|
2551
|
-
const connectedUsers = userBridge.getRegisteredUsers();
|
|
2552
|
-
// Build member list
|
|
2553
|
-
const memberSet = new Set();
|
|
2554
|
-
// Add persisted members
|
|
2555
|
-
if (record?.members) {
|
|
2556
|
-
for (const member of record.members) {
|
|
2557
|
-
memberSet.add(member);
|
|
2558
|
-
}
|
|
2559
|
-
}
|
|
2560
|
-
// For #general, also add all connected agents and users
|
|
2561
|
-
if (channelId === '#general') {
|
|
2562
|
-
for (const agent of onlineAgents) {
|
|
2563
|
-
memberSet.add(agent);
|
|
2564
|
-
}
|
|
2565
|
-
for (const user of connectedUsers) {
|
|
2566
|
-
memberSet.add(user);
|
|
2567
|
-
}
|
|
2568
|
-
}
|
|
2569
|
-
// Build response with entity type info
|
|
2570
|
-
const members = Array.from(memberSet).map((name) => {
|
|
2571
|
-
const isOnlineAgent = onlineAgents.includes(name);
|
|
2572
|
-
const isOnlineUser = connectedUsers.includes(name);
|
|
2573
|
-
return {
|
|
2574
|
-
id: name,
|
|
2575
|
-
displayName: name,
|
|
2576
|
-
entityType: isOnlineUser ? 'user' : 'agent',
|
|
2577
|
-
role: 'member',
|
|
2578
|
-
status: isOnlineAgent || isOnlineUser ? 'online' : 'offline',
|
|
2579
|
-
joinedAt: new Date().toISOString(),
|
|
2580
|
-
};
|
|
2581
|
-
});
|
|
2582
|
-
return res.json({ members });
|
|
2583
|
-
}
|
|
2584
|
-
catch (err) {
|
|
2585
|
-
console.error('[channels] Failed to get channel members:', err);
|
|
2586
|
-
return res.status(500).json({ error: err.message || 'Failed to get channel members' });
|
|
2587
|
-
}
|
|
2588
|
-
});
|
|
2589
|
-
/**
|
|
2590
|
-
* GET /api/channels/:channel/messages - Get persisted messages for a channel
|
|
2591
|
-
*/
|
|
2592
|
-
app.get('/api/channels/:channel/messages', async (req, res) => {
|
|
2593
|
-
if (!storage) {
|
|
2594
|
-
return res.status(503).json({ error: 'Storage not configured' });
|
|
2595
|
-
}
|
|
2596
|
-
const channelId = req.params.channel;
|
|
2597
|
-
const limit = req.query.limit ? parseInt(req.query.limit, 10) : 200;
|
|
2598
|
-
const beforeTs = req.query.before ? parseInt(req.query.before, 10) : undefined;
|
|
2599
|
-
const workspaceId = resolveWorkspaceId(req);
|
|
2600
|
-
try {
|
|
2601
|
-
const query = {
|
|
2602
|
-
to: channelId,
|
|
2603
|
-
limit,
|
|
2604
|
-
order: 'desc',
|
|
2605
|
-
};
|
|
2606
|
-
if (beforeTs) {
|
|
2607
|
-
query.sinceTs = beforeTs;
|
|
2608
|
-
}
|
|
2609
|
-
let messages = await storage.getMessages(query);
|
|
2610
|
-
// Only include channel messages for this workspace
|
|
2611
|
-
messages = messages.filter((m) => {
|
|
2612
|
-
const data = m.data;
|
|
2613
|
-
if (workspaceId && data?._workspaceId && data._workspaceId !== workspaceId) {
|
|
2614
|
-
return false;
|
|
2615
|
-
}
|
|
2616
|
-
return Boolean(data?._isChannelMessage);
|
|
2617
|
-
});
|
|
2618
|
-
// Sort ascending for UI
|
|
2619
|
-
messages.sort((a, b) => a.ts - b.ts);
|
|
2620
|
-
res.json({
|
|
2621
|
-
messages: messages.map((m) => ({
|
|
2622
|
-
id: m.id,
|
|
2623
|
-
channelId: channelId,
|
|
2624
|
-
from: m.from,
|
|
2625
|
-
fromEntityType: 'user',
|
|
2626
|
-
content: m.body,
|
|
2627
|
-
timestamp: new Date(m.ts).toISOString(),
|
|
2628
|
-
threadId: m.thread || undefined,
|
|
2629
|
-
isRead: true,
|
|
2630
|
-
})),
|
|
2631
|
-
hasMore: messages.length === limit,
|
|
2632
|
-
});
|
|
2633
|
-
}
|
|
2634
|
-
catch (err) {
|
|
2635
|
-
console.error('[channels] Failed to fetch channel messages', err);
|
|
2636
|
-
res.status(500).json({ error: 'Failed to fetch channel messages' });
|
|
2637
|
-
}
|
|
2638
|
-
});
|
|
2639
|
-
/**
|
|
2640
|
-
* POST /api/channels/subscribe - Subscribe a cloud user to channel messages
|
|
2641
|
-
* This creates a relay client for the user so they receive channel messages
|
|
2642
|
-
*/
|
|
2643
|
-
app.post('/api/channels/subscribe', express.json(), async (req, res) => {
|
|
2644
|
-
const { username, channels, workspaceId } = req.body;
|
|
2645
|
-
console.log(`[channel-debug] SUBSCRIBE request: username=${username}, channels=${JSON.stringify(channels)}`);
|
|
2646
|
-
if (!username) {
|
|
2647
|
-
return res.status(400).json({ error: 'username required' });
|
|
2648
|
-
}
|
|
2649
|
-
try {
|
|
2650
|
-
// Get or create a relay client for this user
|
|
2651
|
-
const client = await getRelayClient(username);
|
|
2652
|
-
if (!client) {
|
|
2653
|
-
console.log(`[channel-debug] SUBSCRIBE failed: could not create relay client for ${username}`);
|
|
2654
|
-
return res.status(503).json({ error: 'Could not connect to daemon' });
|
|
2655
|
-
}
|
|
2656
|
-
// Wait for client to be ready
|
|
2657
|
-
let attempts = 0;
|
|
2658
|
-
while (client.state !== 'READY' && attempts < 50) {
|
|
2659
|
-
await new Promise((r) => setTimeout(r, 100));
|
|
2660
|
-
attempts++;
|
|
2661
|
-
}
|
|
2662
|
-
if (client.state !== 'READY') {
|
|
2663
|
-
console.log(`[channel-debug] SUBSCRIBE failed: client not ready for ${username}`);
|
|
2664
|
-
return res.status(503).json({ error: 'Relay client not ready' });
|
|
2665
|
-
}
|
|
2666
|
-
// Join the user to their channels
|
|
2667
|
-
const joinedChannels = [];
|
|
2668
|
-
const channelList = channels || ['#general'];
|
|
2669
|
-
for (const channel of channelList) {
|
|
2670
|
-
// Don't add '#' prefix to DM channels (they use 'dm:' prefix)
|
|
2671
|
-
const channelId = channel.startsWith('dm:')
|
|
2672
|
-
? channel
|
|
2673
|
-
: (channel.startsWith('#') ? channel : `#${channel}`);
|
|
2674
|
-
const joined = client.joinChannel(channelId, username);
|
|
2675
|
-
if (joined) {
|
|
2676
|
-
joinedChannels.push(channelId);
|
|
2677
|
-
}
|
|
2678
|
-
}
|
|
2679
|
-
console.log(`[channel-debug] SUBSCRIBE success: ${username} joined ${joinedChannels.join(', ')}`);
|
|
2680
|
-
res.json({ success: true, channels: joinedChannels });
|
|
2681
|
-
}
|
|
2682
|
-
catch (err) {
|
|
2683
|
-
console.log(`[channel-debug] SUBSCRIBE error: ${err.message}`);
|
|
2684
|
-
res.status(500).json({ error: err.message });
|
|
2685
|
-
}
|
|
2686
|
-
});
|
|
2687
|
-
/**
|
|
2688
|
-
* POST /api/channels/message - Send a message to a channel
|
|
2689
|
-
*/
|
|
2690
|
-
app.post('/api/channels/message', express.json(), async (req, res) => {
|
|
2691
|
-
// Build marker - if you don't see this, you're running old code
|
|
2692
|
-
console.log('[channel-msg] === BUILD v4 === Handler called');
|
|
2693
|
-
const { username, channel, body, thread } = req.body;
|
|
2694
|
-
// DEBUG: Enhanced tracing for DM message routing
|
|
2695
|
-
console.log(`[channel-msg] === MESSAGE TRACE ===`);
|
|
2696
|
-
console.log(`[channel-msg] username=${username}, channel=${channel}`);
|
|
2697
|
-
console.log(`[channel-msg] isDM=${channel?.startsWith('dm:')}, thread=${thread || 'none'}`);
|
|
2698
|
-
console.log(`[channel-msg] body length=${body?.length}, preview: ${body?.substring(0, 100)}...`);
|
|
2699
|
-
if (!username || !channel || !body) {
|
|
2700
|
-
console.log('[channel-msg] Missing required fields');
|
|
2701
|
-
return res.status(400).json({ error: 'username, channel, and body required' });
|
|
2702
|
-
}
|
|
2703
|
-
const workspaceId = resolveWorkspaceId(req);
|
|
2704
|
-
// Don't add '#' prefix to DM channels (they use 'dm:' prefix)
|
|
2705
|
-
const channelId = channel.startsWith('dm:')
|
|
2706
|
-
? channel
|
|
2707
|
-
: (channel.startsWith('#') ? channel : `#${channel}`);
|
|
2708
|
-
// SIMPLE APPROACH: Always try relay client first for sending
|
|
2709
|
-
// userBridge is only useful for users connected via local WebSocket
|
|
2710
|
-
// For cloud-proxied requests, we need to use relay client directly
|
|
2711
|
-
let success = false;
|
|
2712
|
-
// Step 1: Check if user is registered with userBridge (local mode)
|
|
2713
|
-
const isLocalUser = userBridge.isUserRegistered(username);
|
|
2714
|
-
console.log(`[channel-msg] Is local user: ${isLocalUser}`);
|
|
2715
|
-
if (isLocalUser) {
|
|
2716
|
-
// Local user - use userBridge
|
|
2717
|
-
console.log('[channel-msg] Using userBridge (local user)');
|
|
2718
|
-
success = await userBridge.sendChannelMessage(username, channelId, body, {
|
|
2719
|
-
thread,
|
|
2720
|
-
data: workspaceId ? { _workspaceId: workspaceId } : undefined,
|
|
2721
|
-
});
|
|
2722
|
-
console.log(`[channel-msg] userBridge result: ${success}`);
|
|
2723
|
-
}
|
|
2724
|
-
// Step 2: If not local or userBridge failed, use relay client
|
|
2725
|
-
if (!success) {
|
|
2726
|
-
console.log('[channel-msg] Using relay client fallback');
|
|
2727
|
-
try {
|
|
2728
|
-
const client = await getRelayClient(username);
|
|
2729
|
-
console.log(`[channel-msg] Got relay client: ${client ? `state=${client.state}` : 'null'}`);
|
|
2730
|
-
if (client && client.state === 'READY') {
|
|
2731
|
-
// Join the channel first (idempotent)
|
|
2732
|
-
const joinResult = client.joinChannel(channelId, username);
|
|
2733
|
-
console.log(`[channel-msg] Join channel result: ${joinResult}`);
|
|
2734
|
-
// Send the message
|
|
2735
|
-
success = client.sendChannelMessage(channelId, body, {
|
|
2736
|
-
thread,
|
|
2737
|
-
data: workspaceId ? { _workspaceId: workspaceId } : undefined,
|
|
2738
|
-
});
|
|
2739
|
-
console.log(`[channel-msg] sendChannelMessage result: ${success}`);
|
|
2740
|
-
}
|
|
2741
|
-
else {
|
|
2742
|
-
console.log('[channel-msg] Relay client not ready or null');
|
|
2743
|
-
}
|
|
2744
|
-
}
|
|
2745
|
-
catch (err) {
|
|
2746
|
-
console.log(`[channel-msg] Relay client error: ${err.message}`);
|
|
2747
|
-
}
|
|
2748
|
-
}
|
|
2749
|
-
console.log(`[channel-msg] Final result: success=${success}`);
|
|
2750
|
-
res.json({ success });
|
|
2751
|
-
});
|
|
2752
|
-
/**
|
|
2753
|
-
* POST /api/channels/archive - Mark a channel as archived (persisted in storage)
|
|
2754
|
-
*/
|
|
2755
|
-
app.post('/api/channels/archive', express.json(), async (req, res) => {
|
|
2756
|
-
if (!storage) {
|
|
2757
|
-
return res.status(503).json({ error: 'Storage not configured' });
|
|
2758
|
-
}
|
|
2759
|
-
const { channel } = req.body;
|
|
2760
|
-
if (!channel) {
|
|
2761
|
-
return res.status(400).json({ error: 'channel required' });
|
|
2762
|
-
}
|
|
2763
|
-
const workspaceId = resolveWorkspaceId(req);
|
|
2764
|
-
try {
|
|
2765
|
-
await storage.saveMessage({
|
|
2766
|
-
id: `state-${Date.now()}`,
|
|
2767
|
-
ts: Date.now(),
|
|
2768
|
-
from: '__system__',
|
|
2769
|
-
to: channel,
|
|
2770
|
-
topic: undefined,
|
|
2771
|
-
kind: 'message',
|
|
2772
|
-
body: 'STATE:archived',
|
|
2773
|
-
data: {
|
|
2774
|
-
_channelState: 'archived',
|
|
2775
|
-
...(workspaceId ? { _workspaceId: workspaceId } : {}),
|
|
2776
|
-
},
|
|
2777
|
-
status: 'read',
|
|
2778
|
-
is_urgent: false,
|
|
2779
|
-
is_broadcast: true,
|
|
2780
|
-
});
|
|
2781
|
-
res.json({ success: true });
|
|
2782
|
-
}
|
|
2783
|
-
catch (err) {
|
|
2784
|
-
console.error('[channels] Failed to archive channel', err);
|
|
2785
|
-
res.status(500).json({ error: 'Failed to archive channel' });
|
|
2786
|
-
}
|
|
2787
|
-
});
|
|
2788
|
-
/**
|
|
2789
|
-
* POST /api/channels/unarchive - Mark a channel as active (persisted in storage)
|
|
2790
|
-
*/
|
|
2791
|
-
app.post('/api/channels/unarchive', express.json(), async (req, res) => {
|
|
2792
|
-
if (!storage) {
|
|
2793
|
-
return res.status(503).json({ error: 'Storage not configured' });
|
|
2794
|
-
}
|
|
2795
|
-
const { channel } = req.body;
|
|
2796
|
-
if (!channel) {
|
|
2797
|
-
return res.status(400).json({ error: 'channel required' });
|
|
2798
|
-
}
|
|
2799
|
-
const workspaceId = resolveWorkspaceId(req);
|
|
2800
|
-
try {
|
|
2801
|
-
await storage.saveMessage({
|
|
2802
|
-
id: `state-${Date.now()}`,
|
|
2803
|
-
ts: Date.now(),
|
|
2804
|
-
from: '__system__',
|
|
2805
|
-
to: channel,
|
|
2806
|
-
topic: undefined,
|
|
2807
|
-
kind: 'message',
|
|
2808
|
-
body: 'STATE:active',
|
|
2809
|
-
data: {
|
|
2810
|
-
_channelState: 'active',
|
|
2811
|
-
...(workspaceId ? { _workspaceId: workspaceId } : {}),
|
|
2812
|
-
},
|
|
2813
|
-
status: 'read',
|
|
2814
|
-
is_urgent: false,
|
|
2815
|
-
is_broadcast: true,
|
|
2816
|
-
});
|
|
2817
|
-
res.json({ success: true });
|
|
2818
|
-
}
|
|
2819
|
-
catch (err) {
|
|
2820
|
-
console.error('[channels] Failed to unarchive channel', err);
|
|
2821
|
-
res.status(500).json({ error: 'Failed to unarchive channel' });
|
|
2822
|
-
}
|
|
2823
|
-
});
|
|
2824
|
-
/**
|
|
2825
|
-
* POST /api/dm - Send a direct message
|
|
2826
|
-
*/
|
|
2827
|
-
app.post('/api/dm', express.json(), async (req, res) => {
|
|
2828
|
-
const { from, to, body, thread } = req.body;
|
|
2829
|
-
if (!from || !to || !body) {
|
|
2830
|
-
return res.status(400).json({ error: 'from, to, and body required' });
|
|
2831
|
-
}
|
|
2832
|
-
try {
|
|
2833
|
-
const success = await userBridge.sendDirectMessage(from, to, body, { thread });
|
|
2834
|
-
res.json({ success });
|
|
2835
|
-
}
|
|
2836
|
-
catch (err) {
|
|
2837
|
-
res.status(500).json({ error: err.message });
|
|
2838
|
-
}
|
|
2839
|
-
});
|
|
2840
|
-
// ===== Health Check API =====
|
|
2841
|
-
/**
|
|
2842
|
-
* GET /health - Health check endpoint for monitoring
|
|
2843
|
-
* Returns 200 if the daemon is healthy
|
|
2844
|
-
*/
|
|
2845
|
-
app.get('/health', async (req, res) => {
|
|
2846
|
-
const uptime = process.uptime();
|
|
2847
|
-
const memUsage = process.memoryUsage();
|
|
2848
|
-
const socketExists = fs.existsSync(socketPath);
|
|
2849
|
-
// Check relay client connectivity (check if default Dashboard client is connected)
|
|
2850
|
-
const defaultClient = relayClients.get('Dashboard');
|
|
2851
|
-
const relayConnected = defaultClient?.state === 'READY';
|
|
2852
|
-
// If socket doesn't exist, daemon may not be running properly
|
|
2853
|
-
if (!socketExists) {
|
|
2854
|
-
return res.status(503).json({
|
|
2855
|
-
status: 'unhealthy',
|
|
2856
|
-
reason: 'Relay socket not found',
|
|
2857
|
-
uptime,
|
|
2858
|
-
memoryMB: Math.round(memUsage.heapUsed / 1024 / 1024),
|
|
2859
|
-
});
|
|
2860
|
-
}
|
|
2861
|
-
res.json({
|
|
2862
|
-
status: 'healthy',
|
|
2863
|
-
uptime,
|
|
2864
|
-
memoryMB: Math.round(memUsage.heapUsed / 1024 / 1024),
|
|
2865
|
-
relayConnected,
|
|
2866
|
-
websocketClients: wss.clients.size,
|
|
2867
|
-
});
|
|
2868
|
-
});
|
|
2869
|
-
/**
|
|
2870
|
-
* GET /api/health - Alternative health endpoint (same as /health)
|
|
2871
|
-
*/
|
|
2872
|
-
app.get('/api/health', async (req, res) => {
|
|
2873
|
-
const uptime = process.uptime();
|
|
2874
|
-
const memUsage = process.memoryUsage();
|
|
2875
|
-
const socketExists = fs.existsSync(socketPath);
|
|
2876
|
-
const defaultClient = relayClients.get('Dashboard');
|
|
2877
|
-
const relayConnected = defaultClient?.state === 'READY';
|
|
2878
|
-
if (!socketExists) {
|
|
2879
|
-
return res.status(503).json({
|
|
2880
|
-
status: 'unhealthy',
|
|
2881
|
-
reason: 'Relay socket not found',
|
|
2882
|
-
uptime,
|
|
2883
|
-
memoryMB: Math.round(memUsage.heapUsed / 1024 / 1024),
|
|
2884
|
-
});
|
|
2885
|
-
}
|
|
2886
|
-
res.json({
|
|
2887
|
-
status: 'healthy',
|
|
2888
|
-
uptime,
|
|
2889
|
-
memoryMB: Math.round(memUsage.heapUsed / 1024 / 1024),
|
|
2890
|
-
relayConnected,
|
|
2891
|
-
websocketClients: wss.clients.size,
|
|
2892
|
-
});
|
|
2893
|
-
});
|
|
2894
|
-
/**
|
|
2895
|
-
* GET /keep-alive - Keep-alive endpoint for Fly.io idle prevention
|
|
2896
|
-
* Called by cloud server when workspace has active agents running.
|
|
2897
|
-
* This inbound request counts as activity for Fly.io's request-based
|
|
2898
|
-
* concurrency tracking, preventing the machine from being idled.
|
|
2899
|
-
*/
|
|
2900
|
-
app.get('/keep-alive', (req, res) => {
|
|
2901
|
-
// Count online agents (seen within last 30 seconds)
|
|
2902
|
-
let activeAgents = 0;
|
|
2903
|
-
const agentsPath = path.join(teamDir, 'agents.json');
|
|
2904
|
-
if (fs.existsSync(agentsPath)) {
|
|
2905
|
-
try {
|
|
2906
|
-
const data = JSON.parse(fs.readFileSync(agentsPath, 'utf-8'));
|
|
2907
|
-
const thirtySecondsAgo = Date.now() - 30 * 1000;
|
|
2908
|
-
activeAgents = (data.agents || []).filter((a) => {
|
|
2909
|
-
if (!a.lastSeen)
|
|
2910
|
-
return false;
|
|
2911
|
-
return new Date(a.lastSeen).getTime() > thirtySecondsAgo;
|
|
2912
|
-
}).length;
|
|
2913
|
-
}
|
|
2914
|
-
catch {
|
|
2915
|
-
// Ignore parse errors
|
|
2916
|
-
}
|
|
2917
|
-
}
|
|
2918
|
-
res.json({
|
|
2919
|
-
ok: true,
|
|
2920
|
-
activeAgents,
|
|
2921
|
-
timestamp: Date.now(),
|
|
2922
|
-
});
|
|
2923
|
-
});
|
|
2924
|
-
// ===== CLI Auth API (for workspace-based provider authentication) =====
|
|
2925
|
-
/**
|
|
2926
|
-
* POST /auth/cli/:provider/start - Start CLI auth flow
|
|
2927
|
-
* Body: { useDeviceFlow?: boolean, userId?: string }
|
|
2928
|
-
*
|
|
2929
|
-
* When userId is provided, credentials are stored per-user at /data/users/{userId}/.{provider}/
|
|
2930
|
-
* This allows multiple users to share a workspace with their own CLI credentials.
|
|
2931
|
-
*/
|
|
2932
|
-
app.post('/auth/cli/:provider/start', async (req, res) => {
|
|
2933
|
-
const { provider } = req.params;
|
|
2934
|
-
const { useDeviceFlow, userId } = req.body || {};
|
|
2935
|
-
try {
|
|
2936
|
-
const session = await startCLIAuth(provider, { useDeviceFlow, userId });
|
|
2937
|
-
res.json({
|
|
2938
|
-
sessionId: session.id,
|
|
2939
|
-
status: session.status,
|
|
2940
|
-
authUrl: session.authUrl,
|
|
2941
|
-
});
|
|
2942
|
-
}
|
|
2943
|
-
catch (err) {
|
|
2944
|
-
res.status(400).json({
|
|
2945
|
-
error: err instanceof Error ? err.message : 'Failed to start CLI auth',
|
|
2946
|
-
});
|
|
2947
|
-
}
|
|
2948
|
-
});
|
|
2949
|
-
/**
|
|
2950
|
-
* GET /auth/cli/:provider/status/:sessionId - Get auth session status
|
|
2951
|
-
*/
|
|
2952
|
-
app.get('/auth/cli/:provider/status/:sessionId', (req, res) => {
|
|
2953
|
-
const { sessionId } = req.params;
|
|
2954
|
-
const session = getAuthSession(sessionId);
|
|
2955
|
-
if (!session) {
|
|
2956
|
-
return res.status(404).json({ error: 'Session not found' });
|
|
2957
|
-
}
|
|
2958
|
-
res.json({
|
|
2959
|
-
status: session.status,
|
|
2960
|
-
authUrl: session.authUrl,
|
|
2961
|
-
error: session.error,
|
|
2962
|
-
});
|
|
2963
|
-
});
|
|
2964
|
-
/**
|
|
2965
|
-
* GET /auth/cli/:provider/creds/:sessionId - Get credentials from completed auth
|
|
2966
|
-
*/
|
|
2967
|
-
app.get('/auth/cli/:provider/creds/:sessionId', (req, res) => {
|
|
2968
|
-
const { sessionId } = req.params;
|
|
2969
|
-
const session = getAuthSession(sessionId);
|
|
2970
|
-
if (!session) {
|
|
2971
|
-
return res.status(404).json({ error: 'Session not found' });
|
|
2972
|
-
}
|
|
2973
|
-
if (session.status !== 'success') {
|
|
2974
|
-
return res.status(400).json({ error: 'Auth not complete', status: session.status });
|
|
2975
|
-
}
|
|
2976
|
-
res.json({
|
|
2977
|
-
token: session.token,
|
|
2978
|
-
refreshToken: session.refreshToken,
|
|
2979
|
-
expiresAt: session.tokenExpiresAt?.toISOString(),
|
|
2980
|
-
});
|
|
2981
|
-
});
|
|
2982
|
-
/**
|
|
2983
|
-
* POST /auth/cli/:provider/cancel/:sessionId - Cancel auth session
|
|
2984
|
-
*/
|
|
2985
|
-
app.post('/auth/cli/:provider/cancel/:sessionId', (req, res) => {
|
|
2986
|
-
const { sessionId } = req.params;
|
|
2987
|
-
const cancelled = cancelAuthSession(sessionId);
|
|
2988
|
-
if (!cancelled) {
|
|
2989
|
-
return res.status(404).json({ error: 'Session not found' });
|
|
2990
|
-
}
|
|
2991
|
-
res.json({ success: true });
|
|
2992
|
-
});
|
|
2993
|
-
/**
|
|
2994
|
-
* POST /auth/cli/:provider/code/:sessionId - Submit auth code to PTY
|
|
2995
|
-
* Used when OAuth returns a code that must be pasted into the CLI
|
|
2996
|
-
*/
|
|
2997
|
-
app.post('/auth/cli/:provider/code/:sessionId', async (req, res) => {
|
|
2998
|
-
const { provider, sessionId } = req.params;
|
|
2999
|
-
const { code } = req.body;
|
|
3000
|
-
console.log('[cli-auth] Auth code submission received', { provider, sessionId, codeLength: code?.length });
|
|
3001
|
-
if (!code || typeof code !== 'string') {
|
|
3002
|
-
return res.status(400).json({ error: 'Auth code is required' });
|
|
3003
|
-
}
|
|
3004
|
-
try {
|
|
3005
|
-
const result = await submitAuthCode(sessionId, code);
|
|
3006
|
-
console.log('[cli-auth] Auth code submission result', { provider, sessionId, result });
|
|
3007
|
-
if (!result.success) {
|
|
3008
|
-
// Use 400 for all errors since they can be retried
|
|
3009
|
-
return res.status(400).json({
|
|
3010
|
-
error: result.error || 'Session not found or process not running',
|
|
3011
|
-
needsRestart: result.needsRestart ?? true,
|
|
3012
|
-
});
|
|
3013
|
-
}
|
|
3014
|
-
// Wait a few seconds for CLI to process and write credentials
|
|
3015
|
-
// The 1s delay in submitAuthCode + CLI processing time means credentials
|
|
3016
|
-
// should be available within 3-5 seconds
|
|
3017
|
-
let sessionStatus = 'waiting_auth';
|
|
3018
|
-
for (let i = 0; i < 10; i++) {
|
|
3019
|
-
await new Promise(resolve => setTimeout(resolve, 500));
|
|
3020
|
-
const session = getAuthSession(sessionId);
|
|
3021
|
-
if (session?.status === 'success') {
|
|
3022
|
-
sessionStatus = 'success';
|
|
3023
|
-
console.log('[cli-auth] Credentials found after code submission', { provider, sessionId, attempt: i + 1 });
|
|
3024
|
-
break;
|
|
3025
|
-
}
|
|
3026
|
-
if (session?.status === 'error') {
|
|
3027
|
-
sessionStatus = 'error';
|
|
3028
|
-
break;
|
|
3029
|
-
}
|
|
3030
|
-
}
|
|
3031
|
-
res.json({
|
|
3032
|
-
success: true,
|
|
3033
|
-
message: 'Auth code submitted',
|
|
3034
|
-
status: sessionStatus,
|
|
3035
|
-
});
|
|
3036
|
-
}
|
|
3037
|
-
catch (err) {
|
|
3038
|
-
console.error('[cli-auth] Auth code submission error', { provider, sessionId, error: String(err) });
|
|
3039
|
-
return res.status(500).json({
|
|
3040
|
-
error: 'Internal error submitting auth code. Please try again.',
|
|
3041
|
-
needsRestart: true,
|
|
3042
|
-
});
|
|
3043
|
-
}
|
|
3044
|
-
});
|
|
3045
|
-
/**
|
|
3046
|
-
* POST /auth/cli/:provider/complete/:sessionId - Complete auth
|
|
3047
|
-
* For providers like Claude: just polls for credentials
|
|
3048
|
-
* For providers like Codex: accepts authCode (redirect URL) and extracts the code
|
|
3049
|
-
*/
|
|
3050
|
-
app.post('/auth/cli/:provider/complete/:sessionId', async (req, res) => {
|
|
3051
|
-
const { sessionId } = req.params;
|
|
3052
|
-
const { authCode } = req.body || {};
|
|
3053
|
-
// If authCode provided, try to extract code and submit it
|
|
3054
|
-
if (authCode && typeof authCode === 'string') {
|
|
3055
|
-
let code = authCode;
|
|
3056
|
-
// If it's a URL, extract the code parameter
|
|
3057
|
-
if (authCode.startsWith('http')) {
|
|
3058
|
-
try {
|
|
3059
|
-
const url = new URL(authCode);
|
|
3060
|
-
const codeParam = url.searchParams.get('code');
|
|
3061
|
-
if (codeParam) {
|
|
3062
|
-
code = codeParam;
|
|
3063
|
-
}
|
|
3064
|
-
}
|
|
3065
|
-
catch {
|
|
3066
|
-
// Not a valid URL, use as-is
|
|
3067
|
-
}
|
|
3068
|
-
}
|
|
3069
|
-
// Submit the code to the CLI process
|
|
3070
|
-
const submitResult = await submitAuthCode(sessionId, code);
|
|
3071
|
-
if (!submitResult.success) {
|
|
3072
|
-
return res.status(400).json({
|
|
3073
|
-
error: submitResult.error,
|
|
3074
|
-
needsRestart: submitResult.needsRestart,
|
|
3075
|
-
});
|
|
3076
|
-
}
|
|
3077
|
-
// Wait a moment for credentials to be written
|
|
3078
|
-
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
3079
|
-
}
|
|
3080
|
-
// Poll for credentials
|
|
3081
|
-
const result = await completeAuthSession(sessionId);
|
|
3082
|
-
if (!result.success) {
|
|
3083
|
-
return res.status(400).json({ error: result.error });
|
|
3084
|
-
}
|
|
3085
|
-
res.json({ success: true, message: 'Authentication complete' });
|
|
3086
|
-
});
|
|
3087
|
-
/**
|
|
3088
|
-
* GET /auth/cli/providers - List supported providers
|
|
3089
|
-
*/
|
|
3090
|
-
app.get('/auth/cli/providers', (req, res) => {
|
|
3091
|
-
res.json({ providers: getSupportedProviders() });
|
|
3092
|
-
});
|
|
3093
|
-
/**
|
|
3094
|
-
* GET /auth/cli/openai/check - Check if OpenAI/Codex is authenticated
|
|
3095
|
-
* Used by the codex-auth CLI helper to detect when auth completes
|
|
3096
|
-
*/
|
|
3097
|
-
app.get('/auth/cli/openai/check', async (req, res) => {
|
|
3098
|
-
try {
|
|
3099
|
-
// Get userId from query params for per-user credential checking
|
|
3100
|
-
// Multiple users can share a workspace, each with their own CLI credentials
|
|
3101
|
-
const userId = req.query.userId;
|
|
3102
|
-
let credPath;
|
|
3103
|
-
if (userId) {
|
|
3104
|
-
// Per-user credential path: /data/users/{userId}/.codex/auth.json
|
|
3105
|
-
const dataDir = process.env.AGENT_RELAY_DATA_DIR || '/data';
|
|
3106
|
-
credPath = path.join(dataDir, 'users', userId, '.codex', 'auth.json');
|
|
3107
|
-
}
|
|
3108
|
-
else {
|
|
3109
|
-
// Fallback to workspace-wide path for backwards compatibility
|
|
3110
|
-
const homedir = process.env.HOME || '/home/workspace';
|
|
3111
|
-
credPath = path.join(homedir, '.codex', 'auth.json');
|
|
3112
|
-
}
|
|
3113
|
-
if (!fs.existsSync(credPath)) {
|
|
3114
|
-
return res.json({ authenticated: false });
|
|
3115
|
-
}
|
|
3116
|
-
const creds = JSON.parse(fs.readFileSync(credPath, 'utf-8'));
|
|
3117
|
-
// Check if we have a valid access token or API key
|
|
3118
|
-
// Codex stores tokens in a nested 'tokens' object: { tokens: { access_token, refresh_token } }
|
|
3119
|
-
const hasToken = !!(creds.access_token ||
|
|
3120
|
-
creds.token ||
|
|
3121
|
-
creds.api_key ||
|
|
3122
|
-
creds.OPENAI_API_KEY ||
|
|
3123
|
-
creds.tokens?.access_token ||
|
|
3124
|
-
creds.tokens?.refresh_token);
|
|
3125
|
-
res.json({ authenticated: hasToken });
|
|
3126
|
-
}
|
|
3127
|
-
catch (_error) {
|
|
3128
|
-
// File doesn't exist or is invalid
|
|
3129
|
-
res.json({ authenticated: false });
|
|
3130
|
-
}
|
|
3131
|
-
});
|
|
3132
|
-
// ===== Metrics API =====
|
|
3133
|
-
/**
|
|
3134
|
-
* GET /api/metrics - JSON format metrics for dashboard
|
|
3135
|
-
*/
|
|
3136
|
-
app.get('/api/metrics', async (req, res) => {
|
|
3137
|
-
try {
|
|
3138
|
-
// Read agent registry for message counts
|
|
3139
|
-
const agentsPath = path.join(teamDir, 'agents.json');
|
|
3140
|
-
let agentRecords = [];
|
|
3141
|
-
if (fs.existsSync(agentsPath)) {
|
|
3142
|
-
const data = JSON.parse(fs.readFileSync(agentsPath, 'utf-8'));
|
|
3143
|
-
agentRecords = (data.agents || []).map((a) => ({
|
|
3144
|
-
name: a.name,
|
|
3145
|
-
messagesSent: a.messagesSent ?? 0,
|
|
3146
|
-
messagesReceived: a.messagesReceived ?? 0,
|
|
3147
|
-
firstSeen: a.firstSeen ?? new Date().toISOString(),
|
|
3148
|
-
lastSeen: a.lastSeen ?? new Date().toISOString(),
|
|
3149
|
-
}));
|
|
3150
|
-
}
|
|
3151
|
-
// Get messages for throughput calculation
|
|
3152
|
-
const team = getTeamData();
|
|
3153
|
-
const messages = team ? await getMessages(team.agents) : [];
|
|
3154
|
-
// Get session data for lifecycle metrics
|
|
3155
|
-
const sessions = storage?.getSessions
|
|
3156
|
-
? await storage.getSessions({ limit: 100 })
|
|
3157
|
-
: [];
|
|
3158
|
-
const metrics = computeSystemMetrics(agentRecords, messages, sessions);
|
|
3159
|
-
res.json(metrics);
|
|
3160
|
-
}
|
|
3161
|
-
catch (err) {
|
|
3162
|
-
console.error('Failed to compute metrics', err);
|
|
3163
|
-
res.status(500).json({ error: 'Failed to compute metrics' });
|
|
3164
|
-
}
|
|
3165
|
-
});
|
|
3166
|
-
/**
|
|
3167
|
-
* GET /api/metrics/prometheus - Prometheus exposition format
|
|
3168
|
-
*/
|
|
3169
|
-
app.get('/api/metrics/prometheus', async (req, res) => {
|
|
3170
|
-
try {
|
|
3171
|
-
// Read agent registry for message counts
|
|
3172
|
-
const agentsPath = path.join(teamDir, 'agents.json');
|
|
3173
|
-
let agentRecords = [];
|
|
3174
|
-
if (fs.existsSync(agentsPath)) {
|
|
3175
|
-
const data = JSON.parse(fs.readFileSync(agentsPath, 'utf-8'));
|
|
3176
|
-
agentRecords = (data.agents || []).map((a) => ({
|
|
3177
|
-
name: a.name,
|
|
3178
|
-
messagesSent: a.messagesSent ?? 0,
|
|
3179
|
-
messagesReceived: a.messagesReceived ?? 0,
|
|
3180
|
-
firstSeen: a.firstSeen ?? new Date().toISOString(),
|
|
3181
|
-
lastSeen: a.lastSeen ?? new Date().toISOString(),
|
|
3182
|
-
}));
|
|
3183
|
-
}
|
|
3184
|
-
// Get messages for throughput calculation
|
|
3185
|
-
const team = getTeamData();
|
|
3186
|
-
const messages = team ? await getMessages(team.agents) : [];
|
|
3187
|
-
// Get session data for lifecycle metrics
|
|
3188
|
-
const sessions = storage?.getSessions
|
|
3189
|
-
? await storage.getSessions({ limit: 100 })
|
|
3190
|
-
: [];
|
|
3191
|
-
const metrics = computeSystemMetrics(agentRecords, messages, sessions);
|
|
3192
|
-
const prometheusOutput = formatPrometheusMetrics(metrics);
|
|
3193
|
-
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
|
|
3194
|
-
res.send(prometheusOutput);
|
|
3195
|
-
}
|
|
3196
|
-
catch (err) {
|
|
3197
|
-
console.error('Failed to compute Prometheus metrics', err);
|
|
3198
|
-
res.status(500).send('# Error computing metrics\n');
|
|
3199
|
-
}
|
|
3200
|
-
});
|
|
3201
|
-
// ===== Agent Memory Metrics API =====
|
|
3202
|
-
/**
|
|
3203
|
-
* GET /api/metrics/agents - Detailed agent memory and resource metrics
|
|
3204
|
-
*/
|
|
3205
|
-
app.get('/api/metrics/agents', async (req, res) => {
|
|
3206
|
-
try {
|
|
3207
|
-
const agents = [];
|
|
3208
|
-
// Get metrics from spawner's active workers
|
|
3209
|
-
if (spawner) {
|
|
3210
|
-
const activeWorkers = spawner.getActiveWorkers();
|
|
3211
|
-
for (const worker of activeWorkers) {
|
|
3212
|
-
// Get memory usage via /proc filesystem
|
|
3213
|
-
let rssBytes = 0;
|
|
3214
|
-
const cpuPercent = 0;
|
|
3215
|
-
if (worker.pid) {
|
|
3216
|
-
try {
|
|
3217
|
-
// Use /proc filesystem instead of ps command (not available in all containers)
|
|
3218
|
-
const statusPath = `/proc/${worker.pid}/status`;
|
|
3219
|
-
if (fs.existsSync(statusPath)) {
|
|
3220
|
-
const status = fs.readFileSync(statusPath, 'utf8');
|
|
3221
|
-
// Parse VmRSS (Resident Set Size) from /proc/[pid]/status
|
|
3222
|
-
const rssMatch = status.match(/VmRSS:\s+(\d+)\s+kB/);
|
|
3223
|
-
if (rssMatch) {
|
|
3224
|
-
rssBytes = parseInt(rssMatch[1], 10) * 1024; // Convert kB to bytes
|
|
3225
|
-
}
|
|
3226
|
-
}
|
|
3227
|
-
// Note: CPU percentage requires sampling /proc/[pid]/stat over time
|
|
3228
|
-
// which is more complex. Leaving at 0 for now.
|
|
3229
|
-
}
|
|
3230
|
-
catch {
|
|
3231
|
-
// Process may have exited or /proc not accessible
|
|
3232
|
-
}
|
|
3233
|
-
}
|
|
3234
|
-
agents.push({
|
|
3235
|
-
name: worker.name,
|
|
3236
|
-
pid: worker.pid,
|
|
3237
|
-
status: worker.pid ? 'running' : 'unknown',
|
|
3238
|
-
rssBytes,
|
|
3239
|
-
cpuPercent,
|
|
3240
|
-
trend: 'unknown',
|
|
3241
|
-
alertLevel: rssBytes > 1024 * 1024 * 1024 ? 'critical' :
|
|
3242
|
-
rssBytes > 512 * 1024 * 1024 ? 'warning' : 'normal',
|
|
3243
|
-
highWatermark: rssBytes,
|
|
3244
|
-
uptimeMs: worker.spawnedAt ? Date.now() - worker.spawnedAt : 0,
|
|
3245
|
-
startedAt: worker.spawnedAt ? new Date(worker.spawnedAt).toISOString() : undefined,
|
|
3246
|
-
});
|
|
3247
|
-
}
|
|
3248
|
-
}
|
|
3249
|
-
// Also check agents.json for registered agents that may not be spawned
|
|
3250
|
-
const agentsPath = path.join(teamDir, 'agents.json');
|
|
3251
|
-
if (fs.existsSync(agentsPath)) {
|
|
3252
|
-
const data = JSON.parse(fs.readFileSync(agentsPath, 'utf-8'));
|
|
3253
|
-
const registeredAgents = data.agents || [];
|
|
3254
|
-
for (const agent of registeredAgents) {
|
|
3255
|
-
if (!agents.find(a => a.name === agent.name)) {
|
|
3256
|
-
// Check if recently active (within 30 seconds)
|
|
3257
|
-
const lastSeen = agent.lastSeen ? new Date(agent.lastSeen).getTime() : 0;
|
|
3258
|
-
const isActive = Date.now() - lastSeen < 30000;
|
|
3259
|
-
if (isActive) {
|
|
3260
|
-
agents.push({
|
|
3261
|
-
name: agent.name,
|
|
3262
|
-
status: 'active',
|
|
3263
|
-
alertLevel: 'normal',
|
|
3264
|
-
});
|
|
3265
|
-
}
|
|
3266
|
-
}
|
|
3267
|
-
}
|
|
3268
|
-
}
|
|
3269
|
-
res.json({
|
|
3270
|
-
agents,
|
|
3271
|
-
system: {
|
|
3272
|
-
totalMemory: os.totalmem(),
|
|
3273
|
-
freeMemory: os.freemem(),
|
|
3274
|
-
heapUsed: process.memoryUsage().heapUsed,
|
|
3275
|
-
},
|
|
3276
|
-
});
|
|
3277
|
-
}
|
|
3278
|
-
catch (err) {
|
|
3279
|
-
console.error('Failed to get agent metrics', err);
|
|
3280
|
-
res.status(500).json({ error: 'Failed to get agent metrics' });
|
|
3281
|
-
}
|
|
3282
|
-
});
|
|
3283
|
-
/**
|
|
3284
|
-
* GET /api/metrics/health - System health and crash insights
|
|
3285
|
-
*/
|
|
3286
|
-
app.get('/api/metrics/health', async (req, res) => {
|
|
3287
|
-
try {
|
|
3288
|
-
// Calculate health score based on available data
|
|
3289
|
-
let healthScore = 100;
|
|
3290
|
-
const issues = [];
|
|
3291
|
-
const recommendations = [];
|
|
3292
|
-
const crashes = [];
|
|
3293
|
-
const alerts = [];
|
|
3294
|
-
let agentCount = 0;
|
|
3295
|
-
const totalCrashes24h = 0;
|
|
3296
|
-
let totalAlerts24h = 0;
|
|
3297
|
-
// Get spawned agent count
|
|
3298
|
-
if (spawner) {
|
|
3299
|
-
const workers = spawner.getActiveWorkers();
|
|
3300
|
-
agentCount = workers.length;
|
|
3301
|
-
// Check for high memory usage
|
|
3302
|
-
for (const worker of workers) {
|
|
3303
|
-
if (worker.pid) {
|
|
3304
|
-
try {
|
|
3305
|
-
const { execSync } = await import('child_process');
|
|
3306
|
-
const output = execSync(`ps -o rss= -p ${worker.pid}`, {
|
|
3307
|
-
encoding: 'utf8',
|
|
3308
|
-
timeout: 3000,
|
|
3309
|
-
}).trim();
|
|
3310
|
-
const rssBytes = parseInt(output, 10) * 1024;
|
|
3311
|
-
if (rssBytes > 1.5 * 1024 * 1024 * 1024) {
|
|
3312
|
-
// > 1.5GB
|
|
3313
|
-
healthScore -= 20;
|
|
3314
|
-
issues.push({
|
|
3315
|
-
severity: 'critical',
|
|
3316
|
-
message: `Agent "${worker.name}" is using ${Math.round(rssBytes / 1024 / 1024)}MB of memory`,
|
|
3317
|
-
});
|
|
3318
|
-
totalAlerts24h++;
|
|
3319
|
-
alerts.push({
|
|
3320
|
-
id: `alert-${Date.now()}-${worker.name}`,
|
|
3321
|
-
agentName: worker.name,
|
|
3322
|
-
alertType: 'oom_imminent',
|
|
3323
|
-
message: `Memory usage critical: ${Math.round(rssBytes / 1024 / 1024)}MB`,
|
|
3324
|
-
createdAt: new Date().toISOString(),
|
|
3325
|
-
});
|
|
3326
|
-
}
|
|
3327
|
-
else if (rssBytes > 1024 * 1024 * 1024) {
|
|
3328
|
-
// > 1GB
|
|
3329
|
-
healthScore -= 10;
|
|
3330
|
-
issues.push({
|
|
3331
|
-
severity: 'high',
|
|
3332
|
-
message: `Agent "${worker.name}" memory usage is elevated (${Math.round(rssBytes / 1024 / 1024)}MB)`,
|
|
3333
|
-
});
|
|
3334
|
-
}
|
|
3335
|
-
}
|
|
3336
|
-
catch {
|
|
3337
|
-
// Process may have exited
|
|
3338
|
-
}
|
|
3339
|
-
}
|
|
3340
|
-
}
|
|
3341
|
-
}
|
|
3342
|
-
// Check registered agents
|
|
3343
|
-
const agentsPath = path.join(teamDir, 'agents.json');
|
|
3344
|
-
if (fs.existsSync(agentsPath)) {
|
|
3345
|
-
const data = JSON.parse(fs.readFileSync(agentsPath, 'utf-8'));
|
|
3346
|
-
const registeredAgents = data.agents || [];
|
|
3347
|
-
const activeAgents = registeredAgents.filter((a) => {
|
|
3348
|
-
const lastSeen = a.lastSeen ? new Date(a.lastSeen).getTime() : 0;
|
|
3349
|
-
return Date.now() - lastSeen < 30000;
|
|
3350
|
-
});
|
|
3351
|
-
agentCount = Math.max(agentCount, activeAgents.length);
|
|
3352
|
-
}
|
|
3353
|
-
// Generate recommendations based on issues
|
|
3354
|
-
if (issues.some(i => i.severity === 'critical')) {
|
|
3355
|
-
recommendations.push('Consider restarting agents with high memory usage');
|
|
3356
|
-
recommendations.push('Monitor system resources closely');
|
|
3357
|
-
}
|
|
3358
|
-
if (agentCount === 0) {
|
|
3359
|
-
recommendations.push('No active agents detected - start agents to begin monitoring');
|
|
3360
|
-
}
|
|
3361
|
-
// Clamp health score
|
|
3362
|
-
healthScore = Math.max(0, Math.min(100, healthScore));
|
|
3363
|
-
// Generate summary
|
|
3364
|
-
let summary;
|
|
3365
|
-
if (healthScore >= 90) {
|
|
3366
|
-
summary = 'System is healthy. All agents operating normally.';
|
|
3367
|
-
}
|
|
3368
|
-
else if (healthScore >= 70) {
|
|
3369
|
-
summary = 'Some issues detected. Review warnings and recommendations.';
|
|
3370
|
-
}
|
|
3371
|
-
else if (healthScore >= 50) {
|
|
3372
|
-
summary = 'Multiple issues detected. Action recommended.';
|
|
3373
|
-
}
|
|
3374
|
-
else {
|
|
3375
|
-
summary = 'Critical issues detected. Immediate action required.';
|
|
3376
|
-
}
|
|
3377
|
-
res.json({
|
|
3378
|
-
healthScore,
|
|
3379
|
-
summary,
|
|
3380
|
-
issues,
|
|
3381
|
-
recommendations,
|
|
3382
|
-
crashes,
|
|
3383
|
-
alerts,
|
|
3384
|
-
stats: {
|
|
3385
|
-
totalCrashes24h,
|
|
3386
|
-
totalAlerts24h,
|
|
3387
|
-
agentCount,
|
|
3388
|
-
},
|
|
3389
|
-
});
|
|
3390
|
-
}
|
|
3391
|
-
catch (err) {
|
|
3392
|
-
console.error('Failed to compute health metrics', err);
|
|
3393
|
-
res.status(500).json({ error: 'Failed to compute health metrics' });
|
|
3394
|
-
}
|
|
3395
|
-
});
|
|
3396
|
-
// ===== File Search API =====
|
|
3397
|
-
/**
|
|
3398
|
-
* GET /api/files - Search for files in the repository
|
|
3399
|
-
* Query params:
|
|
3400
|
-
* - q: Search query (file path pattern)
|
|
3401
|
-
* - limit: Max number of results (default 15)
|
|
3402
|
-
*
|
|
3403
|
-
* This endpoint searches for files in the project root directory
|
|
3404
|
-
* to support @-file autocomplete in the message composer.
|
|
3405
|
-
*/
|
|
3406
|
-
app.get('/api/files', async (req, res) => {
|
|
3407
|
-
const query = req.query.q || '';
|
|
3408
|
-
const limit = Math.min(parseInt(req.query.limit, 10) || 15, 50);
|
|
3409
|
-
// Get project root (parent of dataDir, or use projectRoot if available)
|
|
3410
|
-
const searchRoot = options.projectRoot || path.dirname(dataDir);
|
|
3411
|
-
try {
|
|
3412
|
-
const results = await searchFiles(searchRoot, query, limit);
|
|
3413
|
-
res.json({ files: results, query, searchRoot: path.basename(searchRoot) });
|
|
3414
|
-
}
|
|
3415
|
-
catch (err) {
|
|
3416
|
-
console.error('[api] File search error:', err);
|
|
3417
|
-
res.status(500).json({ error: 'Failed to search files', files: [] });
|
|
3418
|
-
}
|
|
3419
|
-
});
|
|
3420
|
-
// Bridge API endpoint - returns multi-project data
|
|
3421
|
-
// This is a placeholder that returns empty data when not in bridge mode
|
|
3422
|
-
// The actual bridge data comes from MultiProjectClient when running `agent-relay bridge`
|
|
3423
|
-
app.get('/api/bridge', async (req, res) => {
|
|
3424
|
-
try {
|
|
3425
|
-
// Check if bridge state file exists (written by bridge command)
|
|
3426
|
-
const bridgeStatePath = path.join(dataDir, 'bridge-state.json');
|
|
3427
|
-
if (fs.existsSync(bridgeStatePath)) {
|
|
3428
|
-
const bridgeData = JSON.parse(fs.readFileSync(bridgeStatePath, 'utf-8'));
|
|
3429
|
-
res.json(bridgeData);
|
|
3430
|
-
}
|
|
3431
|
-
else {
|
|
3432
|
-
// No bridge running - return empty state
|
|
3433
|
-
res.json({
|
|
3434
|
-
projects: [],
|
|
3435
|
-
messages: [],
|
|
3436
|
-
connected: false,
|
|
3437
|
-
});
|
|
3438
|
-
}
|
|
3439
|
-
}
|
|
3440
|
-
catch (err) {
|
|
3441
|
-
console.error('Failed to fetch bridge data', err);
|
|
3442
|
-
res.status(500).json({ error: 'Failed to load bridge data' });
|
|
3443
|
-
}
|
|
3444
|
-
});
|
|
3445
|
-
// ===== Conversation History API =====
|
|
3446
|
-
/**
|
|
3447
|
-
* GET /api/history/sessions - List all sessions with filters
|
|
3448
|
-
* Query params:
|
|
3449
|
-
* - agent: Filter by agent name
|
|
3450
|
-
* - since: Filter sessions started after this timestamp (ms)
|
|
3451
|
-
* - limit: Max number of sessions (default 50)
|
|
3452
|
-
*/
|
|
3453
|
-
app.get('/api/history/sessions', async (req, res) => {
|
|
3454
|
-
if (!storage) {
|
|
3455
|
-
return res.status(503).json({ error: 'Storage not configured' });
|
|
3456
|
-
}
|
|
3457
|
-
try {
|
|
3458
|
-
const query = {};
|
|
3459
|
-
if (req.query.agent && typeof req.query.agent === 'string') {
|
|
3460
|
-
query.agentName = req.query.agent;
|
|
3461
|
-
}
|
|
3462
|
-
if (req.query.since) {
|
|
3463
|
-
query.since = parseInt(req.query.since, 10);
|
|
3464
|
-
}
|
|
3465
|
-
query.limit = req.query.limit ? parseInt(req.query.limit, 10) : 50;
|
|
3466
|
-
const sessions = storage.getSessions
|
|
3467
|
-
? await storage.getSessions(query)
|
|
3468
|
-
: [];
|
|
3469
|
-
const result = sessions.map(s => ({
|
|
3470
|
-
id: s.id,
|
|
3471
|
-
agentName: s.agentName,
|
|
3472
|
-
cli: s.cli,
|
|
3473
|
-
startedAt: new Date(s.startedAt).toISOString(),
|
|
3474
|
-
endedAt: s.endedAt ? new Date(s.endedAt).toISOString() : undefined,
|
|
3475
|
-
duration: formatDuration(s.startedAt, s.endedAt),
|
|
3476
|
-
messageCount: s.messageCount,
|
|
3477
|
-
summary: s.summary,
|
|
3478
|
-
isActive: !s.endedAt,
|
|
3479
|
-
closedBy: s.closedBy,
|
|
3480
|
-
}));
|
|
3481
|
-
res.json({ sessions: result });
|
|
3482
|
-
}
|
|
3483
|
-
catch (err) {
|
|
3484
|
-
console.error('Failed to fetch sessions', err);
|
|
3485
|
-
res.status(500).json({ error: 'Failed to fetch sessions' });
|
|
3486
|
-
}
|
|
3487
|
-
});
|
|
3488
|
-
/**
|
|
3489
|
-
* GET /api/history/messages - Get messages with filters
|
|
3490
|
-
* Query params:
|
|
3491
|
-
* - from: Filter by sender
|
|
3492
|
-
* - to: Filter by recipient
|
|
3493
|
-
* - thread: Filter by thread ID
|
|
3494
|
-
* - since: Filter messages after this timestamp (ms)
|
|
3495
|
-
* - limit: Max number of messages (default 100)
|
|
3496
|
-
* - order: 'asc' or 'desc' (default 'desc')
|
|
3497
|
-
* - search: Search in message body (basic substring match)
|
|
3498
|
-
*/
|
|
3499
|
-
app.get('/api/history/messages', async (req, res) => {
|
|
3500
|
-
if (!storage) {
|
|
3501
|
-
return res.status(503).json({ error: 'Storage not configured' });
|
|
3502
|
-
}
|
|
3503
|
-
try {
|
|
3504
|
-
const query = {};
|
|
3505
|
-
if (req.query.from && typeof req.query.from === 'string') {
|
|
3506
|
-
query.from = req.query.from;
|
|
3507
|
-
}
|
|
3508
|
-
if (req.query.to && typeof req.query.to === 'string') {
|
|
3509
|
-
query.to = req.query.to;
|
|
3510
|
-
}
|
|
3511
|
-
if (req.query.thread && typeof req.query.thread === 'string') {
|
|
3512
|
-
query.thread = req.query.thread;
|
|
3513
|
-
}
|
|
3514
|
-
if (req.query.since) {
|
|
3515
|
-
query.sinceTs = parseInt(req.query.since, 10);
|
|
3516
|
-
}
|
|
3517
|
-
query.limit = req.query.limit ? parseInt(req.query.limit, 10) : 100;
|
|
3518
|
-
query.order = req.query.order || 'desc';
|
|
3519
|
-
let messages = await storage.getMessages(query);
|
|
3520
|
-
// Filter out messages from/to internal system agents (e.g., __spawner__)
|
|
3521
|
-
messages = messages.filter(m => !isInternalAgent(m.from) && !isInternalAgent(m.to));
|
|
3522
|
-
// Client-side search filter (basic substring match)
|
|
3523
|
-
const searchTerm = req.query.search;
|
|
3524
|
-
if (searchTerm && searchTerm.trim()) {
|
|
3525
|
-
const lowerSearch = searchTerm.toLowerCase();
|
|
3526
|
-
messages = messages.filter(m => m.body.toLowerCase().includes(lowerSearch) ||
|
|
3527
|
-
m.from.toLowerCase().includes(lowerSearch) ||
|
|
3528
|
-
m.to.toLowerCase().includes(lowerSearch));
|
|
3529
|
-
}
|
|
3530
|
-
const result = messages.map(m => ({
|
|
3531
|
-
id: m.id,
|
|
3532
|
-
from: m.from,
|
|
3533
|
-
to: m.to,
|
|
3534
|
-
content: m.body,
|
|
3535
|
-
timestamp: new Date(m.ts).toISOString(),
|
|
3536
|
-
thread: m.thread,
|
|
3537
|
-
isBroadcast: m.is_broadcast,
|
|
3538
|
-
isUrgent: m.is_urgent,
|
|
3539
|
-
status: m.status,
|
|
3540
|
-
}));
|
|
3541
|
-
res.json({ messages: result });
|
|
3542
|
-
}
|
|
3543
|
-
catch (err) {
|
|
3544
|
-
console.error('Failed to fetch messages', err);
|
|
3545
|
-
res.status(500).json({ error: 'Failed to fetch messages' });
|
|
3546
|
-
}
|
|
3547
|
-
});
|
|
3548
|
-
/**
|
|
3549
|
-
* GET /api/history/conversations - Get unique conversations (agent pairs)
|
|
3550
|
-
* Returns list of agent pairs that have exchanged messages
|
|
3551
|
-
*/
|
|
3552
|
-
app.get('/api/history/conversations', async (req, res) => {
|
|
3553
|
-
if (!storage) {
|
|
3554
|
-
return res.status(503).json({ error: 'Storage not configured' });
|
|
3555
|
-
}
|
|
3556
|
-
try {
|
|
3557
|
-
// Get all messages to build conversation list
|
|
3558
|
-
const messages = await storage.getMessages({ limit: 1000, order: 'desc' });
|
|
3559
|
-
// Build unique conversation pairs
|
|
3560
|
-
const conversationMap = new Map();
|
|
3561
|
-
for (const msg of messages) {
|
|
3562
|
-
// Skip broadcasts for conversation pairing
|
|
3563
|
-
if (msg.to === '*' || msg.is_broadcast)
|
|
3564
|
-
continue;
|
|
3565
|
-
// Skip messages from/to internal system agents (e.g., __spawner__)
|
|
3566
|
-
if (isInternalAgent(msg.from) || isInternalAgent(msg.to))
|
|
3567
|
-
continue;
|
|
3568
|
-
// Create normalized key (sorted participants)
|
|
3569
|
-
const participants = [msg.from, msg.to].sort();
|
|
3570
|
-
const key = participants.join(':');
|
|
3571
|
-
const existing = conversationMap.get(key);
|
|
3572
|
-
if (existing) {
|
|
3573
|
-
existing.messageCount++;
|
|
3574
|
-
}
|
|
3575
|
-
else {
|
|
3576
|
-
conversationMap.set(key, {
|
|
3577
|
-
participants,
|
|
3578
|
-
lastMessage: msg.body.substring(0, 100),
|
|
3579
|
-
lastTimestamp: new Date(msg.ts).toISOString(),
|
|
3580
|
-
messageCount: 1,
|
|
3581
|
-
});
|
|
3582
|
-
}
|
|
3583
|
-
}
|
|
3584
|
-
// Convert to array sorted by last timestamp
|
|
3585
|
-
const conversations = Array.from(conversationMap.values())
|
|
3586
|
-
.sort((a, b) => new Date(b.lastTimestamp).getTime() - new Date(a.lastTimestamp).getTime());
|
|
3587
|
-
res.json({ conversations });
|
|
3588
|
-
}
|
|
3589
|
-
catch (err) {
|
|
3590
|
-
console.error('Failed to fetch conversations', err);
|
|
3591
|
-
res.status(500).json({ error: 'Failed to fetch conversations' });
|
|
3592
|
-
}
|
|
3593
|
-
});
|
|
3594
|
-
/**
|
|
3595
|
-
* GET /api/history/message/:id - Get a single message by ID
|
|
3596
|
-
*/
|
|
3597
|
-
app.get('/api/history/message/:id', async (req, res) => {
|
|
3598
|
-
if (!storage) {
|
|
3599
|
-
return res.status(503).json({ error: 'Storage not configured' });
|
|
3600
|
-
}
|
|
3601
|
-
try {
|
|
3602
|
-
const { id } = req.params;
|
|
3603
|
-
const message = storage.getMessageById
|
|
3604
|
-
? await storage.getMessageById(id)
|
|
3605
|
-
: null;
|
|
3606
|
-
if (!message) {
|
|
3607
|
-
return res.status(404).json({ error: 'Message not found' });
|
|
3608
|
-
}
|
|
3609
|
-
res.json({
|
|
3610
|
-
id: message.id,
|
|
3611
|
-
from: message.from,
|
|
3612
|
-
to: message.to,
|
|
3613
|
-
content: message.body,
|
|
3614
|
-
timestamp: new Date(message.ts).toISOString(),
|
|
3615
|
-
thread: message.thread,
|
|
3616
|
-
isBroadcast: message.is_broadcast,
|
|
3617
|
-
isUrgent: message.is_urgent,
|
|
3618
|
-
status: message.status,
|
|
3619
|
-
data: message.data,
|
|
3620
|
-
});
|
|
3621
|
-
}
|
|
3622
|
-
catch (err) {
|
|
3623
|
-
console.error('Failed to fetch message', err);
|
|
3624
|
-
res.status(500).json({ error: 'Failed to fetch message' });
|
|
3625
|
-
}
|
|
3626
|
-
});
|
|
3627
|
-
/**
|
|
3628
|
-
* GET /api/history/stats - Get storage statistics
|
|
3629
|
-
*/
|
|
3630
|
-
app.get('/api/history/stats', async (req, res) => {
|
|
3631
|
-
if (!storage) {
|
|
3632
|
-
return res.status(503).json({ error: 'Storage not configured' });
|
|
3633
|
-
}
|
|
3634
|
-
try {
|
|
3635
|
-
// Get stats from SQLite adapter if available
|
|
3636
|
-
if (storage instanceof SqliteStorageAdapter) {
|
|
3637
|
-
const stats = await storage.getStats();
|
|
3638
|
-
const sessions = await storage.getSessions({ limit: 1000 });
|
|
3639
|
-
// Calculate additional stats
|
|
3640
|
-
const activeSessions = sessions.filter(s => !s.endedAt).length;
|
|
3641
|
-
const uniqueAgents = new Set(sessions.map(s => s.agentName)).size;
|
|
3642
|
-
res.json({
|
|
3643
|
-
messageCount: stats.messageCount,
|
|
3644
|
-
sessionCount: stats.sessionCount,
|
|
3645
|
-
activeSessions,
|
|
3646
|
-
uniqueAgents,
|
|
3647
|
-
oldestMessageDate: stats.oldestMessageTs
|
|
3648
|
-
? new Date(stats.oldestMessageTs).toISOString()
|
|
3649
|
-
: null,
|
|
3650
|
-
});
|
|
3651
|
-
}
|
|
3652
|
-
else {
|
|
3653
|
-
// Basic stats for other adapters
|
|
3654
|
-
const messages = await storage.getMessages({ limit: 1 });
|
|
3655
|
-
res.json({
|
|
3656
|
-
messageCount: messages.length > 0 ? 'unknown' : 0,
|
|
3657
|
-
sessionCount: 'unknown',
|
|
3658
|
-
activeSessions: 'unknown',
|
|
3659
|
-
uniqueAgents: 'unknown',
|
|
3660
|
-
});
|
|
3661
|
-
}
|
|
3662
|
-
}
|
|
3663
|
-
catch (err) {
|
|
3664
|
-
console.error('Failed to fetch stats', err);
|
|
3665
|
-
res.status(500).json({ error: 'Failed to fetch stats' });
|
|
3666
|
-
}
|
|
3667
|
-
});
|
|
3668
|
-
// ===== Agent Logs API =====
|
|
3669
|
-
/**
|
|
3670
|
-
* GET /api/logs/:name - Get historical logs for a spawned agent
|
|
3671
|
-
* Query params:
|
|
3672
|
-
* - limit: Max lines to return (default 500)
|
|
3673
|
-
* - raw: If 'true', return raw output instead of cleaned lines
|
|
3674
|
-
*/
|
|
3675
|
-
app.get('/api/logs/:name', (req, res) => {
|
|
3676
|
-
if (!spawner) {
|
|
3677
|
-
return res.status(503).json({ error: 'Spawner not enabled' });
|
|
3678
|
-
}
|
|
3679
|
-
const { name } = req.params;
|
|
3680
|
-
const limit = req.query.limit ? parseInt(req.query.limit, 10) : 500;
|
|
3681
|
-
const raw = req.query.raw === 'true';
|
|
3682
|
-
// Check if worker exists
|
|
3683
|
-
if (!spawner.hasWorker(name)) {
|
|
3684
|
-
return res.status(404).json({ error: `Agent ${name} not found` });
|
|
3685
|
-
}
|
|
3686
|
-
try {
|
|
3687
|
-
if (raw) {
|
|
3688
|
-
const output = spawner.getWorkerRawOutput(name);
|
|
3689
|
-
res.json({
|
|
3690
|
-
name,
|
|
3691
|
-
raw: true,
|
|
3692
|
-
output: output || '',
|
|
3693
|
-
timestamp: new Date().toISOString(),
|
|
3694
|
-
});
|
|
3695
|
-
}
|
|
3696
|
-
else {
|
|
3697
|
-
const lines = spawner.getWorkerOutput(name, limit);
|
|
3698
|
-
res.json({
|
|
3699
|
-
name,
|
|
3700
|
-
raw: false,
|
|
3701
|
-
lines: lines || [],
|
|
3702
|
-
lineCount: lines?.length || 0,
|
|
3703
|
-
timestamp: new Date().toISOString(),
|
|
3704
|
-
});
|
|
3705
|
-
}
|
|
3706
|
-
}
|
|
3707
|
-
catch (err) {
|
|
3708
|
-
console.error(`Failed to get logs for ${name}:`, err);
|
|
3709
|
-
res.status(500).json({ error: 'Failed to get logs' });
|
|
3710
|
-
}
|
|
3711
|
-
});
|
|
3712
|
-
/**
|
|
3713
|
-
* GET /api/logs - List all agents with available logs
|
|
3714
|
-
*/
|
|
3715
|
-
app.get('/api/logs', (req, res) => {
|
|
3716
|
-
if (!spawner) {
|
|
3717
|
-
return res.status(503).json({ error: 'Spawner not enabled' });
|
|
3718
|
-
}
|
|
3719
|
-
try {
|
|
3720
|
-
const workers = spawner.getActiveWorkers();
|
|
3721
|
-
const agents = workers.map(w => ({
|
|
3722
|
-
name: w.name,
|
|
3723
|
-
cli: w.cli,
|
|
3724
|
-
pid: w.pid,
|
|
3725
|
-
spawnedAt: new Date(w.spawnedAt).toISOString(),
|
|
3726
|
-
hasLogs: true,
|
|
3727
|
-
}));
|
|
3728
|
-
res.json({ agents });
|
|
3729
|
-
}
|
|
3730
|
-
catch (err) {
|
|
3731
|
-
console.error('Failed to list agents with logs:', err);
|
|
3732
|
-
res.status(500).json({ error: 'Failed to list agents' });
|
|
3733
|
-
}
|
|
3734
|
-
});
|
|
3735
|
-
// ===== Agent Status API =====
|
|
3736
|
-
/**
|
|
3737
|
-
* GET /api/agents/:name/online - Check if an agent is online
|
|
3738
|
-
* Used by wrappers to wait for spawned agents before sending tasks.
|
|
3739
|
-
*/
|
|
3740
|
-
app.get('/api/agents/:name/online', (req, res) => {
|
|
3741
|
-
const { name } = req.params;
|
|
3742
|
-
const online = isAgentOnline(name);
|
|
3743
|
-
res.json({ name, online });
|
|
3744
|
-
});
|
|
3745
|
-
// ===== Agent Spawn API =====
|
|
3746
|
-
/**
|
|
3747
|
-
* POST /api/spawn - Spawn a new agent
|
|
3748
|
-
* Body: { name: string, cli?: string, task?: string, team?: string, spawnerName?, cwd?, interactive?, shadowMode?, shadowAgent?, shadowOf?, shadowTriggers?, shadowSpeakOn? }
|
|
3749
|
-
*/
|
|
3750
|
-
app.post('/api/spawn', async (req, res) => {
|
|
3751
|
-
if (!spawner) {
|
|
3752
|
-
return res.status(503).json({
|
|
3753
|
-
success: false,
|
|
3754
|
-
error: 'Spawner not enabled. Start dashboard with enableSpawner: true',
|
|
3755
|
-
});
|
|
3756
|
-
}
|
|
3757
|
-
const { name, cli = 'claude', task = '', team, spawnerName, cwd, interactive, shadowMode, shadowAgent, shadowOf, shadowTriggers, shadowSpeakOn, userId, } = req.body;
|
|
3758
|
-
if (!name || typeof name !== 'string') {
|
|
3759
|
-
return res.status(400).json({
|
|
3760
|
-
success: false,
|
|
3761
|
-
error: 'Missing required field: name',
|
|
3762
|
-
});
|
|
3763
|
-
}
|
|
3764
|
-
try {
|
|
3765
|
-
const request = {
|
|
3766
|
-
name,
|
|
3767
|
-
cli,
|
|
3768
|
-
task,
|
|
3769
|
-
team: team || undefined, // Optional team name
|
|
3770
|
-
spawnerName: spawnerName || undefined, // For policy enforcement
|
|
3771
|
-
cwd: cwd || undefined, // Working directory
|
|
3772
|
-
interactive, // Disables auto-accept for auth setup flows
|
|
3773
|
-
shadowMode,
|
|
3774
|
-
shadowAgent,
|
|
3775
|
-
shadowOf,
|
|
3776
|
-
shadowTriggers,
|
|
3777
|
-
shadowSpeakOn,
|
|
3778
|
-
userId: typeof userId === 'string' ? userId : undefined,
|
|
3779
|
-
};
|
|
3780
|
-
const result = await spawner.spawn(request);
|
|
3781
|
-
if (result.success) {
|
|
3782
|
-
// Broadcast update to WebSocket clients
|
|
3783
|
-
broadcastData().catch(() => { });
|
|
3784
|
-
}
|
|
3785
|
-
res.json(result);
|
|
3786
|
-
}
|
|
3787
|
-
catch (err) {
|
|
3788
|
-
console.error('[api] Spawn error:', err);
|
|
3789
|
-
res.status(500).json({
|
|
3790
|
-
success: false,
|
|
3791
|
-
name,
|
|
3792
|
-
error: err.message,
|
|
3793
|
-
});
|
|
3794
|
-
}
|
|
3795
|
-
});
|
|
3796
|
-
/**
|
|
3797
|
-
* POST /api/spawn/architect - Spawn an Architect agent for bridge mode
|
|
3798
|
-
* Body: { cli?: string }
|
|
3799
|
-
*/
|
|
3800
|
-
app.post('/api/spawn/architect', async (req, res) => {
|
|
3801
|
-
if (!spawner) {
|
|
3802
|
-
return res.status(503).json({
|
|
3803
|
-
success: false,
|
|
3804
|
-
error: 'Spawner not enabled. Start dashboard with enableSpawner: true',
|
|
3805
|
-
});
|
|
3806
|
-
}
|
|
3807
|
-
const { cli = 'claude' } = req.body;
|
|
3808
|
-
// Check if Architect already exists
|
|
3809
|
-
const activeWorkers = spawner.getActiveWorkers();
|
|
3810
|
-
if (activeWorkers.some(w => w.name.toLowerCase() === 'architect')) {
|
|
3811
|
-
return res.status(409).json({
|
|
3812
|
-
success: false,
|
|
3813
|
-
error: 'Architect agent already running',
|
|
3814
|
-
});
|
|
3815
|
-
}
|
|
3816
|
-
// Get bridge state for project context
|
|
3817
|
-
const bridgeStatePath = path.join(dataDir, 'bridge-state.json');
|
|
3818
|
-
let projectContext = 'No bridge projects connected.';
|
|
3819
|
-
if (fs.existsSync(bridgeStatePath)) {
|
|
3820
|
-
try {
|
|
3821
|
-
const bridgeState = JSON.parse(fs.readFileSync(bridgeStatePath, 'utf-8'));
|
|
3822
|
-
if (bridgeState.projects && bridgeState.projects.length > 0) {
|
|
3823
|
-
projectContext = bridgeState.projects
|
|
3824
|
-
.map((p) => `- ${p.id}: ${p.path} (Lead: ${p.lead?.name || 'none'})`)
|
|
3825
|
-
.join('\n');
|
|
3826
|
-
}
|
|
3827
|
-
}
|
|
3828
|
-
catch (e) {
|
|
3829
|
-
console.error('[api] Failed to read bridge state:', e);
|
|
3830
|
-
}
|
|
3831
|
-
}
|
|
3832
|
-
// Build the architect prompt
|
|
3833
|
-
const architectPrompt = `You are the Architect, a cross-project coordinator overseeing multiple codebases.
|
|
3834
|
-
|
|
3835
|
-
## Connected Projects
|
|
3836
|
-
${projectContext}
|
|
3837
|
-
|
|
3838
|
-
## Your Role
|
|
3839
|
-
- Coordinate high-level work across all projects
|
|
3840
|
-
- Assign tasks to project leads
|
|
3841
|
-
- Ensure consistency and resolve cross-project dependencies
|
|
3842
|
-
- Review overall architecture decisions
|
|
3843
|
-
|
|
3844
|
-
## Cross-Project Messaging
|
|
3845
|
-
|
|
3846
|
-
Write a file to your outbox, then output the trigger. Use project:AgentName syntax for cross-project messages:
|
|
3847
|
-
|
|
3848
|
-
\`\`\`bash
|
|
3849
|
-
# Message specific agent in a project
|
|
3850
|
-
cat > /tmp/relay-outbox/\$AGENT_RELAY_NAME/msg << 'EOF'
|
|
3851
|
-
TO: project-id:AgentName
|
|
3852
|
-
|
|
3853
|
-
Your message to this agent.
|
|
3854
|
-
EOF
|
|
3855
|
-
\`\`\`
|
|
3856
|
-
Then output: \`->relay-file:msg\`
|
|
3857
|
-
|
|
3858
|
-
\`\`\`bash
|
|
3859
|
-
# Broadcast to all agents in a project
|
|
3860
|
-
cat > /tmp/relay-outbox/\$AGENT_RELAY_NAME/broadcast << 'EOF'
|
|
3861
|
-
TO: project-id:*
|
|
3862
|
-
|
|
3863
|
-
Broadcast to all agents in a project.
|
|
3864
|
-
EOF
|
|
3865
|
-
\`\`\`
|
|
3866
|
-
Then output: \`->relay-file:broadcast\`
|
|
3867
|
-
|
|
3868
|
-
\`\`\`bash
|
|
3869
|
-
# Broadcast to ALL agents in ALL projects
|
|
3870
|
-
cat > /tmp/relay-outbox/\$AGENT_RELAY_NAME/all << 'EOF'
|
|
3871
|
-
TO: *:*
|
|
3872
|
-
|
|
3873
|
-
Broadcast to ALL agents in ALL projects.
|
|
3874
|
-
EOF
|
|
3875
|
-
\`\`\`
|
|
3876
|
-
Then output: \`->relay-file:all\`
|
|
3877
|
-
|
|
3878
|
-
## Getting Started
|
|
3879
|
-
1. Check in with each project lead to understand current status
|
|
3880
|
-
2. Identify cross-project dependencies
|
|
3881
|
-
3. Coordinate work across teams
|
|
3882
|
-
|
|
3883
|
-
Start by greeting the project leads and asking for status updates.`;
|
|
3884
|
-
try {
|
|
3885
|
-
const result = await spawner.spawn({
|
|
3886
|
-
name: 'Architect',
|
|
3887
|
-
cli,
|
|
3888
|
-
task: architectPrompt,
|
|
3889
|
-
});
|
|
3890
|
-
if (result.success) {
|
|
3891
|
-
broadcastData().catch(() => { });
|
|
3892
|
-
}
|
|
3893
|
-
res.json(result);
|
|
3894
|
-
}
|
|
3895
|
-
catch (err) {
|
|
3896
|
-
console.error('[api] Architect spawn error:', err);
|
|
3897
|
-
res.status(500).json({
|
|
3898
|
-
success: false,
|
|
3899
|
-
name: 'Architect',
|
|
3900
|
-
error: err.message,
|
|
3901
|
-
});
|
|
3902
|
-
}
|
|
3903
|
-
});
|
|
3904
|
-
/**
|
|
3905
|
-
* GET /api/spawned - List active spawned agents
|
|
3906
|
-
*
|
|
3907
|
-
* Returns agents from two sources:
|
|
3908
|
-
* 1. Spawner's active workers (in-memory tracking)
|
|
3909
|
-
* 2. Daemon's agents.json registry (persisted, survives restarts)
|
|
3910
|
-
*
|
|
3911
|
-
* This fallback ensures docker deployments show agents even after
|
|
3912
|
-
* container restarts when spawner's in-memory state is lost but
|
|
3913
|
-
* agents have reconnected to the daemon.
|
|
3914
|
-
*/
|
|
3915
|
-
app.get('/api/spawned', (req, res) => {
|
|
3916
|
-
// Collect agents from all available sources
|
|
3917
|
-
const agentsByName = new Map();
|
|
3918
|
-
// Source 1: Spawner's active workers (authoritative for spawned agents)
|
|
3919
|
-
if (spawner) {
|
|
3920
|
-
for (const worker of spawner.getActiveWorkers()) {
|
|
3921
|
-
agentsByName.set(worker.name, {
|
|
3922
|
-
name: worker.name,
|
|
3923
|
-
cli: worker.cli,
|
|
3924
|
-
pid: worker.pid,
|
|
3925
|
-
spawnedAt: worker.spawnedAt,
|
|
3926
|
-
task: worker.task,
|
|
3927
|
-
team: worker.team,
|
|
3928
|
-
source: 'spawner',
|
|
3929
|
-
});
|
|
3930
|
-
}
|
|
3931
|
-
}
|
|
3932
|
-
// Source 2: Daemon's agents.json registry (fallback for docker restarts)
|
|
3933
|
-
// Only include agents not already tracked by spawner
|
|
3934
|
-
const agentsPath = path.join(teamDir, 'agents.json');
|
|
3935
|
-
if (fs.existsSync(agentsPath)) {
|
|
3936
|
-
try {
|
|
3937
|
-
const data = JSON.parse(fs.readFileSync(agentsPath, 'utf-8'));
|
|
3938
|
-
const registeredAgents = data.agents || [];
|
|
3939
|
-
const thirtySecondsAgo = Date.now() - 30 * 1000;
|
|
3940
|
-
for (const agent of registeredAgents) {
|
|
3941
|
-
// Skip if already tracked by spawner
|
|
3942
|
-
if (agentsByName.has(agent.name))
|
|
3943
|
-
continue;
|
|
3944
|
-
// Only include recently active agents (within 30s heartbeat window)
|
|
3945
|
-
const lastSeen = agent.lastSeen ? new Date(agent.lastSeen).getTime() : 0;
|
|
3946
|
-
if (lastSeen < thirtySecondsAgo)
|
|
3947
|
-
continue;
|
|
3948
|
-
agentsByName.set(agent.name, {
|
|
3949
|
-
name: agent.name,
|
|
3950
|
-
cli: agent.cli || 'unknown',
|
|
3951
|
-
spawnedAt: agent.connectedAt ? new Date(agent.connectedAt).getTime() : undefined,
|
|
3952
|
-
team: agent.team,
|
|
3953
|
-
source: 'daemon',
|
|
3954
|
-
});
|
|
3955
|
-
}
|
|
3956
|
-
}
|
|
3957
|
-
catch (err) {
|
|
3958
|
-
console.error('[api/spawned] Failed to read agents.json:', err);
|
|
3959
|
-
}
|
|
3960
|
-
}
|
|
3961
|
-
const agents = Array.from(agentsByName.values());
|
|
3962
|
-
res.json({
|
|
3963
|
-
success: true,
|
|
3964
|
-
agents,
|
|
3965
|
-
// Include source info for debugging
|
|
3966
|
-
sources: {
|
|
3967
|
-
spawnerEnabled: !!spawner,
|
|
3968
|
-
daemonAgentsFile: fs.existsSync(agentsPath),
|
|
3969
|
-
},
|
|
3970
|
-
});
|
|
3971
|
-
});
|
|
3972
|
-
/**
|
|
3973
|
-
* DELETE /api/spawned/:name - Release a spawned agent
|
|
3974
|
-
*/
|
|
3975
|
-
app.delete('/api/spawned/:name', async (req, res) => {
|
|
3976
|
-
if (!spawner) {
|
|
3977
|
-
return res.status(503).json({
|
|
3978
|
-
success: false,
|
|
3979
|
-
error: 'Spawner not enabled',
|
|
3980
|
-
});
|
|
3981
|
-
}
|
|
3982
|
-
const { name } = req.params;
|
|
3983
|
-
try {
|
|
3984
|
-
const released = await spawner.release(name);
|
|
3985
|
-
if (released) {
|
|
3986
|
-
broadcastData().catch(() => { });
|
|
3987
|
-
}
|
|
3988
|
-
res.json({
|
|
3989
|
-
success: released,
|
|
3990
|
-
name,
|
|
3991
|
-
error: released ? undefined : `Agent ${name} not found`,
|
|
3992
|
-
});
|
|
3993
|
-
}
|
|
3994
|
-
catch (err) {
|
|
3995
|
-
console.error('[api] Release error:', err);
|
|
3996
|
-
res.status(500).json({
|
|
3997
|
-
success: false,
|
|
3998
|
-
name,
|
|
3999
|
-
error: err.message,
|
|
4000
|
-
});
|
|
4001
|
-
}
|
|
4002
|
-
});
|
|
4003
|
-
/**
|
|
4004
|
-
* POST /api/agents/by-name/:name/interrupt - Send ESC sequence to interrupt an agent
|
|
4005
|
-
*
|
|
4006
|
-
* Sends ESC ESC (0x1b 0x1b) to the agent's PTY to interrupt the current operation.
|
|
4007
|
-
* This is useful for breaking agents out of stuck loops without terminating them.
|
|
4008
|
-
*/
|
|
4009
|
-
app.post('/api/agents/by-name/:name/interrupt', (req, res) => {
|
|
4010
|
-
if (!spawner) {
|
|
4011
|
-
return res.status(503).json({
|
|
4012
|
-
success: false,
|
|
4013
|
-
error: 'Spawner not enabled',
|
|
4014
|
-
});
|
|
4015
|
-
}
|
|
4016
|
-
const { name } = req.params;
|
|
4017
|
-
// Check if agent exists
|
|
4018
|
-
if (!spawner.hasWorker(name)) {
|
|
4019
|
-
return res.status(404).json({
|
|
4020
|
-
success: false,
|
|
4021
|
-
error: `Agent ${name} not found or not spawned`,
|
|
4022
|
-
});
|
|
4023
|
-
}
|
|
4024
|
-
try {
|
|
4025
|
-
// Send ESC ESC sequence to interrupt the agent
|
|
4026
|
-
// ESC = 0x1b in hexadecimal
|
|
4027
|
-
const success = spawner.sendWorkerInput(name, '\x1b\x1b');
|
|
4028
|
-
if (success) {
|
|
4029
|
-
console.log(`[api] Sent interrupt (ESC ESC) to agent ${name}`);
|
|
4030
|
-
res.json({
|
|
4031
|
-
success: true,
|
|
4032
|
-
message: `Interrupt signal sent to ${name}`,
|
|
4033
|
-
});
|
|
4034
|
-
}
|
|
4035
|
-
else {
|
|
4036
|
-
res.status(500).json({
|
|
4037
|
-
success: false,
|
|
4038
|
-
error: `Failed to send interrupt to ${name}`,
|
|
4039
|
-
});
|
|
4040
|
-
}
|
|
4041
|
-
}
|
|
4042
|
-
catch (err) {
|
|
4043
|
-
console.error('[api] Interrupt error:', err);
|
|
4044
|
-
res.status(500).json({
|
|
4045
|
-
success: false,
|
|
4046
|
-
error: err.message,
|
|
4047
|
-
});
|
|
4048
|
-
}
|
|
4049
|
-
});
|
|
4050
|
-
/**
|
|
4051
|
-
* GET /api/trajectory - Get current trajectory status
|
|
4052
|
-
*/
|
|
4053
|
-
app.get('/api/trajectory', async (_req, res) => {
|
|
4054
|
-
try {
|
|
4055
|
-
const status = await getTrajectoryStatus();
|
|
4056
|
-
res.json({
|
|
4057
|
-
success: true,
|
|
4058
|
-
...status,
|
|
4059
|
-
});
|
|
4060
|
-
}
|
|
4061
|
-
catch (err) {
|
|
4062
|
-
console.error('[api] Trajectory status error:', err);
|
|
4063
|
-
res.status(500).json({
|
|
4064
|
-
success: false,
|
|
4065
|
-
error: err.message,
|
|
4066
|
-
});
|
|
4067
|
-
}
|
|
4068
|
-
});
|
|
4069
|
-
/**
|
|
4070
|
-
* GET /api/trajectory/steps - List trajectory steps
|
|
4071
|
-
*/
|
|
4072
|
-
app.get('/api/trajectory/steps', async (req, res) => {
|
|
4073
|
-
try {
|
|
4074
|
-
const trajectoryId = req.query.trajectoryId;
|
|
4075
|
-
const result = await listTrajectorySteps(trajectoryId);
|
|
4076
|
-
if (result.success) {
|
|
4077
|
-
res.json({
|
|
4078
|
-
success: true,
|
|
4079
|
-
steps: result.steps,
|
|
4080
|
-
});
|
|
4081
|
-
}
|
|
4082
|
-
else {
|
|
4083
|
-
res.status(500).json({
|
|
4084
|
-
success: false,
|
|
4085
|
-
steps: [],
|
|
4086
|
-
error: result.error,
|
|
4087
|
-
});
|
|
4088
|
-
}
|
|
4089
|
-
}
|
|
4090
|
-
catch (err) {
|
|
4091
|
-
console.error('[api] Trajectory steps error:', err);
|
|
4092
|
-
res.status(500).json({
|
|
4093
|
-
success: false,
|
|
4094
|
-
steps: [],
|
|
4095
|
-
error: err.message,
|
|
4096
|
-
});
|
|
4097
|
-
}
|
|
4098
|
-
});
|
|
4099
|
-
/**
|
|
4100
|
-
* GET /api/trajectory/history - List all trajectories (completed and active)
|
|
4101
|
-
*/
|
|
4102
|
-
app.get('/api/trajectory/history', async (_req, res) => {
|
|
4103
|
-
try {
|
|
4104
|
-
const result = await getTrajectoryHistory();
|
|
4105
|
-
if (result.success) {
|
|
4106
|
-
res.json({
|
|
4107
|
-
success: true,
|
|
4108
|
-
trajectories: result.trajectories,
|
|
4109
|
-
});
|
|
4110
|
-
}
|
|
4111
|
-
else {
|
|
4112
|
-
res.status(500).json({
|
|
4113
|
-
success: false,
|
|
4114
|
-
trajectories: [],
|
|
4115
|
-
error: result.error,
|
|
4116
|
-
});
|
|
4117
|
-
}
|
|
4118
|
-
}
|
|
4119
|
-
catch (err) {
|
|
4120
|
-
console.error('[api] Trajectory history error:', err);
|
|
4121
|
-
res.status(500).json({
|
|
4122
|
-
success: false,
|
|
4123
|
-
trajectories: [],
|
|
4124
|
-
error: err.message,
|
|
4125
|
-
});
|
|
4126
|
-
}
|
|
4127
|
-
});
|
|
4128
|
-
// ===== Settings API =====
|
|
4129
|
-
/**
|
|
4130
|
-
* GET /api/settings - Get all workspace settings with documentation
|
|
4131
|
-
*/
|
|
4132
|
-
app.get('/api/settings', async (_req, res) => {
|
|
4133
|
-
try {
|
|
4134
|
-
const { readRelayConfig, shouldStoreInRepo, getTrajectoriesStorageDescription } = await import('../trajectory/config.js');
|
|
4135
|
-
const config = readRelayConfig();
|
|
4136
|
-
res.json({
|
|
4137
|
-
success: true,
|
|
4138
|
-
settings: {
|
|
4139
|
-
trajectories: {
|
|
4140
|
-
storeInRepo: shouldStoreInRepo(),
|
|
4141
|
-
storageLocation: getTrajectoriesStorageDescription(),
|
|
4142
|
-
description: 'Trajectories record the journey of agent work using the PDERO paradigm (Plan, Design, Execute, Review, Observe). They capture decisions, phase transitions, and retrospectives.',
|
|
4143
|
-
benefits: [
|
|
4144
|
-
'Track why decisions were made, not just what was built',
|
|
4145
|
-
'Enable session recovery when agents crash or context is lost',
|
|
4146
|
-
'Provide learning data for future agents working on similar tasks',
|
|
4147
|
-
'Create an audit trail of agent work for review',
|
|
4148
|
-
],
|
|
4149
|
-
learnMore: 'https://pdero.com',
|
|
4150
|
-
optInReason: 'Enable "Store in repo" to version-control your trajectories alongside your code. This is useful for teams who want to review agent decision-making processes.',
|
|
4151
|
-
},
|
|
4152
|
-
},
|
|
4153
|
-
config,
|
|
4154
|
-
});
|
|
4155
|
-
}
|
|
4156
|
-
catch (err) {
|
|
4157
|
-
console.error('[api] Settings error:', err);
|
|
4158
|
-
res.status(500).json({
|
|
4159
|
-
success: false,
|
|
4160
|
-
error: err.message,
|
|
4161
|
-
});
|
|
4162
|
-
}
|
|
4163
|
-
});
|
|
4164
|
-
/**
|
|
4165
|
-
* GET /api/settings/trajectory - Get trajectory storage settings
|
|
4166
|
-
*/
|
|
4167
|
-
app.get('/api/settings/trajectory', async (_req, res) => {
|
|
4168
|
-
try {
|
|
4169
|
-
const { readRelayConfig, shouldStoreInRepo, getTrajectoriesStorageDescription } = await import('../trajectory/config.js');
|
|
4170
|
-
const config = readRelayConfig();
|
|
4171
|
-
res.json({
|
|
4172
|
-
success: true,
|
|
4173
|
-
settings: {
|
|
4174
|
-
storeInRepo: shouldStoreInRepo(),
|
|
4175
|
-
storageLocation: getTrajectoriesStorageDescription(),
|
|
4176
|
-
},
|
|
4177
|
-
config: config.trajectories || {},
|
|
4178
|
-
// Documentation for the UI
|
|
4179
|
-
documentation: {
|
|
4180
|
-
title: 'Trajectory Storage',
|
|
4181
|
-
description: 'Trajectories record the journey of agent work using the PDERO paradigm (Plan, Design, Execute, Review, Observe).',
|
|
4182
|
-
whatIsIt: 'A trajectory captures not just what an agent built, but WHY it made specific decisions. This includes phase transitions, key decisions with reasoning, and retrospective summaries.',
|
|
4183
|
-
benefits: [
|
|
4184
|
-
'Understand agent decision-making for code review',
|
|
4185
|
-
'Enable session recovery if agents crash',
|
|
4186
|
-
'Train future agents on your codebase patterns',
|
|
4187
|
-
'Create audit trails of AI work',
|
|
4188
|
-
],
|
|
4189
|
-
storeInRepoExplanation: 'When enabled, trajectories are stored in .trajectories/ in your repo and can be committed to source control. When disabled (default), they are stored in your user directory (~/.config/agent-relay/trajectories/).',
|
|
4190
|
-
learnMore: 'https://pdero.com',
|
|
4191
|
-
},
|
|
4192
|
-
});
|
|
4193
|
-
}
|
|
4194
|
-
catch (err) {
|
|
4195
|
-
console.error('[api] Settings trajectory error:', err);
|
|
4196
|
-
res.status(500).json({
|
|
4197
|
-
success: false,
|
|
4198
|
-
error: err.message,
|
|
4199
|
-
});
|
|
4200
|
-
}
|
|
4201
|
-
});
|
|
4202
|
-
/**
|
|
4203
|
-
* PUT /api/settings/trajectory - Update trajectory storage settings
|
|
4204
|
-
*
|
|
4205
|
-
* Body: { storeInRepo: boolean }
|
|
4206
|
-
*
|
|
4207
|
-
* This writes to .relay/config.json in the project root
|
|
4208
|
-
*/
|
|
4209
|
-
app.put('/api/settings/trajectory', async (req, res) => {
|
|
4210
|
-
try {
|
|
4211
|
-
const { storeInRepo } = req.body;
|
|
4212
|
-
if (typeof storeInRepo !== 'boolean') {
|
|
4213
|
-
return res.status(400).json({
|
|
4214
|
-
success: false,
|
|
4215
|
-
error: 'storeInRepo must be a boolean',
|
|
4216
|
-
});
|
|
4217
|
-
}
|
|
4218
|
-
const { getRelayConfigPath, readRelayConfig } = await import('../trajectory/config.js');
|
|
4219
|
-
const { getProjectPaths } = await import('../utils/project-namespace.js');
|
|
4220
|
-
const { projectRoot: _projectRoot } = getProjectPaths();
|
|
4221
|
-
// Read existing config
|
|
4222
|
-
const config = readRelayConfig();
|
|
4223
|
-
// Update trajectory settings
|
|
4224
|
-
config.trajectories = {
|
|
4225
|
-
...config.trajectories,
|
|
4226
|
-
storeInRepo,
|
|
4227
|
-
};
|
|
4228
|
-
// Ensure .relay directory exists
|
|
4229
|
-
const configPath = getRelayConfigPath();
|
|
4230
|
-
const configDir = path.dirname(configPath);
|
|
4231
|
-
if (!fs.existsSync(configDir)) {
|
|
4232
|
-
fs.mkdirSync(configDir, { recursive: true });
|
|
4233
|
-
}
|
|
4234
|
-
// Write updated config
|
|
4235
|
-
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
4236
|
-
res.json({
|
|
4237
|
-
success: true,
|
|
4238
|
-
settings: {
|
|
4239
|
-
storeInRepo,
|
|
4240
|
-
storageLocation: storeInRepo ? 'repo (.trajectories/)' : 'user (~/.config/agent-relay/trajectories/)',
|
|
4241
|
-
},
|
|
4242
|
-
});
|
|
4243
|
-
}
|
|
4244
|
-
catch (err) {
|
|
4245
|
-
console.error('[api] Settings trajectory update error:', err);
|
|
4246
|
-
res.status(500).json({
|
|
4247
|
-
success: false,
|
|
4248
|
-
error: err.message,
|
|
4249
|
-
});
|
|
4250
|
-
}
|
|
4251
|
-
});
|
|
4252
|
-
const decisions = new Map();
|
|
4253
|
-
/**
|
|
4254
|
-
* GET /api/decisions - List all pending decisions
|
|
4255
|
-
*/
|
|
4256
|
-
app.get('/api/decisions', (_req, res) => {
|
|
4257
|
-
const allDecisions = Array.from(decisions.values())
|
|
4258
|
-
.sort((a, b) => {
|
|
4259
|
-
const urgencyOrder = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
4260
|
-
return urgencyOrder[a.urgency] - urgencyOrder[b.urgency];
|
|
4261
|
-
});
|
|
4262
|
-
res.json({ success: true, decisions: allDecisions });
|
|
4263
|
-
});
|
|
4264
|
-
/**
|
|
4265
|
-
* POST /api/decisions - Create a new decision request
|
|
4266
|
-
* Body: { agentName, title, description, options?, urgency, category, expiresAt?, context? }
|
|
4267
|
-
*/
|
|
4268
|
-
app.post('/api/decisions', (req, res) => {
|
|
4269
|
-
const { agentName, title, description, options, urgency, category, expiresAt, context } = req.body;
|
|
4270
|
-
if (!agentName || !title || !urgency || !category) {
|
|
4271
|
-
return res.status(400).json({
|
|
4272
|
-
success: false,
|
|
4273
|
-
error: 'Missing required fields: agentName, title, urgency, category',
|
|
4274
|
-
});
|
|
4275
|
-
}
|
|
4276
|
-
const decision = {
|
|
4277
|
-
id: `decision-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
4278
|
-
agentName,
|
|
4279
|
-
title,
|
|
4280
|
-
description: description || '',
|
|
4281
|
-
options,
|
|
4282
|
-
urgency,
|
|
4283
|
-
category,
|
|
4284
|
-
createdAt: new Date().toISOString(),
|
|
4285
|
-
expiresAt,
|
|
4286
|
-
context,
|
|
4287
|
-
};
|
|
4288
|
-
decisions.set(decision.id, decision);
|
|
4289
|
-
// Broadcast to WebSocket clients
|
|
4290
|
-
broadcastData().catch(() => { });
|
|
4291
|
-
res.json({ success: true, decision });
|
|
4292
|
-
});
|
|
4293
|
-
/**
|
|
4294
|
-
* POST /api/decisions/:id/approve - Approve/resolve a decision
|
|
4295
|
-
* Body: { optionId?: string, response?: string }
|
|
4296
|
-
*/
|
|
4297
|
-
app.post('/api/decisions/:id/approve', async (req, res) => {
|
|
4298
|
-
const { id } = req.params;
|
|
4299
|
-
const { optionId, response } = req.body;
|
|
4300
|
-
const decision = decisions.get(id);
|
|
4301
|
-
if (!decision) {
|
|
4302
|
-
return res.status(404).json({ success: false, error: 'Decision not found' });
|
|
4303
|
-
}
|
|
4304
|
-
// Send response to the agent via relay
|
|
4305
|
-
const agentName = decision.agentName;
|
|
4306
|
-
let responseMessage = `DECISION APPROVED: ${decision.title}`;
|
|
4307
|
-
if (optionId && decision.options) {
|
|
4308
|
-
const option = decision.options.find(o => o.id === optionId);
|
|
4309
|
-
if (option) {
|
|
4310
|
-
responseMessage += `\nSelected: ${option.label}`;
|
|
4311
|
-
}
|
|
4312
|
-
}
|
|
4313
|
-
if (response) {
|
|
4314
|
-
responseMessage += `\nResponse: ${response}`;
|
|
4315
|
-
}
|
|
4316
|
-
// Try to send message to agent
|
|
4317
|
-
try {
|
|
4318
|
-
const client = await getRelayClient('_DashboardUI');
|
|
4319
|
-
if (client) {
|
|
4320
|
-
await client.sendMessage(agentName, responseMessage, 'message');
|
|
4321
|
-
}
|
|
4322
|
-
}
|
|
4323
|
-
catch (err) {
|
|
4324
|
-
console.warn('[api] Could not send decision response to agent:', err);
|
|
4325
|
-
}
|
|
4326
|
-
decisions.delete(id);
|
|
4327
|
-
broadcastData().catch(() => { });
|
|
4328
|
-
res.json({ success: true, message: 'Decision approved' });
|
|
4329
|
-
});
|
|
4330
|
-
/**
|
|
4331
|
-
* POST /api/decisions/:id/reject - Reject a decision
|
|
4332
|
-
* Body: { reason?: string }
|
|
4333
|
-
*/
|
|
4334
|
-
app.post('/api/decisions/:id/reject', async (req, res) => {
|
|
4335
|
-
const { id } = req.params;
|
|
4336
|
-
const { reason } = req.body;
|
|
4337
|
-
const decision = decisions.get(id);
|
|
4338
|
-
if (!decision) {
|
|
4339
|
-
return res.status(404).json({ success: false, error: 'Decision not found' });
|
|
4340
|
-
}
|
|
4341
|
-
// Send rejection to the agent
|
|
4342
|
-
const agentName = decision.agentName;
|
|
4343
|
-
let responseMessage = `DECISION REJECTED: ${decision.title}`;
|
|
4344
|
-
if (reason) {
|
|
4345
|
-
responseMessage += `\nReason: ${reason}`;
|
|
4346
|
-
}
|
|
4347
|
-
try {
|
|
4348
|
-
const client = await getRelayClient('_DashboardUI');
|
|
4349
|
-
if (client) {
|
|
4350
|
-
await client.sendMessage(agentName, responseMessage, 'message');
|
|
4351
|
-
}
|
|
4352
|
-
}
|
|
4353
|
-
catch (err) {
|
|
4354
|
-
console.warn('[api] Could not send decision rejection to agent:', err);
|
|
4355
|
-
}
|
|
4356
|
-
decisions.delete(id);
|
|
4357
|
-
broadcastData().catch(() => { });
|
|
4358
|
-
res.json({ success: true, message: 'Decision rejected' });
|
|
4359
|
-
});
|
|
4360
|
-
/**
|
|
4361
|
-
* DELETE /api/decisions/:id - Delete/dismiss a decision
|
|
4362
|
-
*/
|
|
4363
|
-
app.delete('/api/decisions/:id', (_req, res) => {
|
|
4364
|
-
const { id } = _req.params;
|
|
4365
|
-
if (!decisions.has(id)) {
|
|
4366
|
-
return res.status(404).json({ success: false, error: 'Decision not found' });
|
|
4367
|
-
}
|
|
4368
|
-
decisions.delete(id);
|
|
4369
|
-
broadcastData().catch(() => { });
|
|
4370
|
-
res.json({ success: true, message: 'Decision dismissed' });
|
|
4371
|
-
});
|
|
4372
|
-
/**
|
|
4373
|
-
* GET /api/fleet/servers - Get fleet server overview
|
|
4374
|
-
* Returns local daemon info + any connected bridge servers
|
|
4375
|
-
* Note: When bridge is active, local agents are already included in bridge project agents,
|
|
4376
|
-
* so we don't add a separate "Local Daemon" entry to avoid double-counting.
|
|
4377
|
-
*/
|
|
4378
|
-
app.get('/api/fleet/servers', async (_req, res) => {
|
|
4379
|
-
const servers = [];
|
|
4380
|
-
const localAgents = spawner?.getActiveWorkers() || [];
|
|
4381
|
-
const agentStatuses = await loadAgentStatuses();
|
|
4382
|
-
let hasBridgeProjects = false;
|
|
4383
|
-
// Check for bridge connections first
|
|
4384
|
-
const bridgeStatePath = path.join(dataDir, 'bridge-state.json');
|
|
4385
|
-
if (fs.existsSync(bridgeStatePath)) {
|
|
4386
|
-
try {
|
|
4387
|
-
const bridgeState = JSON.parse(fs.readFileSync(bridgeStatePath, 'utf-8'));
|
|
4388
|
-
if (bridgeState.projects && bridgeState.projects.length > 0) {
|
|
4389
|
-
hasBridgeProjects = true;
|
|
4390
|
-
for (const project of bridgeState.projects) {
|
|
4391
|
-
// Enrich with actual online agents from agents.json (same logic as getBridgeData)
|
|
4392
|
-
// This fixes the bug where stale agents were counted
|
|
4393
|
-
let projectAgents = [];
|
|
4394
|
-
if (project.path) {
|
|
4395
|
-
const projectHash = crypto.createHash('sha256').update(project.path).digest('hex').slice(0, 12);
|
|
4396
|
-
const projectDataDir = path.join(path.dirname(dataDir), projectHash);
|
|
4397
|
-
const projectTeamDir = path.join(projectDataDir, 'team');
|
|
4398
|
-
const agentsPath = path.join(projectTeamDir, 'agents.json');
|
|
4399
|
-
if (fs.existsSync(agentsPath)) {
|
|
4400
|
-
try {
|
|
4401
|
-
const agentsData = JSON.parse(fs.readFileSync(agentsPath, 'utf-8'));
|
|
4402
|
-
if (agentsData.agents && Array.isArray(agentsData.agents)) {
|
|
4403
|
-
// Filter to only show online agents (seen within 30 seconds)
|
|
4404
|
-
const thirtySecondsAgo = Date.now() - 30 * 1000;
|
|
4405
|
-
projectAgents = agentsData.agents
|
|
4406
|
-
.filter((a) => {
|
|
4407
|
-
if (!a.lastSeen)
|
|
4408
|
-
return false;
|
|
4409
|
-
return new Date(a.lastSeen).getTime() > thirtySecondsAgo;
|
|
4410
|
-
})
|
|
4411
|
-
.map((a) => ({
|
|
4412
|
-
name: a.name,
|
|
4413
|
-
status: 'online',
|
|
4414
|
-
}));
|
|
4415
|
-
}
|
|
4416
|
-
}
|
|
4417
|
-
catch (e) {
|
|
4418
|
-
console.warn(`[api] Failed to read agents for ${project.path}:`, e);
|
|
4419
|
-
}
|
|
4420
|
-
}
|
|
4421
|
-
}
|
|
4422
|
-
servers.push({
|
|
4423
|
-
id: project.id,
|
|
4424
|
-
name: project.name || project.path.split('/').pop() || project.id,
|
|
4425
|
-
status: project.connected ? 'healthy' : 'offline',
|
|
4426
|
-
agents: projectAgents,
|
|
4427
|
-
cpuUsage: 0,
|
|
4428
|
-
memoryUsage: 0,
|
|
4429
|
-
activeConnections: project.connected ? 1 : 0,
|
|
4430
|
-
uptime: 0,
|
|
4431
|
-
lastHeartbeat: project.lastSeen || new Date().toISOString(),
|
|
4432
|
-
});
|
|
4433
|
-
}
|
|
4434
|
-
}
|
|
4435
|
-
}
|
|
4436
|
-
catch (err) {
|
|
4437
|
-
console.warn('[api] Failed to read bridge state:', err);
|
|
4438
|
-
}
|
|
4439
|
-
}
|
|
4440
|
-
// Only add local daemon entry if we don't have bridge projects
|
|
4441
|
-
// (otherwise local agents are already counted in the bridge project)
|
|
4442
|
-
if (!hasBridgeProjects) {
|
|
4443
|
-
servers.push({
|
|
4444
|
-
id: 'local',
|
|
4445
|
-
name: 'Local Daemon',
|
|
4446
|
-
status: 'healthy',
|
|
4447
|
-
agents: localAgents.map(a => ({
|
|
4448
|
-
name: a.name,
|
|
4449
|
-
status: agentStatuses[a.name]?.status || 'unknown',
|
|
4450
|
-
})),
|
|
4451
|
-
cpuUsage: Math.random() * 30, // Mock - would come from actual metrics
|
|
4452
|
-
memoryUsage: Math.random() * 50,
|
|
4453
|
-
activeConnections: wss.clients.size,
|
|
4454
|
-
uptime: process.uptime(),
|
|
4455
|
-
lastHeartbeat: new Date().toISOString(),
|
|
4456
|
-
});
|
|
4457
|
-
}
|
|
4458
|
-
res.json({ success: true, servers });
|
|
4459
|
-
});
|
|
4460
|
-
/**
|
|
4461
|
-
* GET /api/fleet/stats - Get aggregate fleet statistics
|
|
4462
|
-
*/
|
|
4463
|
-
app.get('/api/fleet/stats', async (_req, res) => {
|
|
4464
|
-
const localAgents = spawner?.getActiveWorkers() || [];
|
|
4465
|
-
const agentStatuses = await loadAgentStatuses();
|
|
4466
|
-
const totalAgents = localAgents.length;
|
|
4467
|
-
let onlineAgents = 0;
|
|
4468
|
-
let busyAgents = 0;
|
|
4469
|
-
for (const agent of localAgents) {
|
|
4470
|
-
const status = agentStatuses[agent.name]?.status;
|
|
4471
|
-
if (status === 'online')
|
|
4472
|
-
onlineAgents++;
|
|
4473
|
-
if (status === 'busy')
|
|
4474
|
-
busyAgents++;
|
|
4475
|
-
}
|
|
4476
|
-
res.json({
|
|
4477
|
-
success: true,
|
|
4478
|
-
stats: {
|
|
4479
|
-
totalAgents,
|
|
4480
|
-
onlineAgents,
|
|
4481
|
-
busyAgents,
|
|
4482
|
-
pendingDecisions: decisions.size,
|
|
4483
|
-
activeTasks: Array.from(tasks.values()).filter(t => t.status === 'assigned' || t.status === 'in_progress').length,
|
|
4484
|
-
},
|
|
4485
|
-
});
|
|
4486
|
-
});
|
|
4487
|
-
const tasks = new Map();
|
|
4488
|
-
/**
|
|
4489
|
-
* GET /api/tasks - List all tasks
|
|
4490
|
-
*/
|
|
4491
|
-
app.get('/api/tasks', (req, res) => {
|
|
4492
|
-
const status = req.query.status;
|
|
4493
|
-
const agentName = req.query.agent;
|
|
4494
|
-
let allTasks = Array.from(tasks.values());
|
|
4495
|
-
if (status) {
|
|
4496
|
-
allTasks = allTasks.filter(t => t.status === status);
|
|
4497
|
-
}
|
|
4498
|
-
if (agentName) {
|
|
4499
|
-
allTasks = allTasks.filter(t => t.agentName === agentName);
|
|
4500
|
-
}
|
|
4501
|
-
// Sort by priority and creation time
|
|
4502
|
-
allTasks.sort((a, b) => {
|
|
4503
|
-
const priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
4504
|
-
const priorityDiff = priorityOrder[a.priority] - priorityOrder[b.priority];
|
|
4505
|
-
if (priorityDiff !== 0)
|
|
4506
|
-
return priorityDiff;
|
|
4507
|
-
return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime();
|
|
4508
|
-
});
|
|
4509
|
-
res.json({ success: true, tasks: allTasks });
|
|
4510
|
-
});
|
|
4511
|
-
/**
|
|
4512
|
-
* POST /api/tasks - Create and assign a task
|
|
4513
|
-
* Body: { agentName, title, description, priority }
|
|
4514
|
-
*/
|
|
4515
|
-
app.post('/api/tasks', async (req, res) => {
|
|
4516
|
-
const { agentName, title, description, priority } = req.body;
|
|
4517
|
-
if (!agentName || !title || !priority) {
|
|
4518
|
-
return res.status(400).json({
|
|
4519
|
-
success: false,
|
|
4520
|
-
error: 'Missing required fields: agentName, title, priority',
|
|
4521
|
-
});
|
|
4522
|
-
}
|
|
4523
|
-
const task = {
|
|
4524
|
-
id: `task-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
4525
|
-
agentName,
|
|
4526
|
-
title,
|
|
4527
|
-
description: description || '',
|
|
4528
|
-
priority,
|
|
4529
|
-
status: 'assigned',
|
|
4530
|
-
createdAt: new Date().toISOString(),
|
|
4531
|
-
assignedAt: new Date().toISOString(),
|
|
4532
|
-
};
|
|
4533
|
-
tasks.set(task.id, task);
|
|
4534
|
-
// Send task to agent via relay
|
|
4535
|
-
try {
|
|
4536
|
-
const client = await getRelayClient('_DashboardUI');
|
|
4537
|
-
if (client) {
|
|
4538
|
-
const taskMessage = `TASK ASSIGNED [${priority.toUpperCase()}]: ${title}\n\n${description || 'No additional details.'}`;
|
|
4539
|
-
await client.sendMessage(agentName, taskMessage, 'message');
|
|
4540
|
-
}
|
|
4541
|
-
}
|
|
4542
|
-
catch (err) {
|
|
4543
|
-
console.warn('[api] Could not send task to agent:', err);
|
|
4544
|
-
}
|
|
4545
|
-
broadcastData().catch(() => { });
|
|
4546
|
-
res.json({ success: true, task });
|
|
4547
|
-
});
|
|
4548
|
-
/**
|
|
4549
|
-
* PATCH /api/tasks/:id - Update task status
|
|
4550
|
-
* Body: { status, result? }
|
|
4551
|
-
*/
|
|
4552
|
-
app.patch('/api/tasks/:id', (req, res) => {
|
|
4553
|
-
const { id } = req.params;
|
|
4554
|
-
const { status, result } = req.body;
|
|
4555
|
-
const task = tasks.get(id);
|
|
4556
|
-
if (!task) {
|
|
4557
|
-
return res.status(404).json({ success: false, error: 'Task not found' });
|
|
4558
|
-
}
|
|
4559
|
-
if (status) {
|
|
4560
|
-
task.status = status;
|
|
4561
|
-
if (status === 'completed' || status === 'failed') {
|
|
4562
|
-
task.completedAt = new Date().toISOString();
|
|
4563
|
-
}
|
|
4564
|
-
}
|
|
4565
|
-
if (result !== undefined) {
|
|
4566
|
-
task.result = result;
|
|
4567
|
-
}
|
|
4568
|
-
tasks.set(id, task);
|
|
4569
|
-
broadcastData().catch(() => { });
|
|
4570
|
-
res.json({ success: true, task });
|
|
4571
|
-
});
|
|
4572
|
-
/**
|
|
4573
|
-
* DELETE /api/tasks/:id - Cancel/delete a task
|
|
4574
|
-
*/
|
|
4575
|
-
app.delete('/api/tasks/:id', async (req, res) => {
|
|
4576
|
-
const { id } = req.params;
|
|
4577
|
-
const task = tasks.get(id);
|
|
4578
|
-
if (!task) {
|
|
4579
|
-
return res.status(404).json({ success: false, error: 'Task not found' });
|
|
4580
|
-
}
|
|
4581
|
-
// Notify agent of cancellation if task is still active
|
|
4582
|
-
if (task.status === 'pending' || task.status === 'assigned' || task.status === 'in_progress') {
|
|
4583
|
-
try {
|
|
4584
|
-
const client = await getRelayClient('_DashboardUI');
|
|
4585
|
-
if (client) {
|
|
4586
|
-
await client.sendMessage(task.agentName, `TASK CANCELLED: ${task.title}`, 'message');
|
|
4587
|
-
}
|
|
4588
|
-
}
|
|
4589
|
-
catch (err) {
|
|
4590
|
-
console.warn('[api] Could not send task cancellation to agent:', err);
|
|
4591
|
-
}
|
|
4592
|
-
}
|
|
4593
|
-
tasks.delete(id);
|
|
4594
|
-
broadcastData().catch(() => { });
|
|
4595
|
-
res.json({ success: true, message: 'Task cancelled' });
|
|
4596
|
-
});
|
|
4597
|
-
// ===== Beads Integration API =====
|
|
4598
|
-
/**
|
|
4599
|
-
* POST /api/beads - Create a bead (task/issue) via the bd CLI
|
|
4600
|
-
*/
|
|
4601
|
-
app.post('/api/beads', async (req, res) => {
|
|
4602
|
-
const { title, assignee, priority, type, description: _description } = req.body;
|
|
4603
|
-
if (!title || typeof title !== 'string') {
|
|
4604
|
-
return res.status(400).json({ success: false, error: 'Title is required' });
|
|
4605
|
-
}
|
|
4606
|
-
// Build bd create command
|
|
4607
|
-
const args = ['create', `--title="${title.replace(/"/g, '\\"')}"`];
|
|
4608
|
-
if (assignee) {
|
|
4609
|
-
args.push(`--assignee=${assignee}`);
|
|
4610
|
-
}
|
|
4611
|
-
if (priority !== undefined && priority !== null) {
|
|
4612
|
-
args.push(`--priority=${priority}`);
|
|
4613
|
-
}
|
|
4614
|
-
if (type && ['task', 'bug', 'feature'].includes(type)) {
|
|
4615
|
-
args.push(`--type=${type}`);
|
|
4616
|
-
}
|
|
4617
|
-
const cmd = `bd ${args.join(' ')}`;
|
|
4618
|
-
console.log('[api/beads] Creating bead:', cmd);
|
|
4619
|
-
// Execute bd create command
|
|
4620
|
-
exec(cmd, { cwd: dataDir }, (error, stdout, stderr) => {
|
|
4621
|
-
if (error) {
|
|
4622
|
-
console.error('[api/beads] bd create failed:', stderr || error.message);
|
|
4623
|
-
return res.status(500).json({
|
|
4624
|
-
success: false,
|
|
4625
|
-
error: stderr || error.message || 'Failed to create bead',
|
|
4626
|
-
});
|
|
4627
|
-
}
|
|
4628
|
-
// Parse bead ID from output (bd create outputs the ID)
|
|
4629
|
-
const output = stdout.trim();
|
|
4630
|
-
// bd create typically outputs: "Created beads-xxx: title"
|
|
4631
|
-
const idMatch = output.match(/Created\s+(beads-\w+)/i) || output.match(/(beads-\w+)/);
|
|
4632
|
-
const beadId = idMatch ? idMatch[1] : `beads-${Date.now()}`;
|
|
4633
|
-
console.log('[api/beads] Created bead:', beadId);
|
|
4634
|
-
res.json({
|
|
4635
|
-
success: true,
|
|
4636
|
-
bead: {
|
|
4637
|
-
id: beadId,
|
|
4638
|
-
title,
|
|
4639
|
-
assignee,
|
|
4640
|
-
priority,
|
|
4641
|
-
type: type || 'task',
|
|
4642
|
-
},
|
|
4643
|
-
});
|
|
4644
|
-
});
|
|
4645
|
-
});
|
|
4646
|
-
/**
|
|
4647
|
-
* POST /api/relay/send - Send a relay message to an agent
|
|
4648
|
-
*/
|
|
4649
|
-
app.post('/api/relay/send', async (req, res) => {
|
|
4650
|
-
const { to, content, thread } = req.body;
|
|
4651
|
-
if (!to || typeof to !== 'string') {
|
|
4652
|
-
return res.status(400).json({ success: false, error: 'Recipient (to) is required' });
|
|
4653
|
-
}
|
|
4654
|
-
if (!content || typeof content !== 'string') {
|
|
4655
|
-
return res.status(400).json({ success: false, error: 'Message content is required' });
|
|
4656
|
-
}
|
|
4657
|
-
try {
|
|
4658
|
-
const client = await getRelayClient('_DashboardUI');
|
|
4659
|
-
if (!client) {
|
|
4660
|
-
return res.status(503).json({
|
|
4661
|
-
success: false,
|
|
4662
|
-
error: 'Relay client not available',
|
|
4663
|
-
});
|
|
4664
|
-
}
|
|
4665
|
-
const messageId = await client.sendMessage(to, content, thread ? 'message' : 'message');
|
|
4666
|
-
console.log('[api/relay/send] Sent message to', to, ':', messageId);
|
|
4667
|
-
res.json({
|
|
4668
|
-
success: true,
|
|
4669
|
-
messageId: messageId || `msg-${Date.now()}`,
|
|
4670
|
-
});
|
|
4671
|
-
}
|
|
4672
|
-
catch (err) {
|
|
4673
|
-
console.error('[api/relay/send] Failed to send message:', err);
|
|
4674
|
-
res.status(500).json({
|
|
4675
|
-
success: false,
|
|
4676
|
-
error: err instanceof Error ? err.message : 'Failed to send message',
|
|
4677
|
-
});
|
|
4678
|
-
}
|
|
4679
|
-
});
|
|
4680
|
-
// Helper to load agent statuses
|
|
4681
|
-
async function loadAgentStatuses() {
|
|
4682
|
-
const agentsFile = path.join(dataDir, 'agents.json');
|
|
4683
|
-
try {
|
|
4684
|
-
if (fs.existsSync(agentsFile)) {
|
|
4685
|
-
const data = JSON.parse(fs.readFileSync(agentsFile, 'utf-8'));
|
|
4686
|
-
const result = {};
|
|
4687
|
-
for (const agent of data.agents || []) {
|
|
4688
|
-
result[agent.name] = { status: agent.status || 'offline' };
|
|
4689
|
-
}
|
|
4690
|
-
return result;
|
|
4691
|
-
}
|
|
4692
|
-
}
|
|
4693
|
-
catch (err) {
|
|
4694
|
-
console.warn('[api] Failed to load agent statuses:', err);
|
|
4695
|
-
}
|
|
4696
|
-
return {};
|
|
4697
|
-
}
|
|
4698
|
-
// Watch for changes
|
|
4699
|
-
if (storage) {
|
|
4700
|
-
setInterval(() => {
|
|
4701
|
-
broadcastData().catch((err) => console.error('Broadcast failed', err));
|
|
4702
|
-
broadcastBridgeData().catch((err) => console.error('Bridge broadcast failed', err));
|
|
4703
|
-
}, 1000);
|
|
4704
|
-
}
|
|
4705
|
-
else {
|
|
4706
|
-
let fsWait = null;
|
|
4707
|
-
let bridgeFsWait = null;
|
|
4708
|
-
try {
|
|
4709
|
-
if (fs.existsSync(dataDir)) {
|
|
4710
|
-
console.log(`Watching ${dataDir} for changes...`);
|
|
4711
|
-
fs.watch(dataDir, { recursive: true }, (eventType, filename) => {
|
|
4712
|
-
if (filename && (filename.endsWith('inbox.md') || filename.endsWith('team.json') || filename.endsWith('agents.json') || filename.endsWith('processing-state.json'))) {
|
|
4713
|
-
// Debounce
|
|
4714
|
-
if (fsWait)
|
|
4715
|
-
return;
|
|
4716
|
-
fsWait = setTimeout(() => {
|
|
4717
|
-
fsWait = null;
|
|
4718
|
-
broadcastData();
|
|
4719
|
-
}, 100);
|
|
4720
|
-
}
|
|
4721
|
-
// Watch for bridge state changes
|
|
4722
|
-
if (filename && filename.endsWith('bridge-state.json')) {
|
|
4723
|
-
if (bridgeFsWait)
|
|
4724
|
-
return;
|
|
4725
|
-
bridgeFsWait = setTimeout(() => {
|
|
4726
|
-
bridgeFsWait = null;
|
|
4727
|
-
broadcastBridgeData();
|
|
4728
|
-
}, 100);
|
|
4729
|
-
}
|
|
4730
|
-
});
|
|
4731
|
-
}
|
|
4732
|
-
else {
|
|
4733
|
-
console.warn(`Data directory ${dataDir} does not exist yet.`);
|
|
4734
|
-
}
|
|
4735
|
-
}
|
|
4736
|
-
catch (e) {
|
|
4737
|
-
console.error('Watch failed:', e);
|
|
4738
|
-
}
|
|
4739
|
-
}
|
|
4740
|
-
// Try to find an available port, starting from the requested port
|
|
4741
|
-
const findAvailablePort = async (startPort, maxAttempts = 10) => {
|
|
4742
|
-
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
4743
|
-
const portToTry = startPort + attempt;
|
|
4744
|
-
const isAvailable = await new Promise((resolve) => {
|
|
4745
|
-
const testServer = http.createServer();
|
|
4746
|
-
testServer.once('error', () => resolve(false));
|
|
4747
|
-
testServer.once('listening', () => {
|
|
4748
|
-
testServer.close();
|
|
4749
|
-
resolve(true);
|
|
4750
|
-
});
|
|
4751
|
-
testServer.listen(portToTry);
|
|
4752
|
-
});
|
|
4753
|
-
if (isAvailable) {
|
|
4754
|
-
return portToTry;
|
|
4755
|
-
}
|
|
4756
|
-
console.log(`Port ${portToTry} in use, trying ${portToTry + 1}...`);
|
|
4757
|
-
}
|
|
4758
|
-
throw new Error(`Could not find available port after trying ${startPort}-${startPort + maxAttempts - 1}`);
|
|
4759
|
-
};
|
|
4760
|
-
const availablePort = await findAvailablePort(port);
|
|
4761
|
-
if (availablePort !== port) {
|
|
4762
|
-
console.log(`Requested dashboard port ${port} is busy; switching to ${availablePort}.`);
|
|
4763
|
-
}
|
|
4764
|
-
return new Promise((resolve, reject) => {
|
|
4765
|
-
server.listen(availablePort, async () => {
|
|
4766
|
-
console.log(`Dashboard running at http://localhost:${availablePort} (build: cloud-channels-v2)`);
|
|
4767
|
-
console.log(`Monitoring: ${dataDir}`);
|
|
4768
|
-
// Set the dashboard port on spawner so spawned agents can use the API for nested spawns
|
|
4769
|
-
if (spawner) {
|
|
4770
|
-
spawner.setDashboardPort(availablePort);
|
|
4771
|
-
}
|
|
4772
|
-
// Start health worker on separate thread for reliable health checks
|
|
4773
|
-
// This ensures health checks respond even when main event loop is blocked
|
|
4774
|
-
const healthPort = getHealthPort(availablePort);
|
|
4775
|
-
const healthWorker = new HealthWorkerManager({ port: healthPort }, {
|
|
4776
|
-
getUptime: () => process.uptime(),
|
|
4777
|
-
getMemoryMB: () => Math.round(process.memoryUsage().heapUsed / 1024 / 1024),
|
|
4778
|
-
getRelayConnected: () => {
|
|
4779
|
-
const defaultClient = relayClients.get('Dashboard');
|
|
4780
|
-
return defaultClient?.state === 'READY';
|
|
4781
|
-
},
|
|
4782
|
-
getAgentCount: () => relayClients.size,
|
|
4783
|
-
getStatus: () => {
|
|
4784
|
-
const socketExists = fs.existsSync(socketPath);
|
|
4785
|
-
if (!socketExists)
|
|
4786
|
-
return 'degraded';
|
|
4787
|
-
const defaultClient = relayClients.get('Dashboard');
|
|
4788
|
-
return defaultClient?.state === 'READY' ? 'healthy' : 'busy';
|
|
4789
|
-
},
|
|
4790
|
-
});
|
|
4791
|
-
try {
|
|
4792
|
-
await healthWorker.start();
|
|
4793
|
-
console.log(`Health check worker running at http://localhost:${healthPort}/health`);
|
|
4794
|
-
}
|
|
4795
|
-
catch (err) {
|
|
4796
|
-
console.warn('[dashboard] Failed to start health worker, using main thread health check:', err);
|
|
4797
|
-
}
|
|
4798
|
-
resolve(availablePort);
|
|
4799
|
-
});
|
|
4800
|
-
server.on('error', (err) => {
|
|
4801
|
-
console.error('Server error:', err);
|
|
4802
|
-
reject(err);
|
|
4803
|
-
});
|
|
4804
|
-
});
|
|
4805
|
-
}
|
|
4806
|
-
//# sourceMappingURL=server.js.map
|