agent-relay 1.0.21 → 1.1.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.
Files changed (283) hide show
  1. package/dist/bridge/shadow-cli.d.ts +17 -0
  2. package/dist/bridge/shadow-cli.d.ts.map +1 -0
  3. package/dist/bridge/shadow-cli.js +75 -0
  4. package/dist/bridge/shadow-cli.js.map +1 -0
  5. package/dist/bridge/shadow-config.d.ts +87 -0
  6. package/dist/bridge/shadow-config.d.ts.map +1 -0
  7. package/dist/bridge/shadow-config.js +134 -0
  8. package/dist/bridge/shadow-config.js.map +1 -0
  9. package/dist/bridge/spawner.d.ts +15 -1
  10. package/dist/bridge/spawner.d.ts.map +1 -1
  11. package/dist/bridge/spawner.js +164 -4
  12. package/dist/bridge/spawner.js.map +1 -1
  13. package/dist/bridge/types.d.ts +55 -0
  14. package/dist/bridge/types.d.ts.map +1 -1
  15. package/dist/cli/index.js +796 -11
  16. package/dist/cli/index.js.map +1 -1
  17. package/dist/cloud/api/auth.d.ts +19 -0
  18. package/dist/cloud/api/auth.d.ts.map +1 -0
  19. package/dist/cloud/api/auth.js +216 -0
  20. package/dist/cloud/api/auth.js.map +1 -0
  21. package/dist/cloud/api/billing.d.ts +17 -0
  22. package/dist/cloud/api/billing.d.ts.map +1 -0
  23. package/dist/cloud/api/billing.js +353 -0
  24. package/dist/cloud/api/billing.js.map +1 -0
  25. package/dist/cloud/api/coordinators.d.ts +8 -0
  26. package/dist/cloud/api/coordinators.d.ts.map +1 -0
  27. package/dist/cloud/api/coordinators.js +347 -0
  28. package/dist/cloud/api/coordinators.js.map +1 -0
  29. package/dist/cloud/api/daemons.d.ts +12 -0
  30. package/dist/cloud/api/daemons.d.ts.map +1 -0
  31. package/dist/cloud/api/daemons.js +320 -0
  32. package/dist/cloud/api/daemons.js.map +1 -0
  33. package/dist/cloud/api/middleware/planLimits.d.ts +36 -0
  34. package/dist/cloud/api/middleware/planLimits.d.ts.map +1 -0
  35. package/dist/cloud/api/middleware/planLimits.js +164 -0
  36. package/dist/cloud/api/middleware/planLimits.js.map +1 -0
  37. package/dist/cloud/api/onboarding.d.ts +8 -0
  38. package/dist/cloud/api/onboarding.d.ts.map +1 -0
  39. package/dist/cloud/api/onboarding.js +407 -0
  40. package/dist/cloud/api/onboarding.js.map +1 -0
  41. package/dist/cloud/api/providers.d.ts +7 -0
  42. package/dist/cloud/api/providers.d.ts.map +1 -0
  43. package/dist/cloud/api/providers.js +435 -0
  44. package/dist/cloud/api/providers.js.map +1 -0
  45. package/dist/cloud/api/repos.d.ts +7 -0
  46. package/dist/cloud/api/repos.d.ts.map +1 -0
  47. package/dist/cloud/api/repos.js +314 -0
  48. package/dist/cloud/api/repos.js.map +1 -0
  49. package/dist/cloud/api/teams.d.ts +7 -0
  50. package/dist/cloud/api/teams.d.ts.map +1 -0
  51. package/dist/cloud/api/teams.js +279 -0
  52. package/dist/cloud/api/teams.js.map +1 -0
  53. package/dist/cloud/api/usage.d.ts +7 -0
  54. package/dist/cloud/api/usage.d.ts.map +1 -0
  55. package/dist/cloud/api/usage.js +98 -0
  56. package/dist/cloud/api/usage.js.map +1 -0
  57. package/dist/cloud/api/workspaces.d.ts +7 -0
  58. package/dist/cloud/api/workspaces.d.ts.map +1 -0
  59. package/dist/cloud/api/workspaces.js +510 -0
  60. package/dist/cloud/api/workspaces.js.map +1 -0
  61. package/dist/cloud/billing/index.d.ts +9 -0
  62. package/dist/cloud/billing/index.d.ts.map +1 -0
  63. package/dist/cloud/billing/index.js +9 -0
  64. package/dist/cloud/billing/index.js.map +1 -0
  65. package/dist/cloud/billing/plans.d.ts +39 -0
  66. package/dist/cloud/billing/plans.d.ts.map +1 -0
  67. package/dist/cloud/billing/plans.js +232 -0
  68. package/dist/cloud/billing/plans.js.map +1 -0
  69. package/dist/cloud/billing/service.d.ts +80 -0
  70. package/dist/cloud/billing/service.d.ts.map +1 -0
  71. package/dist/cloud/billing/service.js +388 -0
  72. package/dist/cloud/billing/service.js.map +1 -0
  73. package/dist/cloud/billing/types.d.ts +135 -0
  74. package/dist/cloud/billing/types.d.ts.map +1 -0
  75. package/dist/cloud/billing/types.js +7 -0
  76. package/dist/cloud/billing/types.js.map +1 -0
  77. package/dist/cloud/config.d.ts +59 -0
  78. package/dist/cloud/config.d.ts.map +1 -0
  79. package/dist/cloud/config.js +83 -0
  80. package/dist/cloud/config.js.map +1 -0
  81. package/dist/cloud/db/drizzle.d.ts +132 -0
  82. package/dist/cloud/db/drizzle.d.ts.map +1 -0
  83. package/dist/cloud/db/drizzle.js +613 -0
  84. package/dist/cloud/db/drizzle.js.map +1 -0
  85. package/dist/cloud/db/index.d.ts +30 -0
  86. package/dist/cloud/db/index.d.ts.map +1 -0
  87. package/dist/cloud/db/index.js +44 -0
  88. package/dist/cloud/db/index.js.map +1 -0
  89. package/dist/cloud/db/schema.d.ts +1792 -0
  90. package/dist/cloud/db/schema.d.ts.map +1 -0
  91. package/dist/cloud/db/schema.js +234 -0
  92. package/dist/cloud/db/schema.js.map +1 -0
  93. package/dist/cloud/index.d.ts +11 -0
  94. package/dist/cloud/index.d.ts.map +1 -0
  95. package/dist/cloud/index.js +37 -0
  96. package/dist/cloud/index.js.map +1 -0
  97. package/dist/cloud/provisioner/index.d.ts +51 -0
  98. package/dist/cloud/provisioner/index.d.ts.map +1 -0
  99. package/dist/cloud/provisioner/index.js +676 -0
  100. package/dist/cloud/provisioner/index.js.map +1 -0
  101. package/dist/cloud/server.d.ts +16 -0
  102. package/dist/cloud/server.d.ts.map +1 -0
  103. package/dist/cloud/server.js +190 -0
  104. package/dist/cloud/server.js.map +1 -0
  105. package/dist/cloud/services/coordinator.d.ts +62 -0
  106. package/dist/cloud/services/coordinator.d.ts.map +1 -0
  107. package/dist/cloud/services/coordinator.js +389 -0
  108. package/dist/cloud/services/coordinator.js.map +1 -0
  109. package/dist/cloud/services/planLimits.d.ts +110 -0
  110. package/dist/cloud/services/planLimits.d.ts.map +1 -0
  111. package/dist/cloud/services/planLimits.js +254 -0
  112. package/dist/cloud/services/planLimits.js.map +1 -0
  113. package/dist/cloud/vault/index.d.ts +76 -0
  114. package/dist/cloud/vault/index.d.ts.map +1 -0
  115. package/dist/cloud/vault/index.js +219 -0
  116. package/dist/cloud/vault/index.js.map +1 -0
  117. package/dist/daemon/agent-manager.d.ts +87 -0
  118. package/dist/daemon/agent-manager.d.ts.map +1 -0
  119. package/dist/daemon/agent-manager.js +412 -0
  120. package/dist/daemon/agent-manager.js.map +1 -0
  121. package/dist/daemon/agent-registry.d.ts +2 -0
  122. package/dist/daemon/agent-registry.d.ts.map +1 -1
  123. package/dist/daemon/agent-registry.js +3 -0
  124. package/dist/daemon/agent-registry.js.map +1 -1
  125. package/dist/daemon/api.d.ts +69 -0
  126. package/dist/daemon/api.d.ts.map +1 -0
  127. package/dist/daemon/api.js +425 -0
  128. package/dist/daemon/api.js.map +1 -0
  129. package/dist/daemon/cloud-sync.d.ts +101 -0
  130. package/dist/daemon/cloud-sync.d.ts.map +1 -0
  131. package/dist/daemon/cloud-sync.js +261 -0
  132. package/dist/daemon/cloud-sync.js.map +1 -0
  133. package/dist/daemon/index.d.ts +4 -0
  134. package/dist/daemon/index.d.ts.map +1 -1
  135. package/dist/daemon/index.js +6 -0
  136. package/dist/daemon/index.js.map +1 -1
  137. package/dist/daemon/orchestrator.d.ts +155 -0
  138. package/dist/daemon/orchestrator.d.ts.map +1 -0
  139. package/dist/daemon/orchestrator.js +736 -0
  140. package/dist/daemon/orchestrator.js.map +1 -0
  141. package/dist/daemon/router.d.ts +24 -0
  142. package/dist/daemon/router.d.ts.map +1 -1
  143. package/dist/daemon/router.js +71 -1
  144. package/dist/daemon/router.js.map +1 -1
  145. package/dist/daemon/server.d.ts +37 -0
  146. package/dist/daemon/server.d.ts.map +1 -1
  147. package/dist/daemon/server.js +191 -16
  148. package/dist/daemon/server.js.map +1 -1
  149. package/dist/daemon/types.d.ts +127 -0
  150. package/dist/daemon/types.d.ts.map +1 -0
  151. package/dist/daemon/types.js +6 -0
  152. package/dist/daemon/types.js.map +1 -0
  153. package/dist/daemon/workspace-manager.d.ts +75 -0
  154. package/dist/daemon/workspace-manager.d.ts.map +1 -0
  155. package/dist/daemon/workspace-manager.js +289 -0
  156. package/dist/daemon/workspace-manager.js.map +1 -0
  157. package/dist/dashboard/out/404.html +1 -1
  158. package/dist/dashboard/out/_next/static/chunks/693-7b3301d8f6bc5014.js +1 -0
  159. package/dist/dashboard/out/_next/static/chunks/713-f78477eb185f1f4d.js +1 -0
  160. package/dist/dashboard/out/_next/static/chunks/766-e53e1cfe39b0b5b5.js +1 -0
  161. package/dist/dashboard/out/_next/static/chunks/900-037c64bfd797fb2a.js +1 -0
  162. package/dist/dashboard/out/_next/static/chunks/app/app/page-e3d9e1f4466b9bae.js +1 -0
  163. package/dist/dashboard/out/_next/static/chunks/app/history/page-b6edd4dde8d08194.js +1 -0
  164. package/dist/dashboard/out/_next/static/chunks/app/layout-2433bb48965f4333.js +1 -0
  165. package/dist/dashboard/out/_next/static/chunks/app/metrics/page-e68825a81db67ba1.js +1 -0
  166. package/dist/dashboard/out/_next/static/chunks/app/page-cc108bf68c8a657f.js +1 -0
  167. package/dist/dashboard/out/_next/static/chunks/app/pricing/page-d80e03a5297f95b6.js +1 -0
  168. package/dist/dashboard/out/_next/static/chunks/main-app-5d692157a8eb1fd9.js +1 -0
  169. package/dist/dashboard/out/_next/static/chunks/{main-e0a1f53fe0617a63.js → main-c2f423b9c9f4591b.js} +1 -1
  170. package/dist/dashboard/out/_next/static/chunks/{webpack-c81f7fd28659d64f.js → webpack-a5acc2831d094776.js} +1 -1
  171. package/dist/dashboard/out/_next/static/css/79b80143647a07d7.css +1 -0
  172. package/dist/dashboard/out/_next/static/css/8cf277370ad48cfe.css +1 -0
  173. package/dist/dashboard/out/alt-logos/agent-relay-logo-128.png +0 -0
  174. package/dist/dashboard/out/alt-logos/agent-relay-logo-256.png +0 -0
  175. package/dist/dashboard/out/alt-logos/agent-relay-logo-32.png +0 -0
  176. package/dist/dashboard/out/alt-logos/agent-relay-logo-512.png +0 -0
  177. package/dist/dashboard/out/alt-logos/agent-relay-logo-64.png +0 -0
  178. package/dist/dashboard/out/alt-logos/agent-relay-logo.svg +45 -0
  179. package/dist/dashboard/out/alt-logos/logo.svg +38 -0
  180. package/dist/dashboard/out/alt-logos/monogram-logo-128.png +0 -0
  181. package/dist/dashboard/out/alt-logos/monogram-logo-256.png +0 -0
  182. package/dist/dashboard/out/alt-logos/monogram-logo-32.png +0 -0
  183. package/dist/dashboard/out/alt-logos/monogram-logo-512.png +0 -0
  184. package/dist/dashboard/out/alt-logos/monogram-logo-64.png +0 -0
  185. package/dist/dashboard/out/alt-logos/monogram-logo.svg +38 -0
  186. package/dist/dashboard/out/app.html +14 -0
  187. package/dist/dashboard/out/app.txt +7 -0
  188. package/dist/dashboard/out/history.html +1 -0
  189. package/dist/dashboard/out/history.txt +7 -0
  190. package/dist/dashboard/out/index.html +1 -1
  191. package/dist/dashboard/out/index.txt +2 -2
  192. package/dist/dashboard/out/metrics.html +1 -515
  193. package/dist/dashboard/out/metrics.txt +2 -2
  194. package/dist/dashboard/out/pricing.html +13 -0
  195. package/dist/dashboard/out/pricing.txt +7 -0
  196. package/dist/dashboard-server/metrics.d.ts.map +1 -1
  197. package/dist/dashboard-server/metrics.js +3 -2
  198. package/dist/dashboard-server/metrics.js.map +1 -1
  199. package/dist/dashboard-server/server.d.ts.map +1 -1
  200. package/dist/dashboard-server/server.js +1279 -56
  201. package/dist/dashboard-server/server.js.map +1 -1
  202. package/dist/protocol/types.d.ts +10 -1
  203. package/dist/protocol/types.d.ts.map +1 -1
  204. package/dist/resiliency/context-persistence.d.ts +140 -0
  205. package/dist/resiliency/context-persistence.d.ts.map +1 -0
  206. package/dist/resiliency/context-persistence.js +397 -0
  207. package/dist/resiliency/context-persistence.js.map +1 -0
  208. package/dist/resiliency/health-monitor.d.ts +97 -0
  209. package/dist/resiliency/health-monitor.d.ts.map +1 -0
  210. package/dist/resiliency/health-monitor.js +291 -0
  211. package/dist/resiliency/health-monitor.js.map +1 -0
  212. package/dist/resiliency/index.d.ts +63 -0
  213. package/dist/resiliency/index.d.ts.map +1 -0
  214. package/dist/resiliency/index.js +63 -0
  215. package/dist/resiliency/index.js.map +1 -0
  216. package/dist/resiliency/logger.d.ts +114 -0
  217. package/dist/resiliency/logger.d.ts.map +1 -0
  218. package/dist/resiliency/logger.js +250 -0
  219. package/dist/resiliency/logger.js.map +1 -0
  220. package/dist/resiliency/metrics.d.ts +115 -0
  221. package/dist/resiliency/metrics.d.ts.map +1 -0
  222. package/dist/resiliency/metrics.js +239 -0
  223. package/dist/resiliency/metrics.js.map +1 -0
  224. package/dist/resiliency/provider-context.d.ts +100 -0
  225. package/dist/resiliency/provider-context.d.ts.map +1 -0
  226. package/dist/resiliency/provider-context.js +360 -0
  227. package/dist/resiliency/provider-context.js.map +1 -0
  228. package/dist/resiliency/supervisor.d.ts +109 -0
  229. package/dist/resiliency/supervisor.d.ts.map +1 -0
  230. package/dist/resiliency/supervisor.js +337 -0
  231. package/dist/resiliency/supervisor.js.map +1 -0
  232. package/dist/storage/adapter.d.ts +2 -0
  233. package/dist/storage/adapter.d.ts.map +1 -1
  234. package/dist/storage/adapter.js +12 -2
  235. package/dist/storage/adapter.js.map +1 -1
  236. package/dist/storage/sqlite-adapter.d.ts.map +1 -1
  237. package/dist/storage/sqlite-adapter.js +18 -14
  238. package/dist/storage/sqlite-adapter.js.map +1 -1
  239. package/dist/utils/index.d.ts +1 -0
  240. package/dist/utils/index.d.ts.map +1 -1
  241. package/dist/utils/index.js +1 -0
  242. package/dist/utils/index.js.map +1 -1
  243. package/dist/utils/logger.d.ts +40 -0
  244. package/dist/utils/logger.d.ts.map +1 -0
  245. package/dist/utils/logger.js +84 -0
  246. package/dist/utils/logger.js.map +1 -0
  247. package/dist/wrapper/client.d.ts +16 -1
  248. package/dist/wrapper/client.d.ts.map +1 -1
  249. package/dist/wrapper/client.js +32 -1
  250. package/dist/wrapper/client.js.map +1 -1
  251. package/dist/wrapper/parser.d.ts +3 -0
  252. package/dist/wrapper/parser.d.ts.map +1 -1
  253. package/dist/wrapper/parser.js +121 -18
  254. package/dist/wrapper/parser.js.map +1 -1
  255. package/dist/wrapper/pty-wrapper.d.ts +28 -1
  256. package/dist/wrapper/pty-wrapper.d.ts.map +1 -1
  257. package/dist/wrapper/pty-wrapper.js +166 -30
  258. package/dist/wrapper/pty-wrapper.js.map +1 -1
  259. package/dist/wrapper/tmux-wrapper.d.ts +5 -0
  260. package/dist/wrapper/tmux-wrapper.d.ts.map +1 -1
  261. package/dist/wrapper/tmux-wrapper.js +58 -18
  262. package/dist/wrapper/tmux-wrapper.js.map +1 -1
  263. package/docs/CLOUD-ARCHITECTURE.md +652 -0
  264. package/docs/CLOUD-ONBOARDING-DESIGN.md +1983 -0
  265. package/docs/TESTING_PRESENCE_FEATURES.md +327 -0
  266. package/docs/agent-relay-snippet.md +107 -4
  267. package/docs/guides/CLOUD.md +236 -0
  268. package/docs/guides/LOCAL.md +535 -0
  269. package/docs/guides/SELF-HOSTED.md +494 -0
  270. package/docs/proposals/shadow-as-subagent.md +765 -0
  271. package/docs/proposals/slack-bot-integration.md +1457 -0
  272. package/package.json +33 -4
  273. package/dist/dashboard/out/_next/static/chunks/app/layout-c9d8c5d95e48c6bf.js +0 -1
  274. package/dist/dashboard/out/_next/static/chunks/app/metrics/page-8aa9936bc6c771ab.js +0 -1
  275. package/dist/dashboard/out/_next/static/chunks/app/page-49055e5d2b5e34ec.js +0 -1
  276. package/dist/dashboard/out/_next/static/chunks/main-app-bae2e535de00de50.js +0 -1
  277. package/dist/dashboard/out/_next/static/css/50ed6996e3df7bdd.css +0 -1
  278. /package/dist/dashboard/out/_next/static/{gZXwjIKGDKJ0hiTH-HMeJ → 6HHWb2ZmnJ4OSm0zUP7h4}/_buildManifest.js +0 -0
  279. /package/dist/dashboard/out/_next/static/{gZXwjIKGDKJ0hiTH-HMeJ → 6HHWb2ZmnJ4OSm0zUP7h4}/_ssgManifest.js +0 -0
  280. /package/dist/dashboard/out/_next/static/chunks/{117-3bef7b19f3e60751.js → 117-b2cd8d6485aacf2b.js} +0 -0
  281. /package/dist/dashboard/out/_next/static/chunks/{648-6cf686106c891ad3.js → 648-8f3f26864ce515e5.js} +0 -0
  282. /package/dist/dashboard/out/_next/static/chunks/app/_not-found/{page-8ff6572bc7c9bc61.js → page-0b990dbb71d72a98.js} +0 -0
  283. /package/dist/dashboard/out/_next/static/chunks/{fd9d1056-26bd8d656b496dba.js → fd9d1056-bf46c09eb57e019c.js} +0 -0
@@ -0,0 +1,736 @@
1
+ /**
2
+ * Daemon Orchestrator
3
+ *
4
+ * Manages multiple workspace daemons and provides a unified API for the dashboard.
5
+ * This is the top-level service that runs by default, handling workspace switching
6
+ * and agent management across all connected repositories.
7
+ */
8
+ import * as http from 'http';
9
+ import * as path from 'path';
10
+ import * as fs from 'fs';
11
+ import { EventEmitter } from 'events';
12
+ import { WebSocketServer, WebSocket } from 'ws';
13
+ import { createLogger } from '../resiliency/logger.js';
14
+ import { metrics } from '../resiliency/metrics.js';
15
+ import { getSupervisor } from '../resiliency/supervisor.js';
16
+ import { Daemon } from './server.js';
17
+ import { AgentSpawner } from '../bridge/spawner.js';
18
+ import { getProjectPaths } from '../utils/project-namespace.js';
19
+ const logger = createLogger('orchestrator');
20
+ function generateId() {
21
+ return Math.random().toString(36).substring(2, 15);
22
+ }
23
+ const DEFAULT_CONFIG = {
24
+ port: 3456,
25
+ host: 'localhost',
26
+ dataDir: path.join(process.env.HOME || '', '.agent-relay', 'orchestrator'),
27
+ autoStartDaemons: true,
28
+ };
29
+ export class Orchestrator extends EventEmitter {
30
+ config;
31
+ workspaces = new Map();
32
+ activeWorkspaceId;
33
+ server;
34
+ wss;
35
+ sessions = new Map();
36
+ supervisor = getSupervisor({
37
+ autoRestart: true,
38
+ maxRestarts: 5,
39
+ contextPersistence: { enabled: true, autoInjectOnRestart: true },
40
+ });
41
+ workspacesFile;
42
+ constructor(config = {}) {
43
+ super();
44
+ this.config = { ...DEFAULT_CONFIG, ...config };
45
+ this.workspacesFile = path.join(this.config.dataDir, 'workspaces.json');
46
+ // Ensure data directory exists
47
+ if (!fs.existsSync(this.config.dataDir)) {
48
+ fs.mkdirSync(this.config.dataDir, { recursive: true });
49
+ }
50
+ // Load existing workspaces
51
+ this.loadWorkspaces();
52
+ }
53
+ /**
54
+ * Start the orchestrator
55
+ */
56
+ async start() {
57
+ logger.info('Starting orchestrator', {
58
+ port: this.config.port,
59
+ host: this.config.host,
60
+ });
61
+ // Start supervisor
62
+ this.supervisor.start();
63
+ // Auto-start daemons for workspaces
64
+ if (this.config.autoStartDaemons) {
65
+ for (const [id, workspace] of this.workspaces) {
66
+ if (fs.existsSync(workspace.path)) {
67
+ await this.startWorkspaceDaemon(id);
68
+ }
69
+ }
70
+ }
71
+ // Start HTTP server
72
+ this.server = http.createServer((req, res) => this.handleRequest(req, res));
73
+ // Setup WebSocket
74
+ this.wss = new WebSocketServer({ server: this.server });
75
+ this.wss.on('connection', (ws, req) => this.handleWebSocket(ws, req));
76
+ return new Promise((resolve) => {
77
+ this.server.listen(this.config.port, this.config.host, () => {
78
+ logger.info('Orchestrator started', {
79
+ url: `http://${this.config.host}:${this.config.port}`,
80
+ });
81
+ resolve();
82
+ });
83
+ });
84
+ }
85
+ /**
86
+ * Stop the orchestrator
87
+ */
88
+ async stop() {
89
+ logger.info('Stopping orchestrator');
90
+ // Stop all workspace daemons
91
+ for (const [id] of this.workspaces) {
92
+ await this.stopWorkspaceDaemon(id);
93
+ }
94
+ // Stop supervisor
95
+ this.supervisor.stop();
96
+ // Close WebSocket connections
97
+ if (this.wss) {
98
+ for (const ws of this.wss.clients) {
99
+ ws.close();
100
+ }
101
+ this.wss.close();
102
+ }
103
+ // Close HTTP server
104
+ if (this.server) {
105
+ return new Promise((resolve) => {
106
+ this.server.close(() => {
107
+ logger.info('Orchestrator stopped');
108
+ resolve();
109
+ });
110
+ });
111
+ }
112
+ }
113
+ // === Workspace Management ===
114
+ /**
115
+ * Add a workspace
116
+ */
117
+ addWorkspace(request) {
118
+ const resolvedPath = this.resolvePath(request.path);
119
+ // Check if already exists
120
+ const existing = this.findWorkspaceByPath(resolvedPath);
121
+ if (existing) {
122
+ return existing;
123
+ }
124
+ // Validate path exists
125
+ if (!fs.existsSync(resolvedPath)) {
126
+ throw new Error(`Path does not exist: ${resolvedPath}`);
127
+ }
128
+ const workspace = {
129
+ id: generateId(),
130
+ name: request.name || path.basename(resolvedPath),
131
+ path: resolvedPath,
132
+ status: 'inactive',
133
+ provider: request.provider || this.detectProvider(resolvedPath),
134
+ createdAt: new Date(),
135
+ lastActiveAt: new Date(),
136
+ ...this.getGitInfo(resolvedPath),
137
+ };
138
+ this.workspaces.set(workspace.id, workspace);
139
+ this.saveWorkspaces();
140
+ logger.info('Workspace added', { id: workspace.id, name: workspace.name });
141
+ this.broadcastEvent({
142
+ type: 'workspace:added',
143
+ workspaceId: workspace.id,
144
+ data: this.toPublicWorkspace(workspace),
145
+ timestamp: new Date(),
146
+ });
147
+ // Auto-start daemon
148
+ if (this.config.autoStartDaemons) {
149
+ this.startWorkspaceDaemon(workspace.id).catch((err) => {
150
+ logger.error('Failed to start workspace daemon', { id: workspace.id, error: String(err) });
151
+ });
152
+ }
153
+ return this.toPublicWorkspace(workspace);
154
+ }
155
+ /**
156
+ * Remove a workspace
157
+ */
158
+ async removeWorkspace(workspaceId) {
159
+ const workspace = this.workspaces.get(workspaceId);
160
+ if (!workspace)
161
+ return false;
162
+ // Stop daemon if running
163
+ await this.stopWorkspaceDaemon(workspaceId);
164
+ // Clear active if this was active
165
+ if (this.activeWorkspaceId === workspaceId) {
166
+ this.activeWorkspaceId = undefined;
167
+ }
168
+ this.workspaces.delete(workspaceId);
169
+ this.saveWorkspaces();
170
+ logger.info('Workspace removed', { id: workspaceId });
171
+ this.broadcastEvent({
172
+ type: 'workspace:removed',
173
+ workspaceId,
174
+ data: { id: workspaceId },
175
+ timestamp: new Date(),
176
+ });
177
+ return true;
178
+ }
179
+ /**
180
+ * Switch to a workspace
181
+ */
182
+ async switchWorkspace(workspaceId) {
183
+ const workspace = this.workspaces.get(workspaceId);
184
+ if (!workspace) {
185
+ throw new Error(`Workspace not found: ${workspaceId}`);
186
+ }
187
+ const previousId = this.activeWorkspaceId;
188
+ // Update status
189
+ if (previousId && previousId !== workspaceId) {
190
+ const prev = this.workspaces.get(previousId);
191
+ if (prev) {
192
+ prev.status = 'inactive';
193
+ }
194
+ }
195
+ workspace.status = 'active';
196
+ workspace.lastActiveAt = new Date();
197
+ this.activeWorkspaceId = workspaceId;
198
+ // Ensure daemon is running
199
+ if (!workspace.daemon?.isRunning) {
200
+ await this.startWorkspaceDaemon(workspaceId);
201
+ }
202
+ this.saveWorkspaces();
203
+ logger.info('Switched workspace', { id: workspaceId, name: workspace.name });
204
+ this.broadcastEvent({
205
+ type: 'workspace:switched',
206
+ workspaceId,
207
+ data: { previousId, currentId: workspaceId },
208
+ timestamp: new Date(),
209
+ });
210
+ return this.toPublicWorkspace(workspace);
211
+ }
212
+ /**
213
+ * Get all workspaces
214
+ */
215
+ getWorkspaces() {
216
+ return Array.from(this.workspaces.values()).map((w) => this.toPublicWorkspace(w));
217
+ }
218
+ /**
219
+ * Get workspace by ID
220
+ */
221
+ getWorkspace(workspaceId) {
222
+ const workspace = this.workspaces.get(workspaceId);
223
+ return workspace ? this.toPublicWorkspace(workspace) : undefined;
224
+ }
225
+ /**
226
+ * Get active workspace
227
+ */
228
+ getActiveWorkspace() {
229
+ if (!this.activeWorkspaceId)
230
+ return undefined;
231
+ return this.getWorkspace(this.activeWorkspaceId);
232
+ }
233
+ // === Agent Management ===
234
+ /**
235
+ * Spawn an agent in a workspace
236
+ */
237
+ async spawnAgent(workspaceId, request) {
238
+ const workspace = this.workspaces.get(workspaceId);
239
+ if (!workspace) {
240
+ throw new Error(`Workspace not found: ${workspaceId}`);
241
+ }
242
+ // Ensure daemon is running
243
+ if (!workspace.daemon?.isRunning) {
244
+ await this.startWorkspaceDaemon(workspaceId);
245
+ }
246
+ // Ensure spawner exists
247
+ if (!workspace.spawner) {
248
+ workspace.spawner = new AgentSpawner(workspace.path);
249
+ }
250
+ const result = await workspace.spawner.spawn({
251
+ name: request.name,
252
+ cli: this.getCliForProvider(request.provider || workspace.provider),
253
+ task: request.task || '',
254
+ });
255
+ if (!result.success) {
256
+ throw new Error(result.error || 'Failed to spawn agent');
257
+ }
258
+ const agent = {
259
+ id: generateId(),
260
+ name: request.name,
261
+ workspaceId,
262
+ provider: request.provider || workspace.provider,
263
+ status: 'running',
264
+ pid: result.pid,
265
+ task: request.task,
266
+ spawnedAt: new Date(),
267
+ restartCount: 0,
268
+ };
269
+ logger.info('Agent spawned', { id: agent.id, name: agent.name, workspaceId });
270
+ this.broadcastEvent({
271
+ type: 'agent:spawned',
272
+ workspaceId,
273
+ agentId: agent.id,
274
+ data: agent,
275
+ timestamp: new Date(),
276
+ });
277
+ return agent;
278
+ }
279
+ /**
280
+ * Stop an agent
281
+ */
282
+ async stopAgent(workspaceId, agentName) {
283
+ const workspace = this.workspaces.get(workspaceId);
284
+ if (!workspace?.spawner)
285
+ return false;
286
+ const released = await workspace.spawner.release(agentName);
287
+ if (released) {
288
+ this.broadcastEvent({
289
+ type: 'agent:stopped',
290
+ workspaceId,
291
+ data: { name: agentName },
292
+ timestamp: new Date(),
293
+ });
294
+ }
295
+ return released;
296
+ }
297
+ /**
298
+ * Get agents in a workspace
299
+ */
300
+ getAgents(workspaceId) {
301
+ const workspace = this.workspaces.get(workspaceId);
302
+ if (!workspace?.spawner)
303
+ return [];
304
+ return workspace.spawner.getActiveWorkers().map((w) => ({
305
+ id: w.name,
306
+ name: w.name,
307
+ workspaceId,
308
+ provider: this.detectProviderFromCli(w.cli),
309
+ status: 'running',
310
+ pid: w.pid,
311
+ task: w.task,
312
+ spawnedAt: new Date(w.spawnedAt),
313
+ restartCount: 0,
314
+ }));
315
+ }
316
+ // === Private Methods ===
317
+ /**
318
+ * Start daemon for a workspace
319
+ */
320
+ async startWorkspaceDaemon(workspaceId) {
321
+ const workspace = this.workspaces.get(workspaceId);
322
+ if (!workspace)
323
+ return;
324
+ if (workspace.daemon?.isRunning)
325
+ return;
326
+ try {
327
+ const paths = getProjectPaths(workspace.path);
328
+ workspace.daemon = new Daemon({
329
+ socketPath: paths.socketPath,
330
+ teamDir: paths.teamDir,
331
+ });
332
+ await workspace.daemon.start();
333
+ workspace.status = 'active';
334
+ // Create spawner
335
+ workspace.spawner = new AgentSpawner(workspace.path);
336
+ logger.info('Workspace daemon started', { id: workspaceId, socket: paths.socketPath });
337
+ }
338
+ catch (err) {
339
+ workspace.status = 'error';
340
+ logger.error('Failed to start workspace daemon', { id: workspaceId, error: String(err) });
341
+ throw err;
342
+ }
343
+ }
344
+ /**
345
+ * Stop daemon for a workspace
346
+ */
347
+ async stopWorkspaceDaemon(workspaceId) {
348
+ const workspace = this.workspaces.get(workspaceId);
349
+ if (!workspace)
350
+ return;
351
+ // Release all agents first
352
+ if (workspace.spawner) {
353
+ await workspace.spawner.releaseAll();
354
+ }
355
+ // Stop daemon
356
+ if (workspace.daemon) {
357
+ await workspace.daemon.stop();
358
+ workspace.daemon = undefined;
359
+ }
360
+ workspace.spawner = undefined;
361
+ workspace.status = 'inactive';
362
+ logger.info('Workspace daemon stopped', { id: workspaceId });
363
+ }
364
+ /**
365
+ * Handle HTTP request
366
+ */
367
+ async handleRequest(req, res) {
368
+ // CORS
369
+ res.setHeader('Access-Control-Allow-Origin', '*');
370
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
371
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
372
+ if (req.method === 'OPTIONS') {
373
+ res.writeHead(204);
374
+ res.end();
375
+ return;
376
+ }
377
+ const url = new URL(req.url || '/', `http://${req.headers.host}`);
378
+ const pathname = url.pathname;
379
+ const method = req.method || 'GET';
380
+ try {
381
+ let response;
382
+ // Health check
383
+ if (pathname === '/' && method === 'GET') {
384
+ response = { status: 200, body: { status: 'ok', version: '1.0.0' } };
385
+ }
386
+ // Metrics
387
+ else if (pathname === '/metrics' && method === 'GET') {
388
+ res.setHeader('Content-Type', 'text/plain');
389
+ res.writeHead(200);
390
+ res.end(metrics.toPrometheus());
391
+ return;
392
+ }
393
+ // List workspaces
394
+ else if (pathname === '/workspaces' && method === 'GET') {
395
+ response = {
396
+ status: 200,
397
+ body: {
398
+ workspaces: this.getWorkspaces(),
399
+ activeWorkspaceId: this.activeWorkspaceId,
400
+ },
401
+ };
402
+ }
403
+ // Add workspace
404
+ else if (pathname === '/workspaces' && method === 'POST') {
405
+ const body = await this.parseBody(req);
406
+ const workspace = this.addWorkspace(body);
407
+ response = { status: 201, body: workspace };
408
+ }
409
+ // Get workspace
410
+ else if (pathname.match(/^\/workspaces\/[^/]+$/) && method === 'GET') {
411
+ const id = pathname.split('/')[2];
412
+ const workspace = this.getWorkspace(id);
413
+ response = workspace
414
+ ? { status: 200, body: workspace }
415
+ : { status: 404, body: { error: 'Not found' } };
416
+ }
417
+ // Delete workspace
418
+ else if (pathname.match(/^\/workspaces\/[^/]+$/) && method === 'DELETE') {
419
+ const id = pathname.split('/')[2];
420
+ const removed = await this.removeWorkspace(id);
421
+ response = removed
422
+ ? { status: 204, body: null }
423
+ : { status: 404, body: { error: 'Not found' } };
424
+ }
425
+ // Switch workspace
426
+ else if (pathname.match(/^\/workspaces\/[^/]+\/switch$/) && method === 'POST') {
427
+ const id = pathname.split('/')[2];
428
+ const workspace = await this.switchWorkspace(id);
429
+ response = { status: 200, body: workspace };
430
+ }
431
+ // List agents in workspace
432
+ else if (pathname.match(/^\/workspaces\/[^/]+\/agents$/) && method === 'GET') {
433
+ const id = pathname.split('/')[2];
434
+ const agents = this.getAgents(id);
435
+ response = { status: 200, body: { agents, workspaceId: id } };
436
+ }
437
+ // Spawn agent
438
+ else if (pathname.match(/^\/workspaces\/[^/]+\/agents$/) && method === 'POST') {
439
+ const id = pathname.split('/')[2];
440
+ const body = await this.parseBody(req);
441
+ const agent = await this.spawnAgent(id, body);
442
+ response = { status: 201, body: agent };
443
+ }
444
+ // Stop agent
445
+ else if (pathname.match(/^\/workspaces\/[^/]+\/agents\/[^/]+$/) && method === 'DELETE') {
446
+ const parts = pathname.split('/');
447
+ const workspaceId = parts[2];
448
+ const agentName = parts[4];
449
+ const stopped = await this.stopAgent(workspaceId, agentName);
450
+ response = stopped
451
+ ? { status: 204, body: null }
452
+ : { status: 404, body: { error: 'Not found' } };
453
+ }
454
+ // Not found
455
+ else {
456
+ response = { status: 404, body: { error: 'Not found' } };
457
+ }
458
+ res.setHeader('Content-Type', 'application/json');
459
+ res.writeHead(response.status);
460
+ res.end(response.body ? JSON.stringify(response.body) : '');
461
+ }
462
+ catch (err) {
463
+ logger.error('Request error', { error: String(err) });
464
+ res.setHeader('Content-Type', 'application/json');
465
+ res.writeHead(500);
466
+ res.end(JSON.stringify({ error: String(err) }));
467
+ }
468
+ }
469
+ /**
470
+ * Handle WebSocket connection
471
+ */
472
+ handleWebSocket(ws, _req) {
473
+ logger.info('WebSocket client connected');
474
+ const session = {
475
+ userId: 'anonymous',
476
+ githubUsername: 'anonymous',
477
+ connectedAt: new Date(),
478
+ activeWorkspaceId: this.activeWorkspaceId,
479
+ };
480
+ this.sessions.set(ws, session);
481
+ // Send initial state
482
+ this.sendToClient(ws, {
483
+ type: 'init',
484
+ data: {
485
+ workspaces: this.getWorkspaces(),
486
+ activeWorkspaceId: this.activeWorkspaceId,
487
+ agents: this.activeWorkspaceId ? this.getAgents(this.activeWorkspaceId) : [],
488
+ },
489
+ });
490
+ ws.on('message', (data) => {
491
+ try {
492
+ const msg = JSON.parse(data.toString());
493
+ this.handleWebSocketMessage(ws, session, msg);
494
+ }
495
+ catch (err) {
496
+ logger.error('WebSocket message error', { error: String(err) });
497
+ }
498
+ });
499
+ ws.on('close', () => {
500
+ this.sessions.delete(ws);
501
+ logger.info('WebSocket client disconnected');
502
+ });
503
+ }
504
+ /**
505
+ * Handle WebSocket message
506
+ */
507
+ handleWebSocketMessage(ws, session, msg) {
508
+ switch (msg.type) {
509
+ case 'switch_workspace':
510
+ if (typeof msg.data === 'string') {
511
+ this.switchWorkspace(msg.data)
512
+ .then((workspace) => {
513
+ session.activeWorkspaceId = workspace.id;
514
+ })
515
+ .catch((err) => {
516
+ this.sendToClient(ws, { type: 'error', data: String(err) });
517
+ });
518
+ }
519
+ break;
520
+ case 'ping':
521
+ this.sendToClient(ws, { type: 'pong' });
522
+ break;
523
+ }
524
+ }
525
+ /**
526
+ * Send to WebSocket client
527
+ */
528
+ sendToClient(ws, msg) {
529
+ if (ws.readyState === WebSocket.OPEN) {
530
+ ws.send(JSON.stringify(msg));
531
+ }
532
+ }
533
+ /**
534
+ * Broadcast event to all clients
535
+ */
536
+ broadcastEvent(event) {
537
+ if (!this.wss)
538
+ return;
539
+ const msg = JSON.stringify({ type: 'event', data: event });
540
+ for (const ws of this.wss.clients) {
541
+ if (ws.readyState === WebSocket.OPEN) {
542
+ ws.send(msg);
543
+ }
544
+ }
545
+ }
546
+ /**
547
+ * Parse request body
548
+ */
549
+ parseBody(req) {
550
+ return new Promise((resolve, reject) => {
551
+ let data = '';
552
+ req.on('data', (chunk) => (data += chunk));
553
+ req.on('end', () => {
554
+ try {
555
+ resolve(data ? JSON.parse(data) : {});
556
+ }
557
+ catch {
558
+ reject(new Error('Invalid JSON'));
559
+ }
560
+ });
561
+ });
562
+ }
563
+ /**
564
+ * Load workspaces from disk
565
+ */
566
+ loadWorkspaces() {
567
+ if (!fs.existsSync(this.workspacesFile))
568
+ return;
569
+ try {
570
+ const data = JSON.parse(fs.readFileSync(this.workspacesFile, 'utf8'));
571
+ for (const w of data.workspaces || []) {
572
+ this.workspaces.set(w.id, {
573
+ ...w,
574
+ createdAt: new Date(w.createdAt),
575
+ lastActiveAt: new Date(w.lastActiveAt),
576
+ status: 'inactive',
577
+ });
578
+ }
579
+ this.activeWorkspaceId = data.activeWorkspaceId;
580
+ logger.info('Loaded workspaces', { count: this.workspaces.size });
581
+ }
582
+ catch (err) {
583
+ logger.error('Failed to load workspaces', { error: String(err) });
584
+ }
585
+ }
586
+ /**
587
+ * Save workspaces to disk
588
+ */
589
+ saveWorkspaces() {
590
+ try {
591
+ const data = {
592
+ workspaces: Array.from(this.workspaces.values()).map((w) => this.toPublicWorkspace(w)),
593
+ activeWorkspaceId: this.activeWorkspaceId,
594
+ };
595
+ fs.writeFileSync(this.workspacesFile, JSON.stringify(data, null, 2));
596
+ }
597
+ catch (err) {
598
+ logger.error('Failed to save workspaces', { error: String(err) });
599
+ }
600
+ }
601
+ /**
602
+ * Find workspace by path
603
+ */
604
+ findWorkspaceByPath(path) {
605
+ const resolved = this.resolvePath(path);
606
+ const workspace = Array.from(this.workspaces.values()).find((w) => w.path === resolved);
607
+ return workspace ? this.toPublicWorkspace(workspace) : undefined;
608
+ }
609
+ /**
610
+ * Resolve path
611
+ */
612
+ resolvePath(p) {
613
+ if (p.startsWith('~')) {
614
+ p = path.join(process.env.HOME || '', p.slice(1));
615
+ }
616
+ return path.resolve(p);
617
+ }
618
+ /**
619
+ * Detect provider from workspace
620
+ */
621
+ detectProvider(workspacePath) {
622
+ if (fs.existsSync(path.join(workspacePath, 'CLAUDE.md')) ||
623
+ fs.existsSync(path.join(workspacePath, '.claude'))) {
624
+ return 'claude';
625
+ }
626
+ if (fs.existsSync(path.join(workspacePath, '.codex'))) {
627
+ return 'codex';
628
+ }
629
+ if (fs.existsSync(path.join(workspacePath, '.gemini'))) {
630
+ return 'gemini';
631
+ }
632
+ return 'generic';
633
+ }
634
+ /**
635
+ * Detect provider from CLI command
636
+ */
637
+ detectProviderFromCli(cli) {
638
+ if (cli.includes('claude'))
639
+ return 'claude';
640
+ if (cli.includes('codex'))
641
+ return 'codex';
642
+ if (cli.includes('gemini'))
643
+ return 'gemini';
644
+ return 'generic';
645
+ }
646
+ /**
647
+ * Get CLI command for provider
648
+ */
649
+ getCliForProvider(provider) {
650
+ switch (provider) {
651
+ case 'claude':
652
+ return 'claude';
653
+ case 'codex':
654
+ return 'codex';
655
+ case 'gemini':
656
+ return 'gemini';
657
+ default:
658
+ return 'claude';
659
+ }
660
+ }
661
+ /**
662
+ * Get git info
663
+ */
664
+ getGitInfo(workspacePath) {
665
+ try {
666
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
667
+ const { execSync } = require('child_process');
668
+ const branch = execSync('git branch --show-current', {
669
+ cwd: workspacePath,
670
+ encoding: 'utf8',
671
+ stdio: ['pipe', 'pipe', 'pipe'],
672
+ }).trim();
673
+ let remote;
674
+ try {
675
+ remote = execSync('git remote get-url origin', {
676
+ cwd: workspacePath,
677
+ encoding: 'utf8',
678
+ stdio: ['pipe', 'pipe', 'pipe'],
679
+ }).trim();
680
+ }
681
+ catch {
682
+ // No remote
683
+ }
684
+ return { gitRemote: remote, gitBranch: branch };
685
+ }
686
+ catch {
687
+ return {};
688
+ }
689
+ }
690
+ /**
691
+ * Convert to public workspace (without internal references)
692
+ */
693
+ toPublicWorkspace(w) {
694
+ return {
695
+ id: w.id,
696
+ name: w.name,
697
+ path: w.path,
698
+ status: w.status,
699
+ provider: w.provider,
700
+ createdAt: w.createdAt,
701
+ lastActiveAt: w.lastActiveAt,
702
+ cloudId: w.cloudId,
703
+ customDomain: w.customDomain,
704
+ gitRemote: w.gitRemote,
705
+ gitBranch: w.gitBranch,
706
+ };
707
+ }
708
+ }
709
+ let orchestratorInstance;
710
+ /**
711
+ * Start the orchestrator
712
+ */
713
+ export async function startOrchestrator(config = {}) {
714
+ if (orchestratorInstance) {
715
+ return orchestratorInstance;
716
+ }
717
+ orchestratorInstance = new Orchestrator(config);
718
+ await orchestratorInstance.start();
719
+ return orchestratorInstance;
720
+ }
721
+ /**
722
+ * Stop the orchestrator
723
+ */
724
+ export async function stopOrchestrator() {
725
+ if (orchestratorInstance) {
726
+ await orchestratorInstance.stop();
727
+ orchestratorInstance = undefined;
728
+ }
729
+ }
730
+ /**
731
+ * Get orchestrator instance
732
+ */
733
+ export function getOrchestrator() {
734
+ return orchestratorInstance;
735
+ }
736
+ //# sourceMappingURL=orchestrator.js.map