agent-relay 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/bridge/spawner.d.ts +53 -0
- package/dist/bridge/spawner.d.ts.map +1 -1
- package/dist/bridge/spawner.js +203 -19
- package/dist/bridge/spawner.js.map +1 -1
- package/dist/bridge/types.d.ts +12 -0
- package/dist/bridge/types.d.ts.map +1 -1
- package/dist/cli/index.js +401 -5
- package/dist/cli/index.js.map +1 -1
- package/dist/cloud/api/auth.d.ts +3 -2
- package/dist/cloud/api/auth.d.ts.map +1 -1
- package/dist/cloud/api/auth.js +10 -98
- package/dist/cloud/api/auth.js.map +1 -1
- package/dist/cloud/api/cli-pty-runner.d.ts +54 -0
- package/dist/cloud/api/cli-pty-runner.d.ts.map +1 -0
- package/dist/cloud/api/cli-pty-runner.js +119 -0
- package/dist/cloud/api/cli-pty-runner.js.map +1 -0
- package/dist/cloud/api/generic-webhooks.d.ts +8 -0
- package/dist/cloud/api/generic-webhooks.d.ts.map +1 -0
- package/dist/cloud/api/generic-webhooks.js +129 -0
- package/dist/cloud/api/generic-webhooks.js.map +1 -0
- package/dist/cloud/api/git.d.ts +8 -0
- package/dist/cloud/api/git.d.ts.map +1 -0
- package/dist/cloud/api/git.js +131 -0
- package/dist/cloud/api/git.js.map +1 -0
- package/dist/cloud/api/github-app.d.ts +11 -0
- package/dist/cloud/api/github-app.d.ts.map +1 -0
- package/dist/cloud/api/github-app.js +189 -0
- package/dist/cloud/api/github-app.js.map +1 -0
- package/dist/cloud/api/middleware/planLimits.d.ts +7 -0
- package/dist/cloud/api/middleware/planLimits.d.ts.map +1 -1
- package/dist/cloud/api/middleware/planLimits.js +39 -1
- package/dist/cloud/api/middleware/planLimits.js.map +1 -1
- package/dist/cloud/api/monitoring.d.ts +11 -0
- package/dist/cloud/api/monitoring.d.ts.map +1 -0
- package/dist/cloud/api/monitoring.js +578 -0
- package/dist/cloud/api/monitoring.js.map +1 -0
- package/dist/cloud/api/nango-auth.d.ts +9 -0
- package/dist/cloud/api/nango-auth.d.ts.map +1 -0
- package/dist/cloud/api/nango-auth.js +377 -0
- package/dist/cloud/api/nango-auth.js.map +1 -0
- package/dist/cloud/api/onboarding.d.ts +8 -1
- package/dist/cloud/api/onboarding.d.ts.map +1 -1
- package/dist/cloud/api/onboarding.js +300 -119
- package/dist/cloud/api/onboarding.js.map +1 -1
- package/dist/cloud/api/policy.d.ts +8 -0
- package/dist/cloud/api/policy.d.ts.map +1 -0
- package/dist/cloud/api/policy.js +229 -0
- package/dist/cloud/api/policy.js.map +1 -0
- package/dist/cloud/api/providers.js +114 -42
- package/dist/cloud/api/providers.js.map +1 -1
- package/dist/cloud/api/test-helpers.d.ts +10 -0
- package/dist/cloud/api/test-helpers.d.ts.map +1 -0
- package/dist/cloud/api/test-helpers.js +575 -0
- package/dist/cloud/api/test-helpers.js.map +1 -0
- package/dist/cloud/api/webhooks.d.ts +7 -0
- package/dist/cloud/api/webhooks.d.ts.map +1 -0
- package/dist/cloud/api/webhooks.js +496 -0
- package/dist/cloud/api/webhooks.js.map +1 -0
- package/dist/cloud/api/workspaces.js +225 -8
- package/dist/cloud/api/workspaces.js.map +1 -1
- package/dist/cloud/billing/plans.d.ts.map +1 -1
- package/dist/cloud/billing/plans.js +13 -0
- package/dist/cloud/billing/plans.js.map +1 -1
- package/dist/cloud/billing/types.d.ts +9 -3
- package/dist/cloud/billing/types.d.ts.map +1 -1
- package/dist/cloud/config.d.ts +9 -2
- package/dist/cloud/config.d.ts.map +1 -1
- package/dist/cloud/config.js +13 -4
- package/dist/cloud/config.js.map +1 -1
- package/dist/cloud/db/drizzle.d.ts +84 -1
- package/dist/cloud/db/drizzle.d.ts.map +1 -1
- package/dist/cloud/db/drizzle.js +470 -0
- package/dist/cloud/db/drizzle.js.map +1 -1
- package/dist/cloud/db/index.d.ts +9 -4
- package/dist/cloud/db/index.d.ts.map +1 -1
- package/dist/cloud/db/index.js +11 -3
- package/dist/cloud/db/index.js.map +1 -1
- package/dist/cloud/db/schema.d.ts +3283 -556
- package/dist/cloud/db/schema.d.ts.map +1 -1
- package/dist/cloud/db/schema.js +314 -1
- package/dist/cloud/db/schema.js.map +1 -1
- package/dist/cloud/index.d.ts +1 -0
- package/dist/cloud/index.d.ts.map +1 -1
- package/dist/cloud/index.js +2 -0
- package/dist/cloud/index.js.map +1 -1
- package/dist/cloud/provisioner/index.d.ts +24 -0
- package/dist/cloud/provisioner/index.d.ts.map +1 -1
- package/dist/cloud/provisioner/index.js +319 -18
- package/dist/cloud/provisioner/index.js.map +1 -1
- package/dist/cloud/server.d.ts +1 -0
- package/dist/cloud/server.d.ts.map +1 -1
- package/dist/cloud/server.js +357 -13
- package/dist/cloud/server.js.map +1 -1
- package/dist/cloud/services/auto-scaler.d.ts +152 -0
- package/dist/cloud/services/auto-scaler.d.ts.map +1 -0
- package/dist/cloud/services/auto-scaler.js +439 -0
- package/dist/cloud/services/auto-scaler.js.map +1 -0
- package/dist/cloud/services/capacity-manager.d.ts +148 -0
- package/dist/cloud/services/capacity-manager.d.ts.map +1 -0
- package/dist/cloud/services/capacity-manager.js +449 -0
- package/dist/cloud/services/capacity-manager.js.map +1 -0
- package/dist/cloud/services/ci-agent-spawner.d.ts +49 -0
- package/dist/cloud/services/ci-agent-spawner.d.ts.map +1 -0
- package/dist/cloud/services/ci-agent-spawner.js +373 -0
- package/dist/cloud/services/ci-agent-spawner.js.map +1 -0
- package/dist/cloud/services/index.d.ts +12 -0
- package/dist/cloud/services/index.d.ts.map +1 -0
- package/dist/cloud/services/index.js +15 -0
- package/dist/cloud/services/index.js.map +1 -0
- package/dist/cloud/services/mention-handler.d.ts +65 -0
- package/dist/cloud/services/mention-handler.d.ts.map +1 -0
- package/dist/cloud/services/mention-handler.js +405 -0
- package/dist/cloud/services/mention-handler.js.map +1 -0
- package/dist/cloud/services/nango.d.ts +126 -0
- package/dist/cloud/services/nango.d.ts.map +1 -0
- package/dist/cloud/services/nango.js +191 -0
- package/dist/cloud/services/nango.js.map +1 -0
- package/dist/cloud/services/persistence.d.ts +131 -0
- package/dist/cloud/services/persistence.d.ts.map +1 -0
- package/dist/cloud/services/persistence.js +200 -0
- package/dist/cloud/services/persistence.js.map +1 -0
- package/dist/cloud/services/planLimits.d.ts +15 -0
- package/dist/cloud/services/planLimits.d.ts.map +1 -1
- package/dist/cloud/services/planLimits.js +28 -0
- package/dist/cloud/services/planLimits.js.map +1 -1
- package/dist/cloud/services/scaling-orchestrator.d.ts +159 -0
- package/dist/cloud/services/scaling-orchestrator.d.ts.map +1 -0
- package/dist/cloud/services/scaling-orchestrator.js +502 -0
- package/dist/cloud/services/scaling-orchestrator.js.map +1 -0
- package/dist/cloud/services/scaling-policy.d.ts +121 -0
- package/dist/cloud/services/scaling-policy.d.ts.map +1 -0
- package/dist/cloud/services/scaling-policy.js +415 -0
- package/dist/cloud/services/scaling-policy.js.map +1 -0
- package/dist/cloud/vault/index.js +1 -1
- package/dist/cloud/vault/index.js.map +1 -1
- package/dist/cloud/webhooks/index.d.ts +24 -0
- package/dist/cloud/webhooks/index.d.ts.map +1 -0
- package/dist/cloud/webhooks/index.js +29 -0
- package/dist/cloud/webhooks/index.js.map +1 -0
- package/dist/cloud/webhooks/parsers/github.d.ts +8 -0
- package/dist/cloud/webhooks/parsers/github.d.ts.map +1 -0
- package/dist/cloud/webhooks/parsers/github.js +234 -0
- package/dist/cloud/webhooks/parsers/github.js.map +1 -0
- package/dist/cloud/webhooks/parsers/index.d.ts +23 -0
- package/dist/cloud/webhooks/parsers/index.d.ts.map +1 -0
- package/dist/cloud/webhooks/parsers/index.js +30 -0
- package/dist/cloud/webhooks/parsers/index.js.map +1 -0
- package/dist/cloud/webhooks/parsers/linear.d.ts +9 -0
- package/dist/cloud/webhooks/parsers/linear.d.ts.map +1 -0
- package/dist/cloud/webhooks/parsers/linear.js +258 -0
- package/dist/cloud/webhooks/parsers/linear.js.map +1 -0
- package/dist/cloud/webhooks/parsers/slack.d.ts +9 -0
- package/dist/cloud/webhooks/parsers/slack.d.ts.map +1 -0
- package/dist/cloud/webhooks/parsers/slack.js +214 -0
- package/dist/cloud/webhooks/parsers/slack.js.map +1 -0
- package/dist/cloud/webhooks/responders/github.d.ts +8 -0
- package/dist/cloud/webhooks/responders/github.d.ts.map +1 -0
- package/dist/cloud/webhooks/responders/github.js +73 -0
- package/dist/cloud/webhooks/responders/github.js.map +1 -0
- package/dist/cloud/webhooks/responders/index.d.ts +23 -0
- package/dist/cloud/webhooks/responders/index.d.ts.map +1 -0
- package/dist/cloud/webhooks/responders/index.js +30 -0
- package/dist/cloud/webhooks/responders/index.js.map +1 -0
- package/dist/cloud/webhooks/responders/linear.d.ts +9 -0
- package/dist/cloud/webhooks/responders/linear.d.ts.map +1 -0
- package/dist/cloud/webhooks/responders/linear.js +149 -0
- package/dist/cloud/webhooks/responders/linear.js.map +1 -0
- package/dist/cloud/webhooks/responders/slack.d.ts +20 -0
- package/dist/cloud/webhooks/responders/slack.d.ts.map +1 -0
- package/dist/cloud/webhooks/responders/slack.js +178 -0
- package/dist/cloud/webhooks/responders/slack.js.map +1 -0
- package/dist/cloud/webhooks/router.d.ts +25 -0
- package/dist/cloud/webhooks/router.d.ts.map +1 -0
- package/dist/cloud/webhooks/router.js +504 -0
- package/dist/cloud/webhooks/router.js.map +1 -0
- package/dist/cloud/webhooks/rules-engine.d.ts +24 -0
- package/dist/cloud/webhooks/rules-engine.d.ts.map +1 -0
- package/dist/cloud/webhooks/rules-engine.js +287 -0
- package/dist/cloud/webhooks/rules-engine.js.map +1 -0
- package/dist/cloud/webhooks/types.d.ts +186 -0
- package/dist/cloud/webhooks/types.d.ts.map +1 -0
- package/dist/cloud/webhooks/types.js +8 -0
- package/dist/cloud/webhooks/types.js.map +1 -0
- package/dist/continuity/formatter.d.ts +51 -0
- package/dist/continuity/formatter.d.ts.map +1 -0
- package/dist/continuity/formatter.js +313 -0
- package/dist/continuity/formatter.js.map +1 -0
- package/dist/continuity/handoff-store.d.ts +67 -0
- package/dist/continuity/handoff-store.d.ts.map +1 -0
- package/dist/continuity/handoff-store.js +472 -0
- package/dist/continuity/handoff-store.js.map +1 -0
- package/dist/continuity/index.d.ts +45 -0
- package/dist/continuity/index.d.ts.map +1 -0
- package/dist/continuity/index.js +48 -0
- package/dist/continuity/index.js.map +1 -0
- package/dist/continuity/ledger-store.d.ts +110 -0
- package/dist/continuity/ledger-store.d.ts.map +1 -0
- package/dist/continuity/ledger-store.js +500 -0
- package/dist/continuity/ledger-store.js.map +1 -0
- package/dist/continuity/manager.d.ts +178 -0
- package/dist/continuity/manager.d.ts.map +1 -0
- package/dist/continuity/manager.js +562 -0
- package/dist/continuity/manager.js.map +1 -0
- package/dist/continuity/parser.d.ts +76 -0
- package/dist/continuity/parser.d.ts.map +1 -0
- package/dist/continuity/parser.js +579 -0
- package/dist/continuity/parser.js.map +1 -0
- package/dist/continuity/types.d.ts +180 -0
- package/dist/continuity/types.d.ts.map +1 -0
- package/dist/continuity/types.js +9 -0
- package/dist/continuity/types.js.map +1 -0
- package/dist/daemon/agent-manager.d.ts +27 -0
- package/dist/daemon/agent-manager.d.ts.map +1 -1
- package/dist/daemon/agent-manager.js +107 -6
- package/dist/daemon/agent-manager.js.map +1 -1
- package/dist/daemon/agent-registry.d.ts +32 -0
- package/dist/daemon/agent-registry.d.ts.map +1 -1
- package/dist/daemon/agent-registry.js +42 -2
- package/dist/daemon/agent-registry.js.map +1 -1
- package/dist/daemon/api.d.ts +12 -0
- package/dist/daemon/api.d.ts.map +1 -1
- package/dist/daemon/api.js +131 -2
- package/dist/daemon/api.js.map +1 -1
- package/dist/daemon/cli-auth.d.ts +67 -0
- package/dist/daemon/cli-auth.d.ts.map +1 -0
- package/dist/daemon/cli-auth.js +537 -0
- package/dist/daemon/cli-auth.js.map +1 -0
- package/dist/daemon/cloud-sync.d.ts.map +1 -1
- package/dist/daemon/cloud-sync.js +9 -7
- package/dist/daemon/cloud-sync.js.map +1 -1
- package/dist/daemon/orchestrator.d.ts.map +1 -1
- package/dist/daemon/orchestrator.js +30 -0
- package/dist/daemon/orchestrator.js.map +1 -1
- package/dist/daemon/router.d.ts +5 -0
- package/dist/daemon/router.d.ts.map +1 -1
- package/dist/daemon/router.js +78 -26
- package/dist/daemon/router.js.map +1 -1
- package/dist/daemon/server.d.ts +5 -0
- package/dist/daemon/server.d.ts.map +1 -1
- package/dist/daemon/server.js +9 -1
- package/dist/daemon/server.js.map +1 -1
- package/dist/daemon/services/browser-testing.d.ts +88 -0
- package/dist/daemon/services/browser-testing.d.ts.map +1 -0
- package/dist/daemon/services/browser-testing.js +244 -0
- package/dist/daemon/services/browser-testing.js.map +1 -0
- package/dist/daemon/services/container-spawner.d.ts +135 -0
- package/dist/daemon/services/container-spawner.d.ts.map +1 -0
- package/dist/daemon/services/container-spawner.js +313 -0
- package/dist/daemon/services/container-spawner.js.map +1 -0
- package/dist/daemon/types.d.ts +5 -1
- package/dist/daemon/types.d.ts.map +1 -1
- package/dist/dashboard/out/404.html +1 -1
- package/dist/dashboard/out/_next/static/chunks/116-2502180def231162.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/282-980c2eb8fff20123.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/480-2d4111711d4e473c.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/724-73c1ee5f60abe860.js +9 -0
- package/dist/dashboard/out/_next/static/chunks/766-c3a14283c88d815b.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/app/app/page-7120be68bea622f3.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/app/connect-repos/page-dc2e3a1a22478efc.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/app/history/{page-b6edd4dde8d08194.js → page-56a8b4616a90dc43.js} +1 -1
- package/dist/dashboard/out/_next/static/chunks/app/login/page-3eac37ea6f5dd153.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/app/metrics/page-1081dd190a331a91.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/app/page-daf87e86f783f980.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/app/providers/page-b68a681526eb145e.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/app/signup/page-fee4ed1709070bcd.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/e868780c-48e5f147c90a3a41.js +18 -0
- package/dist/dashboard/out/_next/static/chunks/{main-c2f423b9c9f4591b.js → main-97850e03d723ea8c.js} +1 -1
- package/dist/dashboard/out/_next/static/chunks/webpack-1cdd8ed57114d5e1.js +1 -0
- package/dist/dashboard/out/_next/static/css/29852f26181969a0.css +1 -0
- package/dist/dashboard/out/_next/static/css/411ce23ffeae9f76.css +1 -0
- package/dist/dashboard/out/app.html +1 -14
- package/dist/dashboard/out/app.txt +2 -2
- package/dist/dashboard/out/connect-repos.html +1 -0
- package/dist/dashboard/out/connect-repos.txt +7 -0
- package/dist/dashboard/out/history.html +1 -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 +6 -0
- package/dist/dashboard/out/login.txt +7 -0
- package/dist/dashboard/out/metrics.html +1 -1
- package/dist/dashboard/out/metrics.txt +2 -2
- package/dist/dashboard/out/pricing.html +2 -2
- package/dist/dashboard/out/pricing.txt +2 -2
- package/dist/dashboard/out/providers.html +1 -0
- package/dist/dashboard/out/providers.txt +7 -0
- package/dist/dashboard/out/signup.html +6 -0
- package/dist/dashboard/out/signup.txt +7 -0
- package/dist/dashboard-server/server.d.ts.map +1 -1
- package/dist/dashboard-server/server.js +1308 -8
- package/dist/dashboard-server/server.js.map +1 -1
- package/dist/hooks/emitter.d.ts +40 -0
- package/dist/hooks/emitter.d.ts.map +1 -0
- package/dist/hooks/emitter.js +63 -0
- package/dist/hooks/emitter.js.map +1 -0
- package/dist/hooks/index.d.ts +3 -0
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +3 -0
- package/dist/hooks/index.js.map +1 -1
- package/dist/hooks/registry.d.ts +173 -0
- package/dist/hooks/registry.d.ts.map +1 -0
- package/dist/hooks/registry.js +476 -0
- package/dist/hooks/registry.js.map +1 -0
- package/dist/hooks/trajectory-hooks.d.ts +52 -0
- package/dist/hooks/trajectory-hooks.d.ts.map +1 -0
- package/dist/hooks/trajectory-hooks.js +183 -0
- package/dist/hooks/trajectory-hooks.js.map +1 -0
- package/dist/hooks/types.d.ts +141 -0
- package/dist/hooks/types.d.ts.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/memory/adapters/index.d.ts +8 -0
- package/dist/memory/adapters/index.d.ts.map +1 -0
- package/dist/memory/adapters/index.js +8 -0
- package/dist/memory/adapters/index.js.map +1 -0
- package/dist/memory/adapters/inmemory.d.ts +59 -0
- package/dist/memory/adapters/inmemory.d.ts.map +1 -0
- package/dist/memory/adapters/inmemory.js +195 -0
- package/dist/memory/adapters/inmemory.js.map +1 -0
- package/dist/memory/adapters/supermemory.d.ts +71 -0
- package/dist/memory/adapters/supermemory.d.ts.map +1 -0
- package/dist/memory/adapters/supermemory.js +338 -0
- package/dist/memory/adapters/supermemory.js.map +1 -0
- package/dist/memory/factory.d.ts +48 -0
- package/dist/memory/factory.d.ts.map +1 -0
- package/dist/memory/factory.js +143 -0
- package/dist/memory/factory.js.map +1 -0
- package/dist/memory/index.d.ts +32 -0
- package/dist/memory/index.d.ts.map +1 -0
- package/dist/memory/index.js +32 -0
- package/dist/memory/index.js.map +1 -0
- package/dist/memory/memory-hooks.d.ts +60 -0
- package/dist/memory/memory-hooks.d.ts.map +1 -0
- package/dist/memory/memory-hooks.js +313 -0
- package/dist/memory/memory-hooks.js.map +1 -0
- package/dist/memory/service.d.ts +49 -0
- package/dist/memory/service.d.ts.map +1 -0
- package/dist/memory/service.js +146 -0
- package/dist/memory/service.js.map +1 -0
- package/dist/memory/types.d.ts +195 -0
- package/dist/memory/types.d.ts.map +1 -0
- package/dist/memory/types.js +8 -0
- package/dist/memory/types.js.map +1 -0
- package/dist/policy/agent-policy.d.ts +225 -0
- package/dist/policy/agent-policy.d.ts.map +1 -0
- package/dist/policy/agent-policy.js +665 -0
- package/dist/policy/agent-policy.js.map +1 -0
- package/dist/policy/cloud-policy-fetcher.d.ts +12 -0
- package/dist/policy/cloud-policy-fetcher.d.ts.map +1 -0
- package/dist/policy/cloud-policy-fetcher.js +64 -0
- package/dist/policy/cloud-policy-fetcher.js.map +1 -0
- package/dist/resiliency/crash-insights.d.ts +156 -0
- package/dist/resiliency/crash-insights.d.ts.map +1 -0
- package/dist/resiliency/crash-insights.js +492 -0
- package/dist/resiliency/crash-insights.js.map +1 -0
- package/dist/resiliency/gossip-health.d.ts +137 -0
- package/dist/resiliency/gossip-health.d.ts.map +1 -0
- package/dist/resiliency/gossip-health.js +241 -0
- package/dist/resiliency/gossip-health.js.map +1 -0
- package/dist/resiliency/index.d.ts +5 -0
- package/dist/resiliency/index.d.ts.map +1 -1
- package/dist/resiliency/index.js +5 -0
- package/dist/resiliency/index.js.map +1 -1
- package/dist/resiliency/leader-watchdog.d.ts +109 -0
- package/dist/resiliency/leader-watchdog.d.ts.map +1 -0
- package/dist/resiliency/leader-watchdog.js +189 -0
- package/dist/resiliency/leader-watchdog.js.map +1 -0
- package/dist/resiliency/memory-monitor.d.ts +172 -0
- package/dist/resiliency/memory-monitor.d.ts.map +1 -0
- package/dist/resiliency/memory-monitor.js +593 -0
- package/dist/resiliency/memory-monitor.js.map +1 -0
- package/dist/resiliency/stateless-lead.d.ts +149 -0
- package/dist/resiliency/stateless-lead.d.ts.map +1 -0
- package/dist/resiliency/stateless-lead.js +308 -0
- package/dist/resiliency/stateless-lead.js.map +1 -0
- package/dist/resiliency/supervisor.d.ts +38 -0
- package/dist/resiliency/supervisor.d.ts.map +1 -1
- package/dist/resiliency/supervisor.js +122 -0
- package/dist/resiliency/supervisor.js.map +1 -1
- package/dist/shared/cli-auth-config.d.ts +91 -0
- package/dist/shared/cli-auth-config.d.ts.map +1 -0
- package/dist/shared/cli-auth-config.js +264 -0
- package/dist/shared/cli-auth-config.js.map +1 -0
- package/dist/storage/adapter.d.ts +1 -1
- package/dist/storage/adapter.d.ts.map +1 -1
- package/dist/trajectory/config.d.ts +84 -0
- package/dist/trajectory/config.d.ts.map +1 -0
- package/dist/trajectory/config.js +163 -0
- package/dist/trajectory/config.js.map +1 -0
- package/dist/trajectory/index.d.ts +8 -0
- package/dist/trajectory/index.d.ts.map +1 -0
- package/dist/trajectory/index.js +8 -0
- package/dist/trajectory/index.js.map +1 -0
- package/dist/trajectory/integration.d.ts +292 -0
- package/dist/trajectory/integration.d.ts.map +1 -0
- package/dist/trajectory/integration.js +834 -0
- package/dist/trajectory/integration.js.map +1 -0
- package/dist/utils/logger.js +1 -1
- package/dist/utils/logger.js.map +1 -1
- package/dist/utils/project-namespace.d.ts +24 -0
- package/dist/utils/project-namespace.d.ts.map +1 -1
- package/dist/utils/project-namespace.js +84 -0
- package/dist/utils/project-namespace.js.map +1 -1
- package/dist/wrapper/parser.d.ts +10 -0
- package/dist/wrapper/parser.d.ts.map +1 -1
- package/dist/wrapper/parser.js +100 -33
- package/dist/wrapper/parser.js.map +1 -1
- package/dist/wrapper/pty-wrapper.d.ts +197 -16
- package/dist/wrapper/pty-wrapper.d.ts.map +1 -1
- package/dist/wrapper/pty-wrapper.js +943 -106
- package/dist/wrapper/pty-wrapper.js.map +1 -1
- package/dist/wrapper/shared.d.ts +165 -0
- package/dist/wrapper/shared.d.ts.map +1 -0
- package/dist/wrapper/shared.js +270 -0
- package/dist/wrapper/shared.js.map +1 -0
- package/dist/wrapper/tmux-wrapper.d.ts +73 -11
- package/dist/wrapper/tmux-wrapper.d.ts.map +1 -1
- package/dist/wrapper/tmux-wrapper.js +541 -120
- package/dist/wrapper/tmux-wrapper.js.map +1 -1
- package/docs/CLOUD-ARCHITECTURE.md +152 -0
- package/docs/HOOKS_API.md +394 -0
- package/docs/WRAPPER_EVENTS.md +358 -0
- package/docs/agent-policy-snippet.md +40 -0
- package/docs/agent-relay-protocol.md +238 -0
- package/docs/agent-relay-snippet.md +53 -47
- package/docs/archive/EXECUTIVE_SUMMARY.md +358 -0
- package/docs/archive/ROADMAP.md +329 -0
- package/docs/competitive/GASTOWN.md +451 -0
- package/docs/{COMPETITIVE_ANALYSIS.md → competitive/OVERVIEW.md} +1 -0
- package/docs/competitive/README.md +34 -0
- package/docs/competitive/TMUX_ORCHESTRATOR.md +605 -0
- package/docs/dashboard.png +0 -0
- package/docs/design/ci-failure-webhooks.md +812 -0
- package/docs/design/comprehensive-integrations.md +238 -0
- package/docs/design/e2b-sandbox-integration.md +504 -0
- package/docs/design/github-app-permissions.md +264 -0
- package/docs/local-testing.md +428 -0
- package/docs/proposals/continuous-claude-integration.md +622 -0
- package/docs/proposals/custom-commands.md +368 -0
- package/docs/tasks/global-skills-system.tasks.md +230 -0
- package/docs/tasks/webhook-integrations.tasks.md +184 -0
- package/docs/tasks/workspace-capabilities.tasks.md +121 -0
- package/docs/testing/RESILIENCY-TEST-PLAN-2026-01-01.md +366 -0
- package/package.json +16 -7
- package/scripts/cloud-setup.sh +96 -0
- package/scripts/manual-qa.sh +293 -0
- package/scripts/postinstall.js +60 -0
- package/scripts/run-cloud-qa.sh +220 -0
- package/scripts/test-cli-auth/Dockerfile +44 -0
- package/scripts/test-cli-auth/Dockerfile.real +79 -0
- package/scripts/test-cli-auth/README.md +286 -0
- package/scripts/test-cli-auth/ci-test-real-clis.ts +251 -0
- package/scripts/test-cli-auth/ci-test-runner.ts +263 -0
- package/scripts/test-cli-auth/mock-cli.sh +147 -0
- package/scripts/test-cli-auth/package.json +14 -0
- package/scripts/test-cli-auth/test-oauth-flow.ts +220 -0
- package/scripts/test-pty-input-auto.js +222 -0
- package/scripts/test-pty-input.js +150 -0
- package/dist/dashboard/out/_next/static/chunks/693-7b3301d8f6bc5014.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/713-f78477eb185f1f4d.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/766-e53e1cfe39b0b5b5.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/900-037c64bfd797fb2a.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/app/app/page-e3d9e1f4466b9bae.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/app/metrics/page-e68825a81db67ba1.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/app/page-cc108bf68c8a657f.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/webpack-a5acc2831d094776.js +0 -1
- package/dist/dashboard/out/_next/static/css/79b80143647a07d7.css +0 -1
- package/dist/dashboard/out/_next/static/css/8cf277370ad48cfe.css +0 -1
- /package/dist/dashboard/out/_next/static/{6HHWb2ZmnJ4OSm0zUP7h4 → H5aWG0udPB4iOUIl_gytz}/_buildManifest.js +0 -0
- /package/dist/dashboard/out/_next/static/{6HHWb2ZmnJ4OSm0zUP7h4 → H5aWG0udPB4iOUIl_gytz}/_ssgManifest.js +0 -0
- /package/dist/dashboard/out/_next/static/chunks/{117-b2cd8d6485aacf2b.js → 117-b100311aff8d5c61.js} +0 -0
- /package/dist/dashboard/out/_next/static/chunks/{648-8f3f26864ce515e5.js → 648-a13d3c2b1be45466.js} +0 -0
- /package/dist/dashboard/out/_next/static/chunks/app/_not-found/{page-0b990dbb71d72a98.js → page-a4973f3e3c82fb67.js} +0 -0
- /package/dist/dashboard/out/_next/static/chunks/app/pricing/{page-d80e03a5297f95b6.js → page-4d72d5a5d8a9b618.js} +0 -0
- /package/docs/{CHANGELOG.md → archive/CHANGELOG.md} +0 -0
- /package/docs/{CLI-SIMPLIFICATION-COMPLETE.md → archive/CLI-SIMPLIFICATION-COMPLETE.md} +0 -0
- /package/docs/{DESIGN_BRIDGE_STAFFING.md → archive/DESIGN_BRIDGE_STAFFING.md} +0 -0
- /package/docs/{DESIGN_V2.md → archive/DESIGN_V2.md} +0 -0
- /package/docs/{MONETIZATION.md → archive/MONETIZATION.md} +0 -0
- /package/docs/{PROPOSAL-trajectories.md → archive/PROPOSAL-trajectories.md} +0 -0
- /package/docs/{SCALING_ANALYSIS.md → archive/SCALING_ANALYSIS.md} +0 -0
- /package/docs/{TESTING_PRESENCE_FEATURES.md → archive/TESTING_PRESENCE_FEATURES.md} +0 -0
- /package/docs/{TMUX_IMPLEMENTATION_NOTES.md → archive/TMUX_IMPLEMENTATION_NOTES.md} +0 -0
- /package/docs/{TMUX_IMPROVEMENTS.md → archive/TMUX_IMPROVEMENTS.md} +0 -0
- /package/docs/{dashboard-v2-plan.md → archive/dashboard-v2-plan.md} +0 -0
- /package/docs/{removable-code-analysis.md → archive/removable-code-analysis.md} +0 -0
- /package/docs/{competitive-analysis-mcp-agent-mail.md → competitive/MCP_AGENT_MAIL.md} +0 -0
|
@@ -5,6 +5,7 @@ import path from 'path';
|
|
|
5
5
|
import fs from 'fs';
|
|
6
6
|
import os from 'os';
|
|
7
7
|
import crypto from 'crypto';
|
|
8
|
+
import { exec } from 'child_process';
|
|
8
9
|
import { fileURLToPath } from 'url';
|
|
9
10
|
import { SqliteStorageAdapter } from '../storage/sqlite-adapter.js';
|
|
10
11
|
import { RelayClient } from '../wrapper/client.js';
|
|
@@ -12,7 +13,169 @@ import { computeNeedsAttention } from './needs-attention.js';
|
|
|
12
13
|
import { computeSystemMetrics, formatPrometheusMetrics } from './metrics.js';
|
|
13
14
|
import { MultiProjectClient } from '../bridge/multi-project-client.js';
|
|
14
15
|
import { AgentSpawner } from '../bridge/spawner.js';
|
|
16
|
+
import { listTrajectorySteps, getTrajectoryStatus, getTrajectoryHistory } from '../trajectory/integration.js';
|
|
15
17
|
import { loadTeamsConfig } from '../bridge/teams-config.js';
|
|
18
|
+
import { getMemoryMonitor } from '../resiliency/memory-monitor.js';
|
|
19
|
+
import { detectWorkspacePath } from '../utils/project-namespace.js';
|
|
20
|
+
import { startCLIAuth, getAuthSession, cancelAuthSession, submitAuthCode, completeAuthSession, getSupportedProviders, } from '../daemon/cli-auth.js';
|
|
21
|
+
/**
|
|
22
|
+
* Initialize cloud persistence for session tracking.
|
|
23
|
+
*
|
|
24
|
+
* Activation modes:
|
|
25
|
+
* 1. Local dev: Set RELAY_CLOUD_ENABLED=true and DATABASE_URL
|
|
26
|
+
* 2. Cloud deployment: Plan-based - user must have Pro+ subscription
|
|
27
|
+
* (enforced at cloud API level when linking daemon or enabling workspace)
|
|
28
|
+
*
|
|
29
|
+
* Session persistence (Pro+ feature) enables:
|
|
30
|
+
* - [[SUMMARY]] blocks saved to PostgreSQL
|
|
31
|
+
* - [[SESSION_END]] markers for session tracking
|
|
32
|
+
* - Session recovery and agent handoff
|
|
33
|
+
*
|
|
34
|
+
* @see canUseSessionPersistence in services/planLimits.ts
|
|
35
|
+
*/
|
|
36
|
+
async function initCloudPersistence(workspaceId) {
|
|
37
|
+
// Local dev mode: simple env var check
|
|
38
|
+
// Cloud mode: plan check happens at API level (daemon linking, workspace config)
|
|
39
|
+
if (process.env.RELAY_CLOUD_ENABLED !== 'true') {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
try {
|
|
43
|
+
// Dynamic import to avoid loading cloud dependencies unless enabled
|
|
44
|
+
const { getDb } = await import('../cloud/db/drizzle.js');
|
|
45
|
+
const { agentSessions, agentSummaries } = await import('../cloud/db/schema.js');
|
|
46
|
+
const { eq } = await import('drizzle-orm');
|
|
47
|
+
const db = getDb();
|
|
48
|
+
console.log('[dashboard] Cloud persistence enabled');
|
|
49
|
+
// Track active sessions per agent with timestamps for TTL cleanup
|
|
50
|
+
const SESSION_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
51
|
+
const MAX_SESSIONS = 10000;
|
|
52
|
+
const agentSessionIds = new Map();
|
|
53
|
+
// Track pending session creation to prevent race conditions
|
|
54
|
+
const pendingSessionCreation = new Map();
|
|
55
|
+
// Periodic cleanup of stale sessions (every 5 minutes)
|
|
56
|
+
const cleanupInterval = setInterval(() => {
|
|
57
|
+
const now = Date.now();
|
|
58
|
+
let evicted = 0;
|
|
59
|
+
for (const [name, { lastActivity }] of agentSessionIds.entries()) {
|
|
60
|
+
if (now - lastActivity > SESSION_TTL_MS) {
|
|
61
|
+
agentSessionIds.delete(name);
|
|
62
|
+
evicted++;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (evicted > 0) {
|
|
66
|
+
console.log(`[cloud] Evicted ${evicted} stale session entries`);
|
|
67
|
+
}
|
|
68
|
+
}, 5 * 60 * 1000);
|
|
69
|
+
// Don't keep process alive just for cleanup
|
|
70
|
+
cleanupInterval.unref();
|
|
71
|
+
// Helper to get or create session with race protection
|
|
72
|
+
const getOrCreateSession = async (agentName) => {
|
|
73
|
+
// Check cache first
|
|
74
|
+
const cached = agentSessionIds.get(agentName);
|
|
75
|
+
if (cached) {
|
|
76
|
+
return cached.id;
|
|
77
|
+
}
|
|
78
|
+
// Check if creation is already in progress
|
|
79
|
+
const pending = pendingSessionCreation.get(agentName);
|
|
80
|
+
if (pending) {
|
|
81
|
+
return pending;
|
|
82
|
+
}
|
|
83
|
+
// Create session with mutex
|
|
84
|
+
const creationPromise = (async () => {
|
|
85
|
+
try {
|
|
86
|
+
// Double-check cache after acquiring "lock"
|
|
87
|
+
const rechecked = agentSessionIds.get(agentName);
|
|
88
|
+
if (rechecked) {
|
|
89
|
+
return rechecked.id;
|
|
90
|
+
}
|
|
91
|
+
// Enforce max size - evict oldest if needed
|
|
92
|
+
if (agentSessionIds.size >= MAX_SESSIONS) {
|
|
93
|
+
let oldest = null;
|
|
94
|
+
for (const [name, { lastActivity }] of agentSessionIds.entries()) {
|
|
95
|
+
if (!oldest || lastActivity < oldest.time) {
|
|
96
|
+
oldest = { name, time: lastActivity };
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (oldest) {
|
|
100
|
+
agentSessionIds.delete(oldest.name);
|
|
101
|
+
console.log(`[cloud] Evicted oldest session for ${oldest.name} (max sessions reached)`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
// Create a new session with null safety
|
|
105
|
+
const result = await db.insert(agentSessions).values({
|
|
106
|
+
workspaceId,
|
|
107
|
+
agentName,
|
|
108
|
+
status: 'active',
|
|
109
|
+
startedAt: new Date(),
|
|
110
|
+
}).returning();
|
|
111
|
+
const session = result[0];
|
|
112
|
+
if (!session) {
|
|
113
|
+
throw new Error(`Failed to create session for agent ${agentName}`);
|
|
114
|
+
}
|
|
115
|
+
// Update cache
|
|
116
|
+
agentSessionIds.set(agentName, { id: session.id, lastActivity: Date.now() });
|
|
117
|
+
return session.id;
|
|
118
|
+
}
|
|
119
|
+
finally {
|
|
120
|
+
pendingSessionCreation.delete(agentName);
|
|
121
|
+
}
|
|
122
|
+
})();
|
|
123
|
+
pendingSessionCreation.set(agentName, creationPromise);
|
|
124
|
+
return creationPromise;
|
|
125
|
+
};
|
|
126
|
+
return {
|
|
127
|
+
onSummary: async (agentName, event) => {
|
|
128
|
+
try {
|
|
129
|
+
// Get or create session with race protection
|
|
130
|
+
const sessionId = await getOrCreateSession(agentName);
|
|
131
|
+
// Update activity timestamp
|
|
132
|
+
agentSessionIds.set(agentName, { id: sessionId, lastActivity: Date.now() });
|
|
133
|
+
// Insert summary
|
|
134
|
+
await db.insert(agentSummaries).values({
|
|
135
|
+
sessionId,
|
|
136
|
+
agentName,
|
|
137
|
+
summary: event.summary,
|
|
138
|
+
createdAt: new Date(),
|
|
139
|
+
});
|
|
140
|
+
console.log(`[cloud] Saved summary for ${agentName}: ${event.summary.currentTask || 'no task'}`);
|
|
141
|
+
}
|
|
142
|
+
catch (err) {
|
|
143
|
+
console.error(`[cloud] Failed to save summary for ${agentName}:`, err);
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
onSessionEnd: async (agentName, event) => {
|
|
147
|
+
try {
|
|
148
|
+
const cached = agentSessionIds.get(agentName);
|
|
149
|
+
if (cached) {
|
|
150
|
+
// Update session as ended
|
|
151
|
+
await db.update(agentSessions)
|
|
152
|
+
.set({
|
|
153
|
+
status: 'ended',
|
|
154
|
+
endedAt: new Date(),
|
|
155
|
+
endMarker: event.marker,
|
|
156
|
+
})
|
|
157
|
+
.where(eq(agentSessions.id, cached.id));
|
|
158
|
+
agentSessionIds.delete(agentName);
|
|
159
|
+
console.log(`[cloud] Session ended for ${agentName}: ${event.marker.summary || 'no summary'}`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
catch (err) {
|
|
163
|
+
console.error(`[cloud] Failed to end session for ${agentName}:`, err);
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
destroy: () => {
|
|
167
|
+
clearInterval(cleanupInterval);
|
|
168
|
+
agentSessionIds.clear();
|
|
169
|
+
pendingSessionCreation.clear();
|
|
170
|
+
console.log('[cloud] Cloud persistence handler destroyed');
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
catch (err) {
|
|
175
|
+
console.warn('[dashboard] Cloud persistence not available:', err);
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
16
179
|
const __filename = fileURLToPath(import.meta.url);
|
|
17
180
|
const __dirname = path.dirname(__filename);
|
|
18
181
|
/**
|
|
@@ -105,9 +268,49 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
|
|
|
105
268
|
? new SqliteStorageAdapter({ dbPath })
|
|
106
269
|
: undefined;
|
|
107
270
|
// Initialize spawner if enabled
|
|
271
|
+
// Use detectWorkspacePath to find the actual repo directory in cloud workspaces
|
|
272
|
+
const workspacePath = detectWorkspacePath(projectRoot || dataDir);
|
|
273
|
+
console.log(`[dashboard] Workspace path: ${workspacePath}`);
|
|
274
|
+
// Pass dashboard port to spawner so spawned agents can call spawn/release APIs for nested spawning
|
|
108
275
|
const spawner = enableSpawner
|
|
109
|
-
? new AgentSpawner(
|
|
276
|
+
? new AgentSpawner(workspacePath, tmuxSession, port)
|
|
110
277
|
: undefined;
|
|
278
|
+
// Initialize cloud persistence and memory monitoring if enabled (RELAY_CLOUD_ENABLED=true)
|
|
279
|
+
if (spawner) {
|
|
280
|
+
// Use workspace ID from env or generate from project root
|
|
281
|
+
const workspaceId = process.env.RELAY_WORKSPACE_ID ||
|
|
282
|
+
crypto.createHash('sha256').update(projectRoot || dataDir).digest('hex').slice(0, 36);
|
|
283
|
+
initCloudPersistence(workspaceId).then((cloudHandler) => {
|
|
284
|
+
if (cloudHandler) {
|
|
285
|
+
spawner.setCloudPersistence(cloudHandler);
|
|
286
|
+
}
|
|
287
|
+
}).catch((err) => {
|
|
288
|
+
console.warn('[dashboard] Failed to initialize cloud persistence:', err);
|
|
289
|
+
});
|
|
290
|
+
// Initialize memory monitoring for cloud deployments
|
|
291
|
+
// Memory monitoring is enabled by default when cloud is enabled
|
|
292
|
+
if (process.env.RELAY_CLOUD_ENABLED === 'true' || process.env.RELAY_MEMORY_MONITORING === 'true') {
|
|
293
|
+
try {
|
|
294
|
+
const memoryMonitor = getMemoryMonitor({
|
|
295
|
+
checkIntervalMs: 10000, // Check every 10 seconds
|
|
296
|
+
enableTrendAnalysis: true,
|
|
297
|
+
enableProactiveAlerts: true,
|
|
298
|
+
});
|
|
299
|
+
memoryMonitor.start();
|
|
300
|
+
console.log('[dashboard] Memory monitoring enabled');
|
|
301
|
+
// Register existing workers with memory monitor
|
|
302
|
+
const workers = spawner.getActiveWorkers();
|
|
303
|
+
for (const worker of workers) {
|
|
304
|
+
if (worker.pid) {
|
|
305
|
+
memoryMonitor.register(worker.name, worker.pid);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
catch (err) {
|
|
310
|
+
console.warn('[dashboard] Failed to initialize memory monitoring:', err);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
111
314
|
process.on('uncaughtException', (err) => {
|
|
112
315
|
console.error('Uncaught Exception:', err);
|
|
113
316
|
});
|
|
@@ -244,7 +447,7 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
|
|
|
244
447
|
evictedCount++;
|
|
245
448
|
}
|
|
246
449
|
}
|
|
247
|
-
catch (
|
|
450
|
+
catch (_err) {
|
|
248
451
|
// Ignore errors for individual files (may have been deleted)
|
|
249
452
|
}
|
|
250
453
|
}
|
|
@@ -928,20 +1131,27 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
|
|
|
928
1131
|
getRecentSessions(),
|
|
929
1132
|
getAgentSummaries(),
|
|
930
1133
|
]);
|
|
931
|
-
// Filter agents:
|
|
1134
|
+
// Filter and separate agents from human users:
|
|
932
1135
|
// 1. Exclude "Dashboard" (internal agent, not a real team member)
|
|
933
1136
|
// 2. Exclude offline agents (no lastSeen or lastSeen > threshold)
|
|
1137
|
+
// 3. Exclude agents without a known CLI (these are improperly registered or stale)
|
|
1138
|
+
// 4. Separate human users (cli === 'dashboard') from AI agents
|
|
934
1139
|
const now = Date.now();
|
|
935
1140
|
// 30 seconds - aligns with heartbeat timeout (5s heartbeat * 6 multiplier = 30s)
|
|
936
1141
|
// This ensures agents disappear quickly after they stop responding to heartbeats
|
|
937
1142
|
const OFFLINE_THRESHOLD_MS = 30 * 1000;
|
|
938
|
-
|
|
1143
|
+
// First pass: filter out invalid/offline entries
|
|
1144
|
+
const validEntries = Array.from(agentsMap.values())
|
|
1145
|
+
.filter(agent => {
|
|
939
1146
|
// Exclude Dashboard
|
|
940
1147
|
if (agent.name === 'Dashboard')
|
|
941
1148
|
return false;
|
|
942
1149
|
// Exclude agents starting with __ (internal/system agents)
|
|
943
1150
|
if (agent.name.startsWith('__'))
|
|
944
1151
|
return false;
|
|
1152
|
+
// Exclude agents without a proper CLI (improperly registered or stale)
|
|
1153
|
+
if (!agent.cli || agent.cli === 'Unknown')
|
|
1154
|
+
return false;
|
|
945
1155
|
// Exclude offline agents (no lastSeen or too old)
|
|
946
1156
|
if (!agent.lastSeen)
|
|
947
1157
|
return false;
|
|
@@ -950,8 +1160,22 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
|
|
|
950
1160
|
return false;
|
|
951
1161
|
return true;
|
|
952
1162
|
});
|
|
1163
|
+
// Separate AI agents from human users
|
|
1164
|
+
const filteredAgents = validEntries
|
|
1165
|
+
.filter(agent => agent.cli !== 'dashboard')
|
|
1166
|
+
.map(agent => ({
|
|
1167
|
+
...agent,
|
|
1168
|
+
isHuman: false,
|
|
1169
|
+
}));
|
|
1170
|
+
const humanUsers = validEntries
|
|
1171
|
+
.filter(agent => agent.cli === 'dashboard')
|
|
1172
|
+
.map(agent => ({
|
|
1173
|
+
...agent,
|
|
1174
|
+
isHuman: true,
|
|
1175
|
+
}));
|
|
953
1176
|
return {
|
|
954
1177
|
agents: filteredAgents,
|
|
1178
|
+
users: humanUsers,
|
|
955
1179
|
messages: allMessages,
|
|
956
1180
|
activity: allMessages, // For now, activity log is just the message log
|
|
957
1181
|
sessions,
|
|
@@ -1194,8 +1418,8 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
|
|
|
1194
1418
|
logSubscriptions.get(agentName).add(ws);
|
|
1195
1419
|
console.log(`[dashboard] Client subscribed to logs for: ${agentName} (spawned: ${isSpawned}, daemon: ${isDaemon})`);
|
|
1196
1420
|
if (isSpawned && spawner) {
|
|
1197
|
-
// Send initial log history for spawned agents
|
|
1198
|
-
const lines = spawner.getWorkerOutput(agentName,
|
|
1421
|
+
// Send initial log history for spawned agents (5000 lines to match xterm scrollback capacity)
|
|
1422
|
+
const lines = spawner.getWorkerOutput(agentName, 5000);
|
|
1199
1423
|
ws.send(JSON.stringify({
|
|
1200
1424
|
type: 'history',
|
|
1201
1425
|
agent: agentName,
|
|
@@ -1258,11 +1482,51 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
|
|
|
1258
1482
|
console.log(`[dashboard] Logs WebSocket client disconnected (code: ${code}, reason: ${reasonStr})`);
|
|
1259
1483
|
});
|
|
1260
1484
|
});
|
|
1485
|
+
// Deduplication for log output - prevent same content from being broadcast multiple times
|
|
1486
|
+
// Key: agentName -> Set of recent content hashes (rolling window)
|
|
1487
|
+
const recentLogHashes = new Map();
|
|
1488
|
+
const MAX_LOG_HASH_WINDOW = 50; // Keep last 50 hashes per agent
|
|
1489
|
+
// Simple hash function for log dedup
|
|
1490
|
+
const hashLogContent = (content) => {
|
|
1491
|
+
// Normalize whitespace and create a simple hash
|
|
1492
|
+
const normalized = content.replace(/\s+/g, ' ').trim().slice(0, 200);
|
|
1493
|
+
let hash = 0;
|
|
1494
|
+
for (let i = 0; i < normalized.length; i++) {
|
|
1495
|
+
const char = normalized.charCodeAt(i);
|
|
1496
|
+
hash = ((hash << 5) - hash) + char;
|
|
1497
|
+
hash = hash & hash;
|
|
1498
|
+
}
|
|
1499
|
+
return hash.toString(36);
|
|
1500
|
+
};
|
|
1261
1501
|
// Function to broadcast log output to subscribed clients
|
|
1262
1502
|
const broadcastLogOutput = (agentName, output) => {
|
|
1263
1503
|
const clients = logSubscriptions.get(agentName);
|
|
1264
1504
|
if (!clients || clients.size === 0)
|
|
1265
1505
|
return;
|
|
1506
|
+
// Skip empty or whitespace-only output
|
|
1507
|
+
const trimmed = output.trim();
|
|
1508
|
+
if (!trimmed)
|
|
1509
|
+
return;
|
|
1510
|
+
// Dedup: Check if we've recently broadcast this content
|
|
1511
|
+
const hash = hashLogContent(output);
|
|
1512
|
+
let agentHashes = recentLogHashes.get(agentName);
|
|
1513
|
+
if (!agentHashes) {
|
|
1514
|
+
agentHashes = new Set();
|
|
1515
|
+
recentLogHashes.set(agentName, agentHashes);
|
|
1516
|
+
}
|
|
1517
|
+
if (agentHashes.has(hash)) {
|
|
1518
|
+
// Already broadcast this content recently, skip
|
|
1519
|
+
return;
|
|
1520
|
+
}
|
|
1521
|
+
// Add to rolling window
|
|
1522
|
+
agentHashes.add(hash);
|
|
1523
|
+
if (agentHashes.size > MAX_LOG_HASH_WINDOW) {
|
|
1524
|
+
// Remove oldest entry (first in Set iteration order)
|
|
1525
|
+
const oldest = agentHashes.values().next().value;
|
|
1526
|
+
if (oldest !== undefined) {
|
|
1527
|
+
agentHashes.delete(oldest);
|
|
1528
|
+
}
|
|
1529
|
+
}
|
|
1266
1530
|
const payload = JSON.stringify({
|
|
1267
1531
|
type: 'output',
|
|
1268
1532
|
agent: agentName,
|
|
@@ -1291,8 +1555,35 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
|
|
|
1291
1555
|
const getOnlineUsersList = () => {
|
|
1292
1556
|
return Array.from(onlineUsers.values()).map((state) => state.info);
|
|
1293
1557
|
};
|
|
1558
|
+
// Heartbeat to detect dead connections (30 seconds)
|
|
1559
|
+
const PRESENCE_HEARTBEAT_INTERVAL = 30000;
|
|
1560
|
+
const presenceHealth = new WeakMap();
|
|
1561
|
+
const presenceHeartbeat = setInterval(() => {
|
|
1562
|
+
wssPresence.clients.forEach((ws) => {
|
|
1563
|
+
const health = presenceHealth.get(ws);
|
|
1564
|
+
if (!health) {
|
|
1565
|
+
presenceHealth.set(ws, { isAlive: true });
|
|
1566
|
+
return;
|
|
1567
|
+
}
|
|
1568
|
+
if (!health.isAlive) {
|
|
1569
|
+
ws.terminate();
|
|
1570
|
+
return;
|
|
1571
|
+
}
|
|
1572
|
+
health.isAlive = false;
|
|
1573
|
+
ws.ping();
|
|
1574
|
+
});
|
|
1575
|
+
}, PRESENCE_HEARTBEAT_INTERVAL);
|
|
1576
|
+
wssPresence.on('close', () => {
|
|
1577
|
+
clearInterval(presenceHeartbeat);
|
|
1578
|
+
});
|
|
1294
1579
|
wssPresence.on('connection', (ws) => {
|
|
1295
|
-
|
|
1580
|
+
// Initialize health tracking (no log - too noisy)
|
|
1581
|
+
presenceHealth.set(ws, { isAlive: true });
|
|
1582
|
+
ws.on('pong', () => {
|
|
1583
|
+
const health = presenceHealth.get(ws);
|
|
1584
|
+
if (health)
|
|
1585
|
+
health.isAlive = true;
|
|
1586
|
+
});
|
|
1296
1587
|
let clientUsername;
|
|
1297
1588
|
ws.on('message', (data) => {
|
|
1298
1589
|
try {
|
|
@@ -1318,7 +1609,11 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
|
|
|
1318
1609
|
// Add this connection to existing user
|
|
1319
1610
|
existing.connections.add(ws);
|
|
1320
1611
|
existing.info.lastSeen = now;
|
|
1321
|
-
|
|
1612
|
+
// Only log at milestones to reduce noise
|
|
1613
|
+
const count = existing.connections.size;
|
|
1614
|
+
if (count === 2 || count === 5 || count === 10 || count % 50 === 0) {
|
|
1615
|
+
console.log(`[dashboard] User ${username} has ${count} connections`);
|
|
1616
|
+
}
|
|
1322
1617
|
}
|
|
1323
1618
|
else {
|
|
1324
1619
|
// New user - create presence state
|
|
@@ -1495,6 +1790,172 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
|
|
|
1495
1790
|
websocketClients: wss.clients.size,
|
|
1496
1791
|
});
|
|
1497
1792
|
});
|
|
1793
|
+
// ===== CLI Auth API (for workspace-based provider authentication) =====
|
|
1794
|
+
/**
|
|
1795
|
+
* POST /auth/cli/:provider/start - Start CLI auth flow
|
|
1796
|
+
* Body: { useDeviceFlow?: boolean }
|
|
1797
|
+
*/
|
|
1798
|
+
app.post('/auth/cli/:provider/start', async (req, res) => {
|
|
1799
|
+
const { provider } = req.params;
|
|
1800
|
+
const { useDeviceFlow } = req.body || {};
|
|
1801
|
+
try {
|
|
1802
|
+
const session = await startCLIAuth(provider, { useDeviceFlow });
|
|
1803
|
+
res.json({
|
|
1804
|
+
sessionId: session.id,
|
|
1805
|
+
status: session.status,
|
|
1806
|
+
authUrl: session.authUrl,
|
|
1807
|
+
});
|
|
1808
|
+
}
|
|
1809
|
+
catch (err) {
|
|
1810
|
+
res.status(400).json({
|
|
1811
|
+
error: err instanceof Error ? err.message : 'Failed to start CLI auth',
|
|
1812
|
+
});
|
|
1813
|
+
}
|
|
1814
|
+
});
|
|
1815
|
+
/**
|
|
1816
|
+
* GET /auth/cli/:provider/status/:sessionId - Get auth session status
|
|
1817
|
+
*/
|
|
1818
|
+
app.get('/auth/cli/:provider/status/:sessionId', (req, res) => {
|
|
1819
|
+
const { sessionId } = req.params;
|
|
1820
|
+
const session = getAuthSession(sessionId);
|
|
1821
|
+
if (!session) {
|
|
1822
|
+
return res.status(404).json({ error: 'Session not found' });
|
|
1823
|
+
}
|
|
1824
|
+
res.json({
|
|
1825
|
+
status: session.status,
|
|
1826
|
+
authUrl: session.authUrl,
|
|
1827
|
+
error: session.error,
|
|
1828
|
+
});
|
|
1829
|
+
});
|
|
1830
|
+
/**
|
|
1831
|
+
* GET /auth/cli/:provider/creds/:sessionId - Get credentials from completed auth
|
|
1832
|
+
*/
|
|
1833
|
+
app.get('/auth/cli/:provider/creds/:sessionId', (req, res) => {
|
|
1834
|
+
const { sessionId } = req.params;
|
|
1835
|
+
const session = getAuthSession(sessionId);
|
|
1836
|
+
if (!session) {
|
|
1837
|
+
return res.status(404).json({ error: 'Session not found' });
|
|
1838
|
+
}
|
|
1839
|
+
if (session.status !== 'success') {
|
|
1840
|
+
return res.status(400).json({ error: 'Auth not complete', status: session.status });
|
|
1841
|
+
}
|
|
1842
|
+
res.json({
|
|
1843
|
+
token: session.token,
|
|
1844
|
+
refreshToken: session.refreshToken,
|
|
1845
|
+
expiresAt: session.tokenExpiresAt?.toISOString(),
|
|
1846
|
+
});
|
|
1847
|
+
});
|
|
1848
|
+
/**
|
|
1849
|
+
* POST /auth/cli/:provider/cancel/:sessionId - Cancel auth session
|
|
1850
|
+
*/
|
|
1851
|
+
app.post('/auth/cli/:provider/cancel/:sessionId', (req, res) => {
|
|
1852
|
+
const { sessionId } = req.params;
|
|
1853
|
+
const cancelled = cancelAuthSession(sessionId);
|
|
1854
|
+
if (!cancelled) {
|
|
1855
|
+
return res.status(404).json({ error: 'Session not found' });
|
|
1856
|
+
}
|
|
1857
|
+
res.json({ success: true });
|
|
1858
|
+
});
|
|
1859
|
+
/**
|
|
1860
|
+
* POST /auth/cli/:provider/code/:sessionId - Submit auth code to PTY
|
|
1861
|
+
* Used when OAuth returns a code that must be pasted into the CLI
|
|
1862
|
+
*/
|
|
1863
|
+
app.post('/auth/cli/:provider/code/:sessionId', async (req, res) => {
|
|
1864
|
+
const { provider, sessionId } = req.params;
|
|
1865
|
+
const { code } = req.body;
|
|
1866
|
+
console.log('[cli-auth] Auth code submission received', { provider, sessionId, codeLength: code?.length });
|
|
1867
|
+
if (!code || typeof code !== 'string') {
|
|
1868
|
+
return res.status(400).json({ error: 'Auth code is required' });
|
|
1869
|
+
}
|
|
1870
|
+
try {
|
|
1871
|
+
const result = await submitAuthCode(sessionId, code);
|
|
1872
|
+
console.log('[cli-auth] Auth code submission result', { provider, sessionId, result });
|
|
1873
|
+
if (!result.success) {
|
|
1874
|
+
// Use 400 for all errors since they can be retried
|
|
1875
|
+
return res.status(400).json({
|
|
1876
|
+
error: result.error || 'Session not found or process not running',
|
|
1877
|
+
needsRestart: result.needsRestart ?? true,
|
|
1878
|
+
});
|
|
1879
|
+
}
|
|
1880
|
+
// Wait a few seconds for CLI to process and write credentials
|
|
1881
|
+
// The 1s delay in submitAuthCode + CLI processing time means credentials
|
|
1882
|
+
// should be available within 3-5 seconds
|
|
1883
|
+
let sessionStatus = 'waiting_auth';
|
|
1884
|
+
for (let i = 0; i < 10; i++) {
|
|
1885
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
1886
|
+
const session = getAuthSession(sessionId);
|
|
1887
|
+
if (session?.status === 'success') {
|
|
1888
|
+
sessionStatus = 'success';
|
|
1889
|
+
console.log('[cli-auth] Credentials found after code submission', { provider, sessionId, attempt: i + 1 });
|
|
1890
|
+
break;
|
|
1891
|
+
}
|
|
1892
|
+
if (session?.status === 'error') {
|
|
1893
|
+
sessionStatus = 'error';
|
|
1894
|
+
break;
|
|
1895
|
+
}
|
|
1896
|
+
}
|
|
1897
|
+
res.json({
|
|
1898
|
+
success: true,
|
|
1899
|
+
message: 'Auth code submitted',
|
|
1900
|
+
status: sessionStatus,
|
|
1901
|
+
});
|
|
1902
|
+
}
|
|
1903
|
+
catch (err) {
|
|
1904
|
+
console.error('[cli-auth] Auth code submission error', { provider, sessionId, error: String(err) });
|
|
1905
|
+
return res.status(500).json({
|
|
1906
|
+
error: 'Internal error submitting auth code. Please try again.',
|
|
1907
|
+
needsRestart: true,
|
|
1908
|
+
});
|
|
1909
|
+
}
|
|
1910
|
+
});
|
|
1911
|
+
/**
|
|
1912
|
+
* POST /auth/cli/:provider/complete/:sessionId - Complete auth
|
|
1913
|
+
* For providers like Claude: just polls for credentials
|
|
1914
|
+
* For providers like Codex: accepts authCode (redirect URL) and extracts the code
|
|
1915
|
+
*/
|
|
1916
|
+
app.post('/auth/cli/:provider/complete/:sessionId', async (req, res) => {
|
|
1917
|
+
const { sessionId } = req.params;
|
|
1918
|
+
const { authCode } = req.body || {};
|
|
1919
|
+
// If authCode provided, try to extract code and submit it
|
|
1920
|
+
if (authCode && typeof authCode === 'string') {
|
|
1921
|
+
let code = authCode;
|
|
1922
|
+
// If it's a URL, extract the code parameter
|
|
1923
|
+
if (authCode.startsWith('http')) {
|
|
1924
|
+
try {
|
|
1925
|
+
const url = new URL(authCode);
|
|
1926
|
+
const codeParam = url.searchParams.get('code');
|
|
1927
|
+
if (codeParam) {
|
|
1928
|
+
code = codeParam;
|
|
1929
|
+
}
|
|
1930
|
+
}
|
|
1931
|
+
catch {
|
|
1932
|
+
// Not a valid URL, use as-is
|
|
1933
|
+
}
|
|
1934
|
+
}
|
|
1935
|
+
// Submit the code to the CLI process
|
|
1936
|
+
const submitResult = await submitAuthCode(sessionId, code);
|
|
1937
|
+
if (!submitResult.success) {
|
|
1938
|
+
return res.status(400).json({
|
|
1939
|
+
error: submitResult.error,
|
|
1940
|
+
needsRestart: submitResult.needsRestart,
|
|
1941
|
+
});
|
|
1942
|
+
}
|
|
1943
|
+
// Wait a moment for credentials to be written
|
|
1944
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
1945
|
+
}
|
|
1946
|
+
// Poll for credentials
|
|
1947
|
+
const result = await completeAuthSession(sessionId);
|
|
1948
|
+
if (!result.success) {
|
|
1949
|
+
return res.status(400).json({ error: result.error });
|
|
1950
|
+
}
|
|
1951
|
+
res.json({ success: true, message: 'Authentication complete' });
|
|
1952
|
+
});
|
|
1953
|
+
/**
|
|
1954
|
+
* GET /auth/cli/providers - List supported providers
|
|
1955
|
+
*/
|
|
1956
|
+
app.get('/auth/cli/providers', (req, res) => {
|
|
1957
|
+
res.json({ providers: getSupportedProviders() });
|
|
1958
|
+
});
|
|
1498
1959
|
// ===== Metrics API =====
|
|
1499
1960
|
/**
|
|
1500
1961
|
* GET /api/metrics - JSON format metrics for dashboard
|
|
@@ -1564,6 +2025,197 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
|
|
|
1564
2025
|
res.status(500).send('# Error computing metrics\n');
|
|
1565
2026
|
}
|
|
1566
2027
|
});
|
|
2028
|
+
// ===== Agent Memory Metrics API =====
|
|
2029
|
+
/**
|
|
2030
|
+
* GET /api/metrics/agents - Detailed agent memory and resource metrics
|
|
2031
|
+
*/
|
|
2032
|
+
app.get('/api/metrics/agents', async (req, res) => {
|
|
2033
|
+
try {
|
|
2034
|
+
const agents = [];
|
|
2035
|
+
// Get metrics from spawner's active workers
|
|
2036
|
+
if (spawner) {
|
|
2037
|
+
const activeWorkers = spawner.getActiveWorkers();
|
|
2038
|
+
for (const worker of activeWorkers) {
|
|
2039
|
+
// Get memory usage via ps command
|
|
2040
|
+
let rssBytes = 0;
|
|
2041
|
+
let cpuPercent = 0;
|
|
2042
|
+
if (worker.pid) {
|
|
2043
|
+
try {
|
|
2044
|
+
const { execSync } = await import('child_process');
|
|
2045
|
+
const output = execSync(`ps -o rss=,pcpu= -p ${worker.pid}`, {
|
|
2046
|
+
encoding: 'utf8',
|
|
2047
|
+
timeout: 3000,
|
|
2048
|
+
}).trim();
|
|
2049
|
+
const parts = output.split(/\s+/);
|
|
2050
|
+
rssBytes = parseInt(parts[0] || '0', 10) * 1024;
|
|
2051
|
+
cpuPercent = parseFloat(parts[1] || '0');
|
|
2052
|
+
}
|
|
2053
|
+
catch {
|
|
2054
|
+
// Process may have exited
|
|
2055
|
+
}
|
|
2056
|
+
}
|
|
2057
|
+
agents.push({
|
|
2058
|
+
name: worker.name,
|
|
2059
|
+
pid: worker.pid,
|
|
2060
|
+
status: worker.pid ? 'running' : 'unknown',
|
|
2061
|
+
rssBytes,
|
|
2062
|
+
cpuPercent,
|
|
2063
|
+
trend: 'unknown',
|
|
2064
|
+
alertLevel: rssBytes > 1024 * 1024 * 1024 ? 'critical' :
|
|
2065
|
+
rssBytes > 512 * 1024 * 1024 ? 'warning' : 'normal',
|
|
2066
|
+
highWatermark: rssBytes,
|
|
2067
|
+
uptimeMs: worker.spawnedAt ? Date.now() - worker.spawnedAt : 0,
|
|
2068
|
+
startedAt: worker.spawnedAt ? new Date(worker.spawnedAt).toISOString() : undefined,
|
|
2069
|
+
});
|
|
2070
|
+
}
|
|
2071
|
+
}
|
|
2072
|
+
// Also check agents.json for registered agents that may not be spawned
|
|
2073
|
+
const agentsPath = path.join(teamDir, 'agents.json');
|
|
2074
|
+
if (fs.existsSync(agentsPath)) {
|
|
2075
|
+
const data = JSON.parse(fs.readFileSync(agentsPath, 'utf-8'));
|
|
2076
|
+
const registeredAgents = data.agents || [];
|
|
2077
|
+
for (const agent of registeredAgents) {
|
|
2078
|
+
if (!agents.find(a => a.name === agent.name)) {
|
|
2079
|
+
// Check if recently active (within 30 seconds)
|
|
2080
|
+
const lastSeen = agent.lastSeen ? new Date(agent.lastSeen).getTime() : 0;
|
|
2081
|
+
const isActive = Date.now() - lastSeen < 30000;
|
|
2082
|
+
if (isActive) {
|
|
2083
|
+
agents.push({
|
|
2084
|
+
name: agent.name,
|
|
2085
|
+
status: 'active',
|
|
2086
|
+
alertLevel: 'normal',
|
|
2087
|
+
});
|
|
2088
|
+
}
|
|
2089
|
+
}
|
|
2090
|
+
}
|
|
2091
|
+
}
|
|
2092
|
+
res.json({
|
|
2093
|
+
agents,
|
|
2094
|
+
system: {
|
|
2095
|
+
totalMemory: os.totalmem(),
|
|
2096
|
+
freeMemory: os.freemem(),
|
|
2097
|
+
heapUsed: process.memoryUsage().heapUsed,
|
|
2098
|
+
},
|
|
2099
|
+
});
|
|
2100
|
+
}
|
|
2101
|
+
catch (err) {
|
|
2102
|
+
console.error('Failed to get agent metrics', err);
|
|
2103
|
+
res.status(500).json({ error: 'Failed to get agent metrics' });
|
|
2104
|
+
}
|
|
2105
|
+
});
|
|
2106
|
+
/**
|
|
2107
|
+
* GET /api/metrics/health - System health and crash insights
|
|
2108
|
+
*/
|
|
2109
|
+
app.get('/api/metrics/health', async (req, res) => {
|
|
2110
|
+
try {
|
|
2111
|
+
// Calculate health score based on available data
|
|
2112
|
+
let healthScore = 100;
|
|
2113
|
+
const issues = [];
|
|
2114
|
+
const recommendations = [];
|
|
2115
|
+
const crashes = [];
|
|
2116
|
+
const alerts = [];
|
|
2117
|
+
let agentCount = 0;
|
|
2118
|
+
const totalCrashes24h = 0;
|
|
2119
|
+
let totalAlerts24h = 0;
|
|
2120
|
+
// Get spawned agent count
|
|
2121
|
+
if (spawner) {
|
|
2122
|
+
const workers = spawner.getActiveWorkers();
|
|
2123
|
+
agentCount = workers.length;
|
|
2124
|
+
// Check for high memory usage
|
|
2125
|
+
for (const worker of workers) {
|
|
2126
|
+
if (worker.pid) {
|
|
2127
|
+
try {
|
|
2128
|
+
const { execSync } = await import('child_process');
|
|
2129
|
+
const output = execSync(`ps -o rss= -p ${worker.pid}`, {
|
|
2130
|
+
encoding: 'utf8',
|
|
2131
|
+
timeout: 3000,
|
|
2132
|
+
}).trim();
|
|
2133
|
+
const rssBytes = parseInt(output, 10) * 1024;
|
|
2134
|
+
if (rssBytes > 1.5 * 1024 * 1024 * 1024) {
|
|
2135
|
+
// > 1.5GB
|
|
2136
|
+
healthScore -= 20;
|
|
2137
|
+
issues.push({
|
|
2138
|
+
severity: 'critical',
|
|
2139
|
+
message: `Agent "${worker.name}" is using ${Math.round(rssBytes / 1024 / 1024)}MB of memory`,
|
|
2140
|
+
});
|
|
2141
|
+
totalAlerts24h++;
|
|
2142
|
+
alerts.push({
|
|
2143
|
+
id: `alert-${Date.now()}-${worker.name}`,
|
|
2144
|
+
agentName: worker.name,
|
|
2145
|
+
alertType: 'oom_imminent',
|
|
2146
|
+
message: `Memory usage critical: ${Math.round(rssBytes / 1024 / 1024)}MB`,
|
|
2147
|
+
createdAt: new Date().toISOString(),
|
|
2148
|
+
});
|
|
2149
|
+
}
|
|
2150
|
+
else if (rssBytes > 1024 * 1024 * 1024) {
|
|
2151
|
+
// > 1GB
|
|
2152
|
+
healthScore -= 10;
|
|
2153
|
+
issues.push({
|
|
2154
|
+
severity: 'high',
|
|
2155
|
+
message: `Agent "${worker.name}" memory usage is elevated (${Math.round(rssBytes / 1024 / 1024)}MB)`,
|
|
2156
|
+
});
|
|
2157
|
+
}
|
|
2158
|
+
}
|
|
2159
|
+
catch {
|
|
2160
|
+
// Process may have exited
|
|
2161
|
+
}
|
|
2162
|
+
}
|
|
2163
|
+
}
|
|
2164
|
+
}
|
|
2165
|
+
// Check registered agents
|
|
2166
|
+
const agentsPath = path.join(teamDir, 'agents.json');
|
|
2167
|
+
if (fs.existsSync(agentsPath)) {
|
|
2168
|
+
const data = JSON.parse(fs.readFileSync(agentsPath, 'utf-8'));
|
|
2169
|
+
const registeredAgents = data.agents || [];
|
|
2170
|
+
const activeAgents = registeredAgents.filter((a) => {
|
|
2171
|
+
const lastSeen = a.lastSeen ? new Date(a.lastSeen).getTime() : 0;
|
|
2172
|
+
return Date.now() - lastSeen < 30000;
|
|
2173
|
+
});
|
|
2174
|
+
agentCount = Math.max(agentCount, activeAgents.length);
|
|
2175
|
+
}
|
|
2176
|
+
// Generate recommendations based on issues
|
|
2177
|
+
if (issues.some(i => i.severity === 'critical')) {
|
|
2178
|
+
recommendations.push('Consider restarting agents with high memory usage');
|
|
2179
|
+
recommendations.push('Monitor system resources closely');
|
|
2180
|
+
}
|
|
2181
|
+
if (agentCount === 0) {
|
|
2182
|
+
recommendations.push('No active agents detected - start agents to begin monitoring');
|
|
2183
|
+
}
|
|
2184
|
+
// Clamp health score
|
|
2185
|
+
healthScore = Math.max(0, Math.min(100, healthScore));
|
|
2186
|
+
// Generate summary
|
|
2187
|
+
let summary;
|
|
2188
|
+
if (healthScore >= 90) {
|
|
2189
|
+
summary = 'System is healthy. All agents operating normally.';
|
|
2190
|
+
}
|
|
2191
|
+
else if (healthScore >= 70) {
|
|
2192
|
+
summary = 'Some issues detected. Review warnings and recommendations.';
|
|
2193
|
+
}
|
|
2194
|
+
else if (healthScore >= 50) {
|
|
2195
|
+
summary = 'Multiple issues detected. Action recommended.';
|
|
2196
|
+
}
|
|
2197
|
+
else {
|
|
2198
|
+
summary = 'Critical issues detected. Immediate action required.';
|
|
2199
|
+
}
|
|
2200
|
+
res.json({
|
|
2201
|
+
healthScore,
|
|
2202
|
+
summary,
|
|
2203
|
+
issues,
|
|
2204
|
+
recommendations,
|
|
2205
|
+
crashes,
|
|
2206
|
+
alerts,
|
|
2207
|
+
stats: {
|
|
2208
|
+
totalCrashes24h,
|
|
2209
|
+
totalAlerts24h,
|
|
2210
|
+
agentCount,
|
|
2211
|
+
},
|
|
2212
|
+
});
|
|
2213
|
+
}
|
|
2214
|
+
catch (err) {
|
|
2215
|
+
console.error('Failed to compute health metrics', err);
|
|
2216
|
+
res.status(500).json({ error: 'Failed to compute health metrics' });
|
|
2217
|
+
}
|
|
2218
|
+
});
|
|
1567
2219
|
// ===== File Search API =====
|
|
1568
2220
|
/**
|
|
1569
2221
|
* GET /api/files - Search for files in the repository
|
|
@@ -2087,6 +2739,654 @@ Start by greeting the project leads and asking for status updates.`;
|
|
|
2087
2739
|
});
|
|
2088
2740
|
}
|
|
2089
2741
|
});
|
|
2742
|
+
/**
|
|
2743
|
+
* GET /api/trajectory - Get current trajectory status
|
|
2744
|
+
*/
|
|
2745
|
+
app.get('/api/trajectory', async (_req, res) => {
|
|
2746
|
+
try {
|
|
2747
|
+
const status = await getTrajectoryStatus();
|
|
2748
|
+
res.json({
|
|
2749
|
+
success: true,
|
|
2750
|
+
...status,
|
|
2751
|
+
});
|
|
2752
|
+
}
|
|
2753
|
+
catch (err) {
|
|
2754
|
+
console.error('[api] Trajectory status error:', err);
|
|
2755
|
+
res.status(500).json({
|
|
2756
|
+
success: false,
|
|
2757
|
+
error: err.message,
|
|
2758
|
+
});
|
|
2759
|
+
}
|
|
2760
|
+
});
|
|
2761
|
+
/**
|
|
2762
|
+
* GET /api/trajectory/steps - List trajectory steps
|
|
2763
|
+
*/
|
|
2764
|
+
app.get('/api/trajectory/steps', async (req, res) => {
|
|
2765
|
+
try {
|
|
2766
|
+
const trajectoryId = req.query.trajectoryId;
|
|
2767
|
+
const result = await listTrajectorySteps(trajectoryId);
|
|
2768
|
+
if (result.success) {
|
|
2769
|
+
res.json({
|
|
2770
|
+
success: true,
|
|
2771
|
+
steps: result.steps,
|
|
2772
|
+
});
|
|
2773
|
+
}
|
|
2774
|
+
else {
|
|
2775
|
+
res.status(500).json({
|
|
2776
|
+
success: false,
|
|
2777
|
+
steps: [],
|
|
2778
|
+
error: result.error,
|
|
2779
|
+
});
|
|
2780
|
+
}
|
|
2781
|
+
}
|
|
2782
|
+
catch (err) {
|
|
2783
|
+
console.error('[api] Trajectory steps error:', err);
|
|
2784
|
+
res.status(500).json({
|
|
2785
|
+
success: false,
|
|
2786
|
+
steps: [],
|
|
2787
|
+
error: err.message,
|
|
2788
|
+
});
|
|
2789
|
+
}
|
|
2790
|
+
});
|
|
2791
|
+
/**
|
|
2792
|
+
* GET /api/trajectory/history - List all trajectories (completed and active)
|
|
2793
|
+
*/
|
|
2794
|
+
app.get('/api/trajectory/history', async (_req, res) => {
|
|
2795
|
+
try {
|
|
2796
|
+
const result = await getTrajectoryHistory();
|
|
2797
|
+
if (result.success) {
|
|
2798
|
+
res.json({
|
|
2799
|
+
success: true,
|
|
2800
|
+
trajectories: result.trajectories,
|
|
2801
|
+
});
|
|
2802
|
+
}
|
|
2803
|
+
else {
|
|
2804
|
+
res.status(500).json({
|
|
2805
|
+
success: false,
|
|
2806
|
+
trajectories: [],
|
|
2807
|
+
error: result.error,
|
|
2808
|
+
});
|
|
2809
|
+
}
|
|
2810
|
+
}
|
|
2811
|
+
catch (err) {
|
|
2812
|
+
console.error('[api] Trajectory history error:', err);
|
|
2813
|
+
res.status(500).json({
|
|
2814
|
+
success: false,
|
|
2815
|
+
trajectories: [],
|
|
2816
|
+
error: err.message,
|
|
2817
|
+
});
|
|
2818
|
+
}
|
|
2819
|
+
});
|
|
2820
|
+
// ===== Settings API =====
|
|
2821
|
+
/**
|
|
2822
|
+
* GET /api/settings - Get all workspace settings with documentation
|
|
2823
|
+
*/
|
|
2824
|
+
app.get('/api/settings', async (_req, res) => {
|
|
2825
|
+
try {
|
|
2826
|
+
const { readRelayConfig, shouldStoreInRepo, getTrajectoriesStorageDescription } = await import('../trajectory/config.js');
|
|
2827
|
+
const config = readRelayConfig();
|
|
2828
|
+
res.json({
|
|
2829
|
+
success: true,
|
|
2830
|
+
settings: {
|
|
2831
|
+
trajectories: {
|
|
2832
|
+
storeInRepo: shouldStoreInRepo(),
|
|
2833
|
+
storageLocation: getTrajectoriesStorageDescription(),
|
|
2834
|
+
description: 'Trajectories record the journey of agent work using the PDERO paradigm (Plan, Design, Execute, Review, Observe). They capture decisions, phase transitions, and retrospectives.',
|
|
2835
|
+
benefits: [
|
|
2836
|
+
'Track why decisions were made, not just what was built',
|
|
2837
|
+
'Enable session recovery when agents crash or context is lost',
|
|
2838
|
+
'Provide learning data for future agents working on similar tasks',
|
|
2839
|
+
'Create an audit trail of agent work for review',
|
|
2840
|
+
],
|
|
2841
|
+
learnMore: 'https://pdero.com',
|
|
2842
|
+
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.',
|
|
2843
|
+
},
|
|
2844
|
+
},
|
|
2845
|
+
config,
|
|
2846
|
+
});
|
|
2847
|
+
}
|
|
2848
|
+
catch (err) {
|
|
2849
|
+
console.error('[api] Settings error:', err);
|
|
2850
|
+
res.status(500).json({
|
|
2851
|
+
success: false,
|
|
2852
|
+
error: err.message,
|
|
2853
|
+
});
|
|
2854
|
+
}
|
|
2855
|
+
});
|
|
2856
|
+
/**
|
|
2857
|
+
* GET /api/settings/trajectory - Get trajectory storage settings
|
|
2858
|
+
*/
|
|
2859
|
+
app.get('/api/settings/trajectory', async (_req, res) => {
|
|
2860
|
+
try {
|
|
2861
|
+
const { readRelayConfig, shouldStoreInRepo, getTrajectoriesStorageDescription } = await import('../trajectory/config.js');
|
|
2862
|
+
const config = readRelayConfig();
|
|
2863
|
+
res.json({
|
|
2864
|
+
success: true,
|
|
2865
|
+
settings: {
|
|
2866
|
+
storeInRepo: shouldStoreInRepo(),
|
|
2867
|
+
storageLocation: getTrajectoriesStorageDescription(),
|
|
2868
|
+
},
|
|
2869
|
+
config: config.trajectories || {},
|
|
2870
|
+
// Documentation for the UI
|
|
2871
|
+
documentation: {
|
|
2872
|
+
title: 'Trajectory Storage',
|
|
2873
|
+
description: 'Trajectories record the journey of agent work using the PDERO paradigm (Plan, Design, Execute, Review, Observe).',
|
|
2874
|
+
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.',
|
|
2875
|
+
benefits: [
|
|
2876
|
+
'Understand agent decision-making for code review',
|
|
2877
|
+
'Enable session recovery if agents crash',
|
|
2878
|
+
'Train future agents on your codebase patterns',
|
|
2879
|
+
'Create audit trails of AI work',
|
|
2880
|
+
],
|
|
2881
|
+
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/).',
|
|
2882
|
+
learnMore: 'https://pdero.com',
|
|
2883
|
+
},
|
|
2884
|
+
});
|
|
2885
|
+
}
|
|
2886
|
+
catch (err) {
|
|
2887
|
+
console.error('[api] Settings trajectory error:', err);
|
|
2888
|
+
res.status(500).json({
|
|
2889
|
+
success: false,
|
|
2890
|
+
error: err.message,
|
|
2891
|
+
});
|
|
2892
|
+
}
|
|
2893
|
+
});
|
|
2894
|
+
/**
|
|
2895
|
+
* PUT /api/settings/trajectory - Update trajectory storage settings
|
|
2896
|
+
*
|
|
2897
|
+
* Body: { storeInRepo: boolean }
|
|
2898
|
+
*
|
|
2899
|
+
* This writes to .relay/config.json in the project root
|
|
2900
|
+
*/
|
|
2901
|
+
app.put('/api/settings/trajectory', async (req, res) => {
|
|
2902
|
+
try {
|
|
2903
|
+
const { storeInRepo } = req.body;
|
|
2904
|
+
if (typeof storeInRepo !== 'boolean') {
|
|
2905
|
+
return res.status(400).json({
|
|
2906
|
+
success: false,
|
|
2907
|
+
error: 'storeInRepo must be a boolean',
|
|
2908
|
+
});
|
|
2909
|
+
}
|
|
2910
|
+
const { getRelayConfigPath, readRelayConfig } = await import('../trajectory/config.js');
|
|
2911
|
+
const { getProjectPaths } = await import('../utils/project-namespace.js');
|
|
2912
|
+
const { projectRoot: _projectRoot } = getProjectPaths();
|
|
2913
|
+
// Read existing config
|
|
2914
|
+
const config = readRelayConfig();
|
|
2915
|
+
// Update trajectory settings
|
|
2916
|
+
config.trajectories = {
|
|
2917
|
+
...config.trajectories,
|
|
2918
|
+
storeInRepo,
|
|
2919
|
+
};
|
|
2920
|
+
// Ensure .relay directory exists
|
|
2921
|
+
const configPath = getRelayConfigPath();
|
|
2922
|
+
const configDir = path.dirname(configPath);
|
|
2923
|
+
if (!fs.existsSync(configDir)) {
|
|
2924
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
2925
|
+
}
|
|
2926
|
+
// Write updated config
|
|
2927
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
2928
|
+
res.json({
|
|
2929
|
+
success: true,
|
|
2930
|
+
settings: {
|
|
2931
|
+
storeInRepo,
|
|
2932
|
+
storageLocation: storeInRepo ? 'repo (.trajectories/)' : 'user (~/.config/agent-relay/trajectories/)',
|
|
2933
|
+
},
|
|
2934
|
+
});
|
|
2935
|
+
}
|
|
2936
|
+
catch (err) {
|
|
2937
|
+
console.error('[api] Settings trajectory update error:', err);
|
|
2938
|
+
res.status(500).json({
|
|
2939
|
+
success: false,
|
|
2940
|
+
error: err.message,
|
|
2941
|
+
});
|
|
2942
|
+
}
|
|
2943
|
+
});
|
|
2944
|
+
const decisions = new Map();
|
|
2945
|
+
/**
|
|
2946
|
+
* GET /api/decisions - List all pending decisions
|
|
2947
|
+
*/
|
|
2948
|
+
app.get('/api/decisions', (_req, res) => {
|
|
2949
|
+
const allDecisions = Array.from(decisions.values())
|
|
2950
|
+
.sort((a, b) => {
|
|
2951
|
+
const urgencyOrder = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
2952
|
+
return urgencyOrder[a.urgency] - urgencyOrder[b.urgency];
|
|
2953
|
+
});
|
|
2954
|
+
res.json({ success: true, decisions: allDecisions });
|
|
2955
|
+
});
|
|
2956
|
+
/**
|
|
2957
|
+
* POST /api/decisions - Create a new decision request
|
|
2958
|
+
* Body: { agentName, title, description, options?, urgency, category, expiresAt?, context? }
|
|
2959
|
+
*/
|
|
2960
|
+
app.post('/api/decisions', (req, res) => {
|
|
2961
|
+
const { agentName, title, description, options, urgency, category, expiresAt, context } = req.body;
|
|
2962
|
+
if (!agentName || !title || !urgency || !category) {
|
|
2963
|
+
return res.status(400).json({
|
|
2964
|
+
success: false,
|
|
2965
|
+
error: 'Missing required fields: agentName, title, urgency, category',
|
|
2966
|
+
});
|
|
2967
|
+
}
|
|
2968
|
+
const decision = {
|
|
2969
|
+
id: `decision-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
2970
|
+
agentName,
|
|
2971
|
+
title,
|
|
2972
|
+
description: description || '',
|
|
2973
|
+
options,
|
|
2974
|
+
urgency,
|
|
2975
|
+
category,
|
|
2976
|
+
createdAt: new Date().toISOString(),
|
|
2977
|
+
expiresAt,
|
|
2978
|
+
context,
|
|
2979
|
+
};
|
|
2980
|
+
decisions.set(decision.id, decision);
|
|
2981
|
+
// Broadcast to WebSocket clients
|
|
2982
|
+
broadcastData().catch(() => { });
|
|
2983
|
+
res.json({ success: true, decision });
|
|
2984
|
+
});
|
|
2985
|
+
/**
|
|
2986
|
+
* POST /api/decisions/:id/approve - Approve/resolve a decision
|
|
2987
|
+
* Body: { optionId?: string, response?: string }
|
|
2988
|
+
*/
|
|
2989
|
+
app.post('/api/decisions/:id/approve', async (req, res) => {
|
|
2990
|
+
const { id } = req.params;
|
|
2991
|
+
const { optionId, response } = req.body;
|
|
2992
|
+
const decision = decisions.get(id);
|
|
2993
|
+
if (!decision) {
|
|
2994
|
+
return res.status(404).json({ success: false, error: 'Decision not found' });
|
|
2995
|
+
}
|
|
2996
|
+
// Send response to the agent via relay
|
|
2997
|
+
const agentName = decision.agentName;
|
|
2998
|
+
let responseMessage = `DECISION APPROVED: ${decision.title}`;
|
|
2999
|
+
if (optionId && decision.options) {
|
|
3000
|
+
const option = decision.options.find(o => o.id === optionId);
|
|
3001
|
+
if (option) {
|
|
3002
|
+
responseMessage += `\nSelected: ${option.label}`;
|
|
3003
|
+
}
|
|
3004
|
+
}
|
|
3005
|
+
if (response) {
|
|
3006
|
+
responseMessage += `\nResponse: ${response}`;
|
|
3007
|
+
}
|
|
3008
|
+
// Try to send message to agent
|
|
3009
|
+
try {
|
|
3010
|
+
const client = await getRelayClient('Dashboard');
|
|
3011
|
+
if (client) {
|
|
3012
|
+
await client.sendMessage(agentName, responseMessage, 'message');
|
|
3013
|
+
}
|
|
3014
|
+
}
|
|
3015
|
+
catch (err) {
|
|
3016
|
+
console.warn('[api] Could not send decision response to agent:', err);
|
|
3017
|
+
}
|
|
3018
|
+
decisions.delete(id);
|
|
3019
|
+
broadcastData().catch(() => { });
|
|
3020
|
+
res.json({ success: true, message: 'Decision approved' });
|
|
3021
|
+
});
|
|
3022
|
+
/**
|
|
3023
|
+
* POST /api/decisions/:id/reject - Reject a decision
|
|
3024
|
+
* Body: { reason?: string }
|
|
3025
|
+
*/
|
|
3026
|
+
app.post('/api/decisions/:id/reject', async (req, res) => {
|
|
3027
|
+
const { id } = req.params;
|
|
3028
|
+
const { reason } = req.body;
|
|
3029
|
+
const decision = decisions.get(id);
|
|
3030
|
+
if (!decision) {
|
|
3031
|
+
return res.status(404).json({ success: false, error: 'Decision not found' });
|
|
3032
|
+
}
|
|
3033
|
+
// Send rejection to the agent
|
|
3034
|
+
const agentName = decision.agentName;
|
|
3035
|
+
let responseMessage = `DECISION REJECTED: ${decision.title}`;
|
|
3036
|
+
if (reason) {
|
|
3037
|
+
responseMessage += `\nReason: ${reason}`;
|
|
3038
|
+
}
|
|
3039
|
+
try {
|
|
3040
|
+
const client = await getRelayClient('Dashboard');
|
|
3041
|
+
if (client) {
|
|
3042
|
+
await client.sendMessage(agentName, responseMessage, 'message');
|
|
3043
|
+
}
|
|
3044
|
+
}
|
|
3045
|
+
catch (err) {
|
|
3046
|
+
console.warn('[api] Could not send decision rejection to agent:', err);
|
|
3047
|
+
}
|
|
3048
|
+
decisions.delete(id);
|
|
3049
|
+
broadcastData().catch(() => { });
|
|
3050
|
+
res.json({ success: true, message: 'Decision rejected' });
|
|
3051
|
+
});
|
|
3052
|
+
/**
|
|
3053
|
+
* DELETE /api/decisions/:id - Delete/dismiss a decision
|
|
3054
|
+
*/
|
|
3055
|
+
app.delete('/api/decisions/:id', (_req, res) => {
|
|
3056
|
+
const { id } = _req.params;
|
|
3057
|
+
if (!decisions.has(id)) {
|
|
3058
|
+
return res.status(404).json({ success: false, error: 'Decision not found' });
|
|
3059
|
+
}
|
|
3060
|
+
decisions.delete(id);
|
|
3061
|
+
broadcastData().catch(() => { });
|
|
3062
|
+
res.json({ success: true, message: 'Decision dismissed' });
|
|
3063
|
+
});
|
|
3064
|
+
/**
|
|
3065
|
+
* GET /api/fleet/servers - Get fleet server overview
|
|
3066
|
+
* Returns local daemon info + any connected bridge servers
|
|
3067
|
+
* Note: When bridge is active, local agents are already included in bridge project agents,
|
|
3068
|
+
* so we don't add a separate "Local Daemon" entry to avoid double-counting.
|
|
3069
|
+
*/
|
|
3070
|
+
app.get('/api/fleet/servers', async (_req, res) => {
|
|
3071
|
+
const servers = [];
|
|
3072
|
+
const localAgents = spawner?.getActiveWorkers() || [];
|
|
3073
|
+
const agentStatuses = await loadAgentStatuses();
|
|
3074
|
+
let hasBridgeProjects = false;
|
|
3075
|
+
// Check for bridge connections first
|
|
3076
|
+
const bridgeStatePath = path.join(dataDir, 'bridge-state.json');
|
|
3077
|
+
if (fs.existsSync(bridgeStatePath)) {
|
|
3078
|
+
try {
|
|
3079
|
+
const bridgeState = JSON.parse(fs.readFileSync(bridgeStatePath, 'utf-8'));
|
|
3080
|
+
if (bridgeState.projects && bridgeState.projects.length > 0) {
|
|
3081
|
+
hasBridgeProjects = true;
|
|
3082
|
+
for (const project of bridgeState.projects) {
|
|
3083
|
+
// Enrich with actual online agents from agents.json (same logic as getBridgeData)
|
|
3084
|
+
// This fixes the bug where stale agents were counted
|
|
3085
|
+
let projectAgents = [];
|
|
3086
|
+
if (project.path) {
|
|
3087
|
+
const projectHash = crypto.createHash('sha256').update(project.path).digest('hex').slice(0, 12);
|
|
3088
|
+
const projectDataDir = path.join(path.dirname(dataDir), projectHash);
|
|
3089
|
+
const projectTeamDir = path.join(projectDataDir, 'team');
|
|
3090
|
+
const agentsPath = path.join(projectTeamDir, 'agents.json');
|
|
3091
|
+
if (fs.existsSync(agentsPath)) {
|
|
3092
|
+
try {
|
|
3093
|
+
const agentsData = JSON.parse(fs.readFileSync(agentsPath, 'utf-8'));
|
|
3094
|
+
if (agentsData.agents && Array.isArray(agentsData.agents)) {
|
|
3095
|
+
// Filter to only show online agents (seen within 30 seconds)
|
|
3096
|
+
const thirtySecondsAgo = Date.now() - 30 * 1000;
|
|
3097
|
+
projectAgents = agentsData.agents
|
|
3098
|
+
.filter((a) => {
|
|
3099
|
+
if (!a.lastSeen)
|
|
3100
|
+
return false;
|
|
3101
|
+
return new Date(a.lastSeen).getTime() > thirtySecondsAgo;
|
|
3102
|
+
})
|
|
3103
|
+
.map((a) => ({
|
|
3104
|
+
name: a.name,
|
|
3105
|
+
status: 'online',
|
|
3106
|
+
}));
|
|
3107
|
+
}
|
|
3108
|
+
}
|
|
3109
|
+
catch (e) {
|
|
3110
|
+
console.warn(`[api] Failed to read agents for ${project.path}:`, e);
|
|
3111
|
+
}
|
|
3112
|
+
}
|
|
3113
|
+
}
|
|
3114
|
+
servers.push({
|
|
3115
|
+
id: project.id,
|
|
3116
|
+
name: project.name || project.path.split('/').pop() || project.id,
|
|
3117
|
+
status: project.connected ? 'healthy' : 'offline',
|
|
3118
|
+
agents: projectAgents,
|
|
3119
|
+
cpuUsage: 0,
|
|
3120
|
+
memoryUsage: 0,
|
|
3121
|
+
activeConnections: project.connected ? 1 : 0,
|
|
3122
|
+
uptime: 0,
|
|
3123
|
+
lastHeartbeat: project.lastSeen || new Date().toISOString(),
|
|
3124
|
+
});
|
|
3125
|
+
}
|
|
3126
|
+
}
|
|
3127
|
+
}
|
|
3128
|
+
catch (err) {
|
|
3129
|
+
console.warn('[api] Failed to read bridge state:', err);
|
|
3130
|
+
}
|
|
3131
|
+
}
|
|
3132
|
+
// Only add local daemon entry if we don't have bridge projects
|
|
3133
|
+
// (otherwise local agents are already counted in the bridge project)
|
|
3134
|
+
if (!hasBridgeProjects) {
|
|
3135
|
+
servers.push({
|
|
3136
|
+
id: 'local',
|
|
3137
|
+
name: 'Local Daemon',
|
|
3138
|
+
status: 'healthy',
|
|
3139
|
+
agents: localAgents.map(a => ({
|
|
3140
|
+
name: a.name,
|
|
3141
|
+
status: agentStatuses[a.name]?.status || 'unknown',
|
|
3142
|
+
})),
|
|
3143
|
+
cpuUsage: Math.random() * 30, // Mock - would come from actual metrics
|
|
3144
|
+
memoryUsage: Math.random() * 50,
|
|
3145
|
+
activeConnections: wss.clients.size,
|
|
3146
|
+
uptime: process.uptime(),
|
|
3147
|
+
lastHeartbeat: new Date().toISOString(),
|
|
3148
|
+
});
|
|
3149
|
+
}
|
|
3150
|
+
res.json({ success: true, servers });
|
|
3151
|
+
});
|
|
3152
|
+
/**
|
|
3153
|
+
* GET /api/fleet/stats - Get aggregate fleet statistics
|
|
3154
|
+
*/
|
|
3155
|
+
app.get('/api/fleet/stats', async (_req, res) => {
|
|
3156
|
+
const localAgents = spawner?.getActiveWorkers() || [];
|
|
3157
|
+
const agentStatuses = await loadAgentStatuses();
|
|
3158
|
+
const totalAgents = localAgents.length;
|
|
3159
|
+
let onlineAgents = 0;
|
|
3160
|
+
let busyAgents = 0;
|
|
3161
|
+
for (const agent of localAgents) {
|
|
3162
|
+
const status = agentStatuses[agent.name]?.status;
|
|
3163
|
+
if (status === 'online')
|
|
3164
|
+
onlineAgents++;
|
|
3165
|
+
if (status === 'busy')
|
|
3166
|
+
busyAgents++;
|
|
3167
|
+
}
|
|
3168
|
+
res.json({
|
|
3169
|
+
success: true,
|
|
3170
|
+
stats: {
|
|
3171
|
+
totalAgents,
|
|
3172
|
+
onlineAgents,
|
|
3173
|
+
busyAgents,
|
|
3174
|
+
pendingDecisions: decisions.size,
|
|
3175
|
+
activeTasks: Array.from(tasks.values()).filter(t => t.status === 'assigned' || t.status === 'in_progress').length,
|
|
3176
|
+
},
|
|
3177
|
+
});
|
|
3178
|
+
});
|
|
3179
|
+
const tasks = new Map();
|
|
3180
|
+
/**
|
|
3181
|
+
* GET /api/tasks - List all tasks
|
|
3182
|
+
*/
|
|
3183
|
+
app.get('/api/tasks', (req, res) => {
|
|
3184
|
+
const status = req.query.status;
|
|
3185
|
+
const agentName = req.query.agent;
|
|
3186
|
+
let allTasks = Array.from(tasks.values());
|
|
3187
|
+
if (status) {
|
|
3188
|
+
allTasks = allTasks.filter(t => t.status === status);
|
|
3189
|
+
}
|
|
3190
|
+
if (agentName) {
|
|
3191
|
+
allTasks = allTasks.filter(t => t.agentName === agentName);
|
|
3192
|
+
}
|
|
3193
|
+
// Sort by priority and creation time
|
|
3194
|
+
allTasks.sort((a, b) => {
|
|
3195
|
+
const priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
3196
|
+
const priorityDiff = priorityOrder[a.priority] - priorityOrder[b.priority];
|
|
3197
|
+
if (priorityDiff !== 0)
|
|
3198
|
+
return priorityDiff;
|
|
3199
|
+
return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime();
|
|
3200
|
+
});
|
|
3201
|
+
res.json({ success: true, tasks: allTasks });
|
|
3202
|
+
});
|
|
3203
|
+
/**
|
|
3204
|
+
* POST /api/tasks - Create and assign a task
|
|
3205
|
+
* Body: { agentName, title, description, priority }
|
|
3206
|
+
*/
|
|
3207
|
+
app.post('/api/tasks', async (req, res) => {
|
|
3208
|
+
const { agentName, title, description, priority } = req.body;
|
|
3209
|
+
if (!agentName || !title || !priority) {
|
|
3210
|
+
return res.status(400).json({
|
|
3211
|
+
success: false,
|
|
3212
|
+
error: 'Missing required fields: agentName, title, priority',
|
|
3213
|
+
});
|
|
3214
|
+
}
|
|
3215
|
+
const task = {
|
|
3216
|
+
id: `task-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
3217
|
+
agentName,
|
|
3218
|
+
title,
|
|
3219
|
+
description: description || '',
|
|
3220
|
+
priority,
|
|
3221
|
+
status: 'assigned',
|
|
3222
|
+
createdAt: new Date().toISOString(),
|
|
3223
|
+
assignedAt: new Date().toISOString(),
|
|
3224
|
+
};
|
|
3225
|
+
tasks.set(task.id, task);
|
|
3226
|
+
// Send task to agent via relay
|
|
3227
|
+
try {
|
|
3228
|
+
const client = await getRelayClient('Dashboard');
|
|
3229
|
+
if (client) {
|
|
3230
|
+
const taskMessage = `TASK ASSIGNED [${priority.toUpperCase()}]: ${title}\n\n${description || 'No additional details.'}`;
|
|
3231
|
+
await client.sendMessage(agentName, taskMessage, 'message');
|
|
3232
|
+
}
|
|
3233
|
+
}
|
|
3234
|
+
catch (err) {
|
|
3235
|
+
console.warn('[api] Could not send task to agent:', err);
|
|
3236
|
+
}
|
|
3237
|
+
broadcastData().catch(() => { });
|
|
3238
|
+
res.json({ success: true, task });
|
|
3239
|
+
});
|
|
3240
|
+
/**
|
|
3241
|
+
* PATCH /api/tasks/:id - Update task status
|
|
3242
|
+
* Body: { status, result? }
|
|
3243
|
+
*/
|
|
3244
|
+
app.patch('/api/tasks/:id', (req, res) => {
|
|
3245
|
+
const { id } = req.params;
|
|
3246
|
+
const { status, result } = req.body;
|
|
3247
|
+
const task = tasks.get(id);
|
|
3248
|
+
if (!task) {
|
|
3249
|
+
return res.status(404).json({ success: false, error: 'Task not found' });
|
|
3250
|
+
}
|
|
3251
|
+
if (status) {
|
|
3252
|
+
task.status = status;
|
|
3253
|
+
if (status === 'completed' || status === 'failed') {
|
|
3254
|
+
task.completedAt = new Date().toISOString();
|
|
3255
|
+
}
|
|
3256
|
+
}
|
|
3257
|
+
if (result !== undefined) {
|
|
3258
|
+
task.result = result;
|
|
3259
|
+
}
|
|
3260
|
+
tasks.set(id, task);
|
|
3261
|
+
broadcastData().catch(() => { });
|
|
3262
|
+
res.json({ success: true, task });
|
|
3263
|
+
});
|
|
3264
|
+
/**
|
|
3265
|
+
* DELETE /api/tasks/:id - Cancel/delete a task
|
|
3266
|
+
*/
|
|
3267
|
+
app.delete('/api/tasks/:id', async (req, res) => {
|
|
3268
|
+
const { id } = req.params;
|
|
3269
|
+
const task = tasks.get(id);
|
|
3270
|
+
if (!task) {
|
|
3271
|
+
return res.status(404).json({ success: false, error: 'Task not found' });
|
|
3272
|
+
}
|
|
3273
|
+
// Notify agent of cancellation if task is still active
|
|
3274
|
+
if (task.status === 'pending' || task.status === 'assigned' || task.status === 'in_progress') {
|
|
3275
|
+
try {
|
|
3276
|
+
const client = await getRelayClient('Dashboard');
|
|
3277
|
+
if (client) {
|
|
3278
|
+
await client.sendMessage(task.agentName, `TASK CANCELLED: ${task.title}`, 'message');
|
|
3279
|
+
}
|
|
3280
|
+
}
|
|
3281
|
+
catch (err) {
|
|
3282
|
+
console.warn('[api] Could not send task cancellation to agent:', err);
|
|
3283
|
+
}
|
|
3284
|
+
}
|
|
3285
|
+
tasks.delete(id);
|
|
3286
|
+
broadcastData().catch(() => { });
|
|
3287
|
+
res.json({ success: true, message: 'Task cancelled' });
|
|
3288
|
+
});
|
|
3289
|
+
// ===== Beads Integration API =====
|
|
3290
|
+
/**
|
|
3291
|
+
* POST /api/beads - Create a bead (task/issue) via the bd CLI
|
|
3292
|
+
*/
|
|
3293
|
+
app.post('/api/beads', async (req, res) => {
|
|
3294
|
+
const { title, assignee, priority, type, description: _description } = req.body;
|
|
3295
|
+
if (!title || typeof title !== 'string') {
|
|
3296
|
+
return res.status(400).json({ success: false, error: 'Title is required' });
|
|
3297
|
+
}
|
|
3298
|
+
// Build bd create command
|
|
3299
|
+
const args = ['create', `--title="${title.replace(/"/g, '\\"')}"`];
|
|
3300
|
+
if (assignee) {
|
|
3301
|
+
args.push(`--assignee=${assignee}`);
|
|
3302
|
+
}
|
|
3303
|
+
if (priority !== undefined && priority !== null) {
|
|
3304
|
+
args.push(`--priority=${priority}`);
|
|
3305
|
+
}
|
|
3306
|
+
if (type && ['task', 'bug', 'feature'].includes(type)) {
|
|
3307
|
+
args.push(`--type=${type}`);
|
|
3308
|
+
}
|
|
3309
|
+
const cmd = `bd ${args.join(' ')}`;
|
|
3310
|
+
console.log('[api/beads] Creating bead:', cmd);
|
|
3311
|
+
// Execute bd create command
|
|
3312
|
+
exec(cmd, { cwd: dataDir }, (error, stdout, stderr) => {
|
|
3313
|
+
if (error) {
|
|
3314
|
+
console.error('[api/beads] bd create failed:', stderr || error.message);
|
|
3315
|
+
return res.status(500).json({
|
|
3316
|
+
success: false,
|
|
3317
|
+
error: stderr || error.message || 'Failed to create bead',
|
|
3318
|
+
});
|
|
3319
|
+
}
|
|
3320
|
+
// Parse bead ID from output (bd create outputs the ID)
|
|
3321
|
+
const output = stdout.trim();
|
|
3322
|
+
// bd create typically outputs: "Created beads-xxx: title"
|
|
3323
|
+
const idMatch = output.match(/Created\s+(beads-\w+)/i) || output.match(/(beads-\w+)/);
|
|
3324
|
+
const beadId = idMatch ? idMatch[1] : `beads-${Date.now()}`;
|
|
3325
|
+
console.log('[api/beads] Created bead:', beadId);
|
|
3326
|
+
res.json({
|
|
3327
|
+
success: true,
|
|
3328
|
+
bead: {
|
|
3329
|
+
id: beadId,
|
|
3330
|
+
title,
|
|
3331
|
+
assignee,
|
|
3332
|
+
priority,
|
|
3333
|
+
type: type || 'task',
|
|
3334
|
+
},
|
|
3335
|
+
});
|
|
3336
|
+
});
|
|
3337
|
+
});
|
|
3338
|
+
/**
|
|
3339
|
+
* POST /api/relay/send - Send a relay message to an agent
|
|
3340
|
+
*/
|
|
3341
|
+
app.post('/api/relay/send', async (req, res) => {
|
|
3342
|
+
const { to, content, thread } = req.body;
|
|
3343
|
+
if (!to || typeof to !== 'string') {
|
|
3344
|
+
return res.status(400).json({ success: false, error: 'Recipient (to) is required' });
|
|
3345
|
+
}
|
|
3346
|
+
if (!content || typeof content !== 'string') {
|
|
3347
|
+
return res.status(400).json({ success: false, error: 'Message content is required' });
|
|
3348
|
+
}
|
|
3349
|
+
try {
|
|
3350
|
+
const client = await getRelayClient('Dashboard');
|
|
3351
|
+
if (!client) {
|
|
3352
|
+
return res.status(503).json({
|
|
3353
|
+
success: false,
|
|
3354
|
+
error: 'Relay client not available',
|
|
3355
|
+
});
|
|
3356
|
+
}
|
|
3357
|
+
const messageId = await client.sendMessage(to, content, thread ? 'message' : 'message');
|
|
3358
|
+
console.log('[api/relay/send] Sent message to', to, ':', messageId);
|
|
3359
|
+
res.json({
|
|
3360
|
+
success: true,
|
|
3361
|
+
messageId: messageId || `msg-${Date.now()}`,
|
|
3362
|
+
});
|
|
3363
|
+
}
|
|
3364
|
+
catch (err) {
|
|
3365
|
+
console.error('[api/relay/send] Failed to send message:', err);
|
|
3366
|
+
res.status(500).json({
|
|
3367
|
+
success: false,
|
|
3368
|
+
error: err instanceof Error ? err.message : 'Failed to send message',
|
|
3369
|
+
});
|
|
3370
|
+
}
|
|
3371
|
+
});
|
|
3372
|
+
// Helper to load agent statuses
|
|
3373
|
+
async function loadAgentStatuses() {
|
|
3374
|
+
const agentsFile = path.join(dataDir, 'agents.json');
|
|
3375
|
+
try {
|
|
3376
|
+
if (fs.existsSync(agentsFile)) {
|
|
3377
|
+
const data = JSON.parse(fs.readFileSync(agentsFile, 'utf-8'));
|
|
3378
|
+
const result = {};
|
|
3379
|
+
for (const agent of data.agents || []) {
|
|
3380
|
+
result[agent.name] = { status: agent.status || 'offline' };
|
|
3381
|
+
}
|
|
3382
|
+
return result;
|
|
3383
|
+
}
|
|
3384
|
+
}
|
|
3385
|
+
catch (err) {
|
|
3386
|
+
console.warn('[api] Failed to load agent statuses:', err);
|
|
3387
|
+
}
|
|
3388
|
+
return {};
|
|
3389
|
+
}
|
|
2090
3390
|
// Watch for changes
|
|
2091
3391
|
if (storage) {
|
|
2092
3392
|
setInterval(() => {
|