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
package/dist/cli/index.js CHANGED
@@ -17,6 +17,8 @@ import { RelayClient } from '../wrapper/client.js';
17
17
  import { generateAgentName } from '../utils/name-generator.js';
18
18
  import { getTmuxPath } from '../utils/tmux-resolver.js';
19
19
  import { readWorkersMetadata, getWorkerLogsDir } from '../bridge/spawner.js';
20
+ import { getShadowForAgent } from '../bridge/shadow-config.js';
21
+ import { selectShadowCli } from '../bridge/shadow-cli.js';
20
22
  import { checkForUpdatesInBackground, checkForUpdates } from '../utils/update-checker.js';
21
23
  import fs from 'node:fs';
22
24
  import path from 'node:path';
@@ -53,6 +55,8 @@ program
53
55
  .option('-n, --name <name>', 'Agent name (auto-generated if not set)')
54
56
  .option('-q, --quiet', 'Disable debug output', false)
55
57
  .option('--prefix <pattern>', 'Relay prefix pattern (default: ->relay:)')
58
+ .option('--shadow <name>', 'Spawn a shadow agent with this name that monitors the primary')
59
+ .option('--shadow-role <role>', 'Shadow role: reviewer, auditor, or triggers (comma-separated: SESSION_END,CODE_WRITTEN,REVIEW_REQUEST,EXPLICIT_ASK,ALL_MESSAGES)')
56
60
  .argument('[command...]', 'Command to wrap (e.g., claude)')
57
61
  .action(async (commandParts, options) => {
58
62
  // If no command provided, show help
@@ -125,6 +129,76 @@ program
125
129
  process.exit(0);
126
130
  });
127
131
  await wrapper.start();
132
+ let shadowName;
133
+ let shadowRole;
134
+ let speakOn;
135
+ let shadowCli;
136
+ let shadowPrompt;
137
+ const rolePresets = {
138
+ reviewer: ['CODE_WRITTEN', 'REVIEW_REQUEST', 'EXPLICIT_ASK'],
139
+ auditor: ['SESSION_END', 'EXPLICIT_ASK'],
140
+ active: ['ALL_MESSAGES'],
141
+ };
142
+ if (options.shadow) {
143
+ // CLI flags provided
144
+ shadowName = options.shadow;
145
+ const role = options.shadowRole || 'EXPLICIT_ASK';
146
+ shadowRole = role;
147
+ if (rolePresets[role.toLowerCase()]) {
148
+ speakOn = rolePresets[role.toLowerCase()];
149
+ }
150
+ else {
151
+ speakOn = role.split(',').map((s) => s.trim().toUpperCase());
152
+ }
153
+ }
154
+ else {
155
+ // Check config file for shadow configuration
156
+ const shadowConfig = getShadowForAgent(paths.projectRoot, agentName);
157
+ if (shadowConfig) {
158
+ shadowName = shadowConfig.shadowName;
159
+ shadowRole = shadowConfig.roleName;
160
+ speakOn = shadowConfig.speakOn;
161
+ shadowCli = shadowConfig.cli;
162
+ shadowPrompt = shadowConfig.prompt;
163
+ console.error(`Shadow config: ${shadowName} (from .agent-relay.json)`);
164
+ }
165
+ }
166
+ // Spawn shadow if configured
167
+ if (shadowName && speakOn) {
168
+ // Decide how to run the shadow (subagent for Claude/OpenCode primaries)
169
+ let shadowSelection = null;
170
+ try {
171
+ shadowSelection = await selectShadowCli(mainCommand, { preferredShadowCli: shadowCli });
172
+ console.error(`[shadow] Mode: ${shadowSelection.mode} via ${shadowSelection.command || shadowSelection.cli} (primary: ${mainCommand})`);
173
+ }
174
+ catch (err) {
175
+ console.error(`[shadow] Shadow CLI selection failed: ${err.message}`);
176
+ }
177
+ // Subagent mode: do not spawn a separate shadow process
178
+ if (shadowSelection?.mode === 'subagent') {
179
+ console.error(`[shadow] ${shadowName} will run as ${shadowSelection.cli} subagent inside ${agentName}; no separate process spawned`);
180
+ return;
181
+ }
182
+ console.error(`Shadow: ${shadowName} (shadowing ${agentName}, speakOn: ${speakOn.join(',')})`);
183
+ // Wait for primary to register before spawning shadow
184
+ await new Promise(r => setTimeout(r, 3000));
185
+ // Build shadow task prompt
186
+ const defaultPrompt = `You are a shadow agent monitoring "${agentName}". You receive copies of their messages. Your role: ${shadowRole || 'observer'}. Stay passive unless your triggers activate.`;
187
+ const shadowTask = shadowPrompt || defaultPrompt;
188
+ const result = await spawner.spawn({
189
+ name: shadowName,
190
+ cli: shadowSelection?.command || shadowCli || mainCommand,
191
+ task: shadowTask,
192
+ shadowOf: agentName,
193
+ shadowSpeakOn: speakOn,
194
+ });
195
+ if (result.success) {
196
+ console.error(`Shadow ${shadowName} started [pid: ${result.pid}]`);
197
+ }
198
+ else {
199
+ console.error(`Failed to spawn shadow ${shadowName}: ${result.error}`);
200
+ }
201
+ }
128
202
  });
129
203
  // up - Start daemon + dashboard
130
204
  program
@@ -134,7 +208,67 @@ program
134
208
  .option('--port <port>', 'Dashboard port', DEFAULT_DASHBOARD_PORT)
135
209
  .option('--spawn', 'Force spawn all agents from teams.json')
136
210
  .option('--no-spawn', 'Do not auto-spawn agents (just start daemon)')
211
+ .option('--watch', 'Auto-restart daemon on crash (supervisor mode)')
212
+ .option('--max-restarts <n>', 'Max restarts in 60s before giving up (default: 5)', '5')
137
213
  .action(async (options) => {
214
+ // If --watch is specified, run in supervisor mode
215
+ if (options.watch) {
216
+ const { spawn } = await import('node:child_process');
217
+ const maxRestarts = parseInt(options.maxRestarts, 10) || 5;
218
+ const restartWindow = 60_000; // 60 seconds
219
+ const restartTimes = [];
220
+ let child = null;
221
+ let shuttingDown = false;
222
+ const startDaemon = () => {
223
+ // Build args without --watch to prevent infinite recursion
224
+ const args = ['up'];
225
+ if (options.dashboard === false)
226
+ args.push('--no-dashboard');
227
+ if (options.port)
228
+ args.push('--port', options.port);
229
+ if (options.spawn === true)
230
+ args.push('--spawn');
231
+ if (options.spawn === false)
232
+ args.push('--no-spawn');
233
+ console.log(`[supervisor] Starting daemon...`);
234
+ child = spawn(process.execPath, [process.argv[1], ...args], {
235
+ stdio: 'inherit',
236
+ env: { ...process.env, AGENT_RELAY_SUPERVISED: '1' },
237
+ });
238
+ child.on('exit', (code, signal) => {
239
+ if (shuttingDown) {
240
+ process.exit(0);
241
+ return;
242
+ }
243
+ const now = Date.now();
244
+ restartTimes.push(now);
245
+ // Remove restarts outside the window
246
+ while (restartTimes.length > 0 && restartTimes[0] < now - restartWindow) {
247
+ restartTimes.shift();
248
+ }
249
+ if (restartTimes.length >= maxRestarts) {
250
+ console.error(`[supervisor] Daemon crashed ${maxRestarts} times in ${restartWindow / 1000}s, giving up`);
251
+ process.exit(1);
252
+ }
253
+ const exitReason = signal ? `signal ${signal}` : `code ${code}`;
254
+ console.log(`[supervisor] Daemon exited (${exitReason}), restarting in 2s... (${restartTimes.length}/${maxRestarts} restarts)`);
255
+ setTimeout(startDaemon, 2000);
256
+ });
257
+ };
258
+ process.on('SIGINT', () => {
259
+ console.log('\n[supervisor] Stopping...');
260
+ shuttingDown = true;
261
+ if (child)
262
+ child.kill('SIGINT');
263
+ });
264
+ process.on('SIGTERM', () => {
265
+ shuttingDown = true;
266
+ if (child)
267
+ child.kill('SIGTERM');
268
+ });
269
+ startDaemon();
270
+ return;
271
+ }
138
272
  const { ensureProjectDir } = await import('../utils/project-namespace.js');
139
273
  const { loadTeamsConfig } = await import('../bridge/teams-config.js');
140
274
  const { AgentSpawner } = await import('../bridge/spawner.js');
@@ -157,6 +291,34 @@ program
157
291
  });
158
292
  // Create spawner for auto-spawn (will be initialized after dashboard starts)
159
293
  let spawner = null;
294
+ // Track if we're already shutting down to prevent double-cleanup
295
+ let isShuttingDown = false;
296
+ const gracefulShutdown = async (reason) => {
297
+ if (isShuttingDown)
298
+ return;
299
+ isShuttingDown = true;
300
+ console.log(`\n[daemon] ${reason}, shutting down...`);
301
+ try {
302
+ if (spawner)
303
+ await spawner.releaseAll();
304
+ await daemon.stop();
305
+ }
306
+ catch (err) {
307
+ console.error('[daemon] Error during shutdown:', err);
308
+ }
309
+ process.exit(1);
310
+ };
311
+ // Handle uncaught exceptions - log and exit (supervisor will restart)
312
+ process.on('uncaughtException', (err) => {
313
+ console.error('[daemon] Uncaught exception:', err);
314
+ gracefulShutdown('Uncaught exception');
315
+ });
316
+ // Handle unhandled promise rejections
317
+ process.on('unhandledRejection', (reason, promise) => {
318
+ console.error('[daemon] Unhandled rejection at:', promise, 'reason:', reason);
319
+ // Don't exit on unhandled rejections - just log them
320
+ // Most are recoverable (e.g., failed message delivery)
321
+ });
160
322
  process.on('SIGINT', async () => {
161
323
  console.log('\nStopping...');
162
324
  if (spawner) {
@@ -189,6 +351,13 @@ program
189
351
  projectRoot: paths.projectRoot,
190
352
  });
191
353
  console.log(`Dashboard: http://localhost:${dashboardPort}`);
354
+ // Hook daemon log output to dashboard WebSocket
355
+ daemon.onLogOutput = (agentName, data, _timestamp) => {
356
+ const broadcast = global.__broadcastLogOutput;
357
+ if (broadcast) {
358
+ broadcast(agentName, data);
359
+ }
360
+ };
192
361
  }
193
362
  // Determine if we should auto-spawn agents
194
363
  // --spawn: force spawn
@@ -288,9 +457,11 @@ program
288
457
  .command('agents')
289
458
  .description('List connected agents and spawned workers')
290
459
  .option('--all', 'Include internal/CLI agents')
460
+ .option('--remote', 'Include agents from other linked machines (requires cloud link)')
291
461
  .option('--json', 'Output as JSON')
292
462
  .action(async (options) => {
293
463
  const { getProjectPaths } = await import('../utils/project-namespace.js');
464
+ const os = await import('node:os');
294
465
  const paths = getProjectPaths();
295
466
  const agentsPath = path.join(paths.teamDir, 'agents.json');
296
467
  // Load registered agents
@@ -311,6 +482,7 @@ program
311
482
  lastSeen: agent.lastSeen,
312
483
  team: worker?.team,
313
484
  pid: worker?.pid,
485
+ location: 'local',
314
486
  });
315
487
  });
316
488
  // Add workers not in registry (orphaned or not yet registered)
@@ -323,9 +495,51 @@ program
323
495
  cli: worker.cli || '-',
324
496
  team: worker.team,
325
497
  pid: worker.pid,
498
+ location: 'local',
326
499
  });
327
500
  }
328
501
  });
502
+ // Include remote agents if --remote flag is set
503
+ if (options.remote) {
504
+ const dataDir = process.env.AGENT_RELAY_DATA_DIR ||
505
+ path.join(os.homedir(), '.local', 'share', 'agent-relay');
506
+ const configPath = path.join(dataDir, 'cloud-config.json');
507
+ if (fs.existsSync(configPath)) {
508
+ try {
509
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
510
+ const response = await fetch(`${config.cloudUrl}/api/daemons/agents`, {
511
+ method: 'POST',
512
+ headers: {
513
+ 'Authorization': `Bearer ${config.apiKey}`,
514
+ 'Content-Type': 'application/json',
515
+ },
516
+ body: JSON.stringify({ agents: [] }),
517
+ });
518
+ if (response.ok) {
519
+ const data = await response.json();
520
+ // Add remote agents (exclude local ones by name)
521
+ const localNames = new Set(combined.map(a => a.name));
522
+ for (const agent of data.allAgents) {
523
+ if (!localNames.has(agent.name)) {
524
+ combined.push({
525
+ name: agent.name,
526
+ status: agent.status.toUpperCase(),
527
+ cli: '-',
528
+ location: agent.daemonName,
529
+ daemonId: agent.daemonId,
530
+ });
531
+ }
532
+ }
533
+ }
534
+ }
535
+ catch (err) {
536
+ console.error('[warn] Failed to fetch remote agents:', err.message);
537
+ }
538
+ }
539
+ else {
540
+ console.error('[warn] Cloud not linked. Run `agent-relay cloud link` to see remote agents.');
541
+ }
542
+ }
329
543
  if (options.json) {
330
544
  console.log(JSON.stringify(combined, null, 2));
331
545
  return;
@@ -335,21 +549,39 @@ program
335
549
  console.log(`No agents found. Ensure the daemon is running and agents are connected${hint}.`);
336
550
  return;
337
551
  }
338
- console.log('NAME STATUS CLI TEAM');
339
- console.log('─'.repeat(50));
340
- combined.forEach((agent) => {
341
- const name = agent.name.padEnd(15);
342
- const status = agent.status.padEnd(8);
343
- const cli = agent.cli.padEnd(9);
344
- const team = agent.team ?? '-';
345
- console.log(`${name} ${status} ${cli} ${team}`);
346
- });
552
+ const hasRemote = combined.some(a => a.location !== 'local');
553
+ if (hasRemote) {
554
+ console.log('NAME STATUS CLI LOCATION');
555
+ console.log('─'.repeat(55));
556
+ combined.forEach((agent) => {
557
+ const name = agent.name.padEnd(15);
558
+ const status = agent.status.padEnd(8);
559
+ const cli = agent.cli.padEnd(9);
560
+ const location = agent.location ?? 'local';
561
+ console.log(`${name} ${status} ${cli} ${location}`);
562
+ });
563
+ }
564
+ else {
565
+ console.log('NAME STATUS CLI TEAM');
566
+ console.log('─'.repeat(50));
567
+ combined.forEach((agent) => {
568
+ const name = agent.name.padEnd(15);
569
+ const status = agent.status.padEnd(8);
570
+ const cli = agent.cli.padEnd(9);
571
+ const team = agent.team ?? '-';
572
+ console.log(`${name} ${status} ${cli} ${team}`);
573
+ });
574
+ }
347
575
  if (workers.length > 0) {
348
576
  console.log('');
349
577
  console.log('Commands:');
350
578
  console.log(' agent-relay agents:logs <name> - View spawned agent output');
351
579
  console.log(' agent-relay agents:kill <name> - Kill a spawned agent');
352
580
  }
581
+ if (!options.remote) {
582
+ console.log('');
583
+ console.log('Tip: Use --remote to include agents from other linked machines.');
584
+ }
353
585
  });
354
586
  // who - Show currently active agents (online within last 30s)
355
587
  program
@@ -541,6 +773,7 @@ program
541
773
  .description('Bridge multiple projects as orchestrator')
542
774
  .argument('[projects...]', 'Project paths to bridge')
543
775
  .option('--cli <tool>', 'CLI tool override for all projects')
776
+ .option('--architect [cli]', 'Spawn an architect agent to coordinate all projects (default: claude)')
544
777
  .action(async (projectPaths, options) => {
545
778
  const { resolveProjects, validateDaemons } = await import('../bridge/config.js');
546
779
  const { MultiProjectClient } = await import('../bridge/multi-project-client.js');
@@ -646,9 +879,106 @@ program
646
879
  console.log('Connected to all projects.');
647
880
  console.log('');
648
881
  console.log('Cross-project messaging:');
649
- console.log(' @relay:projectId:agent Message');
650
- console.log(' @relay:*:lead Broadcast to all leads');
882
+ console.log(' ->relay:projectId:agent Message');
883
+ console.log(' ->relay:*:lead Broadcast to all leads');
651
884
  console.log('');
885
+ // Spawn architect agent if --architect flag is set
886
+ let architectWrapper = null;
887
+ if (options.architect !== undefined) {
888
+ const { TmuxWrapper } = await import('../wrapper/tmux-wrapper.js');
889
+ // Determine CLI to use (default to claude)
890
+ const architectCli = typeof options.architect === 'string' ? options.architect : 'claude';
891
+ // Use first project as the base for the architect
892
+ const baseProject = valid[0];
893
+ const basePaths = getProjectPaths(baseProject.path);
894
+ // Build project context for the architect
895
+ const projectContext = valid.map(p => `- ${p.id}: ${p.path} (Lead: ${p.leadName})`).join('\n');
896
+ // Create architect system prompt
897
+ const architectPrompt = `You are the Architect, a cross-project coordinator overseeing multiple codebases.
898
+
899
+ ## Connected Projects
900
+ ${projectContext}
901
+
902
+ ## Your Role
903
+ - Coordinate high-level work across all projects
904
+ - Assign tasks to project leads
905
+ - Ensure consistency and resolve cross-project dependencies
906
+ - Review overall architecture decisions
907
+
908
+ ## Cross-Project Messaging
909
+
910
+ Use this syntax to message agents in specific projects:
911
+
912
+ \`\`\`
913
+ ->relay:${valid[0].id}:${valid[0].leadName} <<<
914
+ Your message to this project's lead>>>
915
+
916
+ ->relay:${valid.length > 1 ? valid[1].id : valid[0].id}:* <<<
917
+ Broadcast to all agents in a project>>>
918
+
919
+ ->relay:*:* <<<
920
+ Broadcast to ALL agents in ALL projects>>>
921
+ \`\`\`
922
+
923
+ Format: \`->relay:project-id:agent-name\`
924
+
925
+ ## Getting Started
926
+ 1. Check in with each project lead to understand current status
927
+ 2. Identify cross-project dependencies
928
+ 3. Coordinate work across teams
929
+
930
+ Start by greeting the project leads and asking for status updates.`;
931
+ console.log('Spawning Architect agent...');
932
+ console.log(` CLI: ${architectCli}`);
933
+ console.log(` Base project: ${baseProject.path}`);
934
+ console.log('');
935
+ // Determine command and args based on CLI
936
+ let command;
937
+ let args = [];
938
+ if (architectCli === 'claude' || architectCli.startsWith('claude:')) {
939
+ command = 'claude';
940
+ args = ['--dangerously-skip-permissions'];
941
+ // Add model if specified (e.g., claude:opus)
942
+ if (architectCli.includes(':')) {
943
+ const model = architectCli.split(':')[1];
944
+ args.push('--model', model);
945
+ }
946
+ }
947
+ else if (architectCli === 'codex') {
948
+ command = 'codex';
949
+ args = ['--dangerously-skip-permissions'];
950
+ }
951
+ else {
952
+ command = architectCli;
953
+ }
954
+ try {
955
+ architectWrapper = new TmuxWrapper({
956
+ name: 'Architect',
957
+ command,
958
+ args,
959
+ socketPath: basePaths.socketPath,
960
+ debug: false,
961
+ useInbox: true,
962
+ inboxDir: basePaths.dataDir,
963
+ });
964
+ await architectWrapper.start();
965
+ // Wait for agent to be ready, then inject the prompt
966
+ setTimeout(async () => {
967
+ try {
968
+ await architectWrapper.injectMessage(architectPrompt);
969
+ console.log('Architect agent started and initialized.');
970
+ console.log('Attach to session: tmux attach -t relay-Architect');
971
+ console.log('');
972
+ }
973
+ catch (err) {
974
+ console.error('Failed to inject architect prompt:', err);
975
+ }
976
+ }, 3000);
977
+ }
978
+ catch (err) {
979
+ console.error('Failed to spawn Architect agent:', err);
980
+ }
981
+ }
652
982
  // Handle messages from projects
653
983
  client.onMessage = (projectId, from, payload, messageId) => {
654
984
  console.log(`[${projectId}] ${from}: ${payload.body.substring(0, 80)}...`);
@@ -985,6 +1315,40 @@ program
985
1315
  }
986
1316
  }
987
1317
  });
1318
+ // release - Release a spawned agent via API (works from any context, no terminal required)
1319
+ program
1320
+ .command('release')
1321
+ .description('Release a spawned agent via API (no terminal required)')
1322
+ .argument('<name>', 'Agent name to release')
1323
+ .option('--port <port>', 'Dashboard port', DEFAULT_DASHBOARD_PORT)
1324
+ .action(async (name, options) => {
1325
+ const port = options.port || DEFAULT_DASHBOARD_PORT;
1326
+ try {
1327
+ const response = await fetch(`http://localhost:${port}/api/spawned/${encodeURIComponent(name)}`, {
1328
+ method: 'DELETE',
1329
+ });
1330
+ const result = await response.json();
1331
+ if (result.success) {
1332
+ console.log(`Released agent: ${name}`);
1333
+ process.exit(0);
1334
+ }
1335
+ else {
1336
+ console.error(`Failed to release ${name}: ${result.error || 'Unknown error'}`);
1337
+ process.exit(1);
1338
+ }
1339
+ }
1340
+ catch (err) {
1341
+ // If API call fails, try to provide helpful error message
1342
+ if (err.code === 'ECONNREFUSED') {
1343
+ console.error(`Cannot connect to dashboard at port ${port}. Is the daemon running?`);
1344
+ console.log(`Run 'agent-relay up' to start the daemon.`);
1345
+ }
1346
+ else {
1347
+ console.error(`Failed to release ${name}: ${err.message}`);
1348
+ }
1349
+ process.exit(1);
1350
+ }
1351
+ });
988
1352
  // agents:kill - Kill a spawned agent by PID
989
1353
  program
990
1354
  .command('agents:kill')
@@ -1039,5 +1403,426 @@ program
1039
1403
  }
1040
1404
  }
1041
1405
  });
1406
+ // ============================================================================
1407
+ // Cloud commands
1408
+ // ============================================================================
1409
+ const cloudCommand = program
1410
+ .command('cloud')
1411
+ .description('Cloud account and sync commands');
1412
+ cloudCommand
1413
+ .command('link')
1414
+ .description('Link this machine to your Agent Relay Cloud account')
1415
+ .option('--name <name>', 'Name for this machine')
1416
+ .option('--cloud-url <url>', 'Cloud API URL', process.env.AGENT_RELAY_CLOUD_URL || 'https://api.agent-relay.com')
1417
+ .action(async (options) => {
1418
+ const os = await import('node:os');
1419
+ const crypto = await import('node:crypto');
1420
+ const readline = await import('node:readline');
1421
+ const cloudUrl = options.cloudUrl;
1422
+ const machineName = options.name || os.hostname();
1423
+ // Generate machine ID
1424
+ const dataDir = process.env.AGENT_RELAY_DATA_DIR ||
1425
+ path.join(os.homedir(), '.local', 'share', 'agent-relay');
1426
+ const machineIdPath = path.join(dataDir, 'machine-id');
1427
+ const configPath = path.join(dataDir, 'cloud-config.json');
1428
+ let machineId;
1429
+ if (fs.existsSync(machineIdPath)) {
1430
+ machineId = fs.readFileSync(machineIdPath, 'utf-8').trim();
1431
+ }
1432
+ else {
1433
+ machineId = `${os.hostname()}-${crypto.randomBytes(8).toString('hex')}`;
1434
+ fs.mkdirSync(dataDir, { recursive: true });
1435
+ fs.writeFileSync(machineIdPath, machineId);
1436
+ }
1437
+ console.log('');
1438
+ console.log('🔗 Agent Relay Cloud - Link Machine');
1439
+ console.log('');
1440
+ console.log(`Machine: ${machineName}`);
1441
+ console.log(`ID: ${machineId}`);
1442
+ console.log('');
1443
+ // Generate a temporary code for the browser auth flow
1444
+ const tempCode = crypto.randomBytes(16).toString('hex');
1445
+ // Store temp code for callback
1446
+ const tempCodePath = path.join(dataDir, '.link-code');
1447
+ fs.writeFileSync(tempCodePath, tempCode);
1448
+ const authUrl = `${cloudUrl.replace('/api', '')}/cloud/link?code=${tempCode}&machine=${encodeURIComponent(machineId)}&name=${encodeURIComponent(machineName)}`;
1449
+ console.log('Open this URL in your browser to authenticate:');
1450
+ console.log('');
1451
+ console.log(` ${authUrl}`);
1452
+ console.log('');
1453
+ // Try to open browser automatically
1454
+ try {
1455
+ const openCommand = process.platform === 'darwin' ? 'open' :
1456
+ process.platform === 'win32' ? 'start' : 'xdg-open';
1457
+ await execAsync(`${openCommand} "${authUrl}"`);
1458
+ console.log('(Browser opened automatically)');
1459
+ }
1460
+ catch {
1461
+ console.log('(Copy the URL above and paste it in your browser)');
1462
+ }
1463
+ console.log('');
1464
+ console.log('After authenticating, paste your API key here:');
1465
+ const rl = readline.createInterface({
1466
+ input: process.stdin,
1467
+ output: process.stdout,
1468
+ });
1469
+ const apiKey = await new Promise((resolve) => {
1470
+ rl.question('API Key: ', (answer) => {
1471
+ rl.close();
1472
+ resolve(answer.trim());
1473
+ });
1474
+ });
1475
+ if (!apiKey || !apiKey.startsWith('ar_live_')) {
1476
+ console.error('');
1477
+ console.error('Invalid API key format. Expected ar_live_...');
1478
+ process.exit(1);
1479
+ }
1480
+ // Verify the API key works
1481
+ console.log('');
1482
+ console.log('Verifying API key...');
1483
+ try {
1484
+ const response = await fetch(`${cloudUrl}/api/daemons/heartbeat`, {
1485
+ method: 'POST',
1486
+ headers: {
1487
+ 'Authorization': `Bearer ${apiKey}`,
1488
+ 'Content-Type': 'application/json',
1489
+ },
1490
+ body: JSON.stringify({
1491
+ agents: [],
1492
+ metrics: { linkedAt: new Date().toISOString() },
1493
+ }),
1494
+ });
1495
+ if (!response.ok) {
1496
+ const error = await response.text();
1497
+ console.error(`Failed to verify API key: ${error}`);
1498
+ process.exit(1);
1499
+ }
1500
+ // Save config
1501
+ const config = {
1502
+ apiKey,
1503
+ cloudUrl,
1504
+ machineId,
1505
+ machineName,
1506
+ linkedAt: new Date().toISOString(),
1507
+ };
1508
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
1509
+ fs.chmodSync(configPath, 0o600); // Secure the file
1510
+ // Clean up temp code
1511
+ if (fs.existsSync(tempCodePath)) {
1512
+ fs.unlinkSync(tempCodePath);
1513
+ }
1514
+ console.log('');
1515
+ console.log('✓ Machine linked successfully!');
1516
+ console.log('');
1517
+ console.log('Your daemon will now sync with Agent Relay Cloud.');
1518
+ console.log('Run `agent-relay up` to start with cloud sync enabled.');
1519
+ console.log('');
1520
+ }
1521
+ catch (err) {
1522
+ console.error(`Failed to connect to cloud: ${err.message}`);
1523
+ process.exit(1);
1524
+ }
1525
+ });
1526
+ cloudCommand
1527
+ .command('unlink')
1528
+ .description('Unlink this machine from Agent Relay Cloud')
1529
+ .action(async () => {
1530
+ const os = await import('node:os');
1531
+ const dataDir = process.env.AGENT_RELAY_DATA_DIR ||
1532
+ path.join(os.homedir(), '.local', 'share', 'agent-relay');
1533
+ const configPath = path.join(dataDir, 'cloud-config.json');
1534
+ if (!fs.existsSync(configPath)) {
1535
+ console.log('This machine is not linked to Agent Relay Cloud.');
1536
+ return;
1537
+ }
1538
+ // Read current config
1539
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
1540
+ // Delete config file
1541
+ fs.unlinkSync(configPath);
1542
+ console.log('');
1543
+ console.log('✓ Machine unlinked from Agent Relay Cloud');
1544
+ console.log('');
1545
+ console.log(`Machine ID: ${config.machineId}`);
1546
+ console.log(`Was linked since: ${config.linkedAt}`);
1547
+ console.log('');
1548
+ console.log('Note: The API key has been removed locally. To fully revoke access,');
1549
+ console.log('visit your Agent Relay Cloud dashboard and remove this machine.');
1550
+ console.log('');
1551
+ });
1552
+ cloudCommand
1553
+ .command('status')
1554
+ .description('Show cloud sync status')
1555
+ .action(async () => {
1556
+ const os = await import('node:os');
1557
+ const dataDir = process.env.AGENT_RELAY_DATA_DIR ||
1558
+ path.join(os.homedir(), '.local', 'share', 'agent-relay');
1559
+ const configPath = path.join(dataDir, 'cloud-config.json');
1560
+ if (!fs.existsSync(configPath)) {
1561
+ console.log('');
1562
+ console.log('Cloud sync: Not configured');
1563
+ console.log('');
1564
+ console.log('Run `agent-relay cloud link` to connect to Agent Relay Cloud.');
1565
+ console.log('');
1566
+ return;
1567
+ }
1568
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
1569
+ console.log('');
1570
+ console.log('Cloud sync: Enabled');
1571
+ console.log('');
1572
+ console.log(` Machine: ${config.machineName}`);
1573
+ console.log(` ID: ${config.machineId}`);
1574
+ console.log(` Cloud URL: ${config.cloudUrl}`);
1575
+ console.log(` Linked: ${new Date(config.linkedAt).toLocaleString()}`);
1576
+ console.log('');
1577
+ // Check if daemon is running and connected
1578
+ const { getProjectPaths } = await import('../utils/project-namespace.js');
1579
+ const paths = getProjectPaths();
1580
+ if (fs.existsSync(paths.socketPath)) {
1581
+ console.log(' Daemon: Running');
1582
+ // Try to get cloud sync status from daemon
1583
+ try {
1584
+ const response = await fetch(`${config.cloudUrl}/api/daemons/heartbeat`, {
1585
+ method: 'POST',
1586
+ headers: {
1587
+ 'Authorization': `Bearer ${config.apiKey}`,
1588
+ 'Content-Type': 'application/json',
1589
+ },
1590
+ body: JSON.stringify({ agents: [], metrics: {} }),
1591
+ });
1592
+ if (response.ok) {
1593
+ console.log(' Cloud connection: Online');
1594
+ }
1595
+ else {
1596
+ console.log(' Cloud connection: Error (API key may be invalid)');
1597
+ }
1598
+ }
1599
+ catch (err) {
1600
+ console.log(` Cloud connection: Offline (${err.message})`);
1601
+ }
1602
+ }
1603
+ else {
1604
+ console.log(' Daemon: Not running');
1605
+ console.log(' Cloud connection: Offline (daemon not started)');
1606
+ }
1607
+ console.log('');
1608
+ });
1609
+ cloudCommand
1610
+ .command('sync')
1611
+ .description('Manually sync credentials from cloud')
1612
+ .action(async () => {
1613
+ const os = await import('node:os');
1614
+ const dataDir = process.env.AGENT_RELAY_DATA_DIR ||
1615
+ path.join(os.homedir(), '.local', 'share', 'agent-relay');
1616
+ const configPath = path.join(dataDir, 'cloud-config.json');
1617
+ if (!fs.existsSync(configPath)) {
1618
+ console.error('Not linked to cloud. Run `agent-relay cloud link` first.');
1619
+ process.exit(1);
1620
+ }
1621
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
1622
+ console.log('Syncing credentials from cloud...');
1623
+ try {
1624
+ const response = await fetch(`${config.cloudUrl}/api/daemons/credentials`, {
1625
+ headers: {
1626
+ 'Authorization': `Bearer ${config.apiKey}`,
1627
+ },
1628
+ });
1629
+ if (!response.ok) {
1630
+ const error = await response.text();
1631
+ console.error(`Failed to sync: ${error}`);
1632
+ process.exit(1);
1633
+ }
1634
+ const data = await response.json();
1635
+ console.log('');
1636
+ console.log(`Synced ${data.credentials.length} provider credentials:`);
1637
+ for (const cred of data.credentials) {
1638
+ console.log(` - ${cred.provider}`);
1639
+ }
1640
+ // Save credentials locally for daemon to use
1641
+ const credentialsPath = path.join(dataDir, 'cloud-credentials.json');
1642
+ fs.writeFileSync(credentialsPath, JSON.stringify(data.credentials, null, 2));
1643
+ fs.chmodSync(credentialsPath, 0o600);
1644
+ console.log('');
1645
+ console.log('✓ Credentials synced successfully');
1646
+ console.log('');
1647
+ }
1648
+ catch (err) {
1649
+ console.error(`Failed to sync: ${err.message}`);
1650
+ process.exit(1);
1651
+ }
1652
+ });
1653
+ cloudCommand
1654
+ .command('agents')
1655
+ .description('List agents across all linked machines')
1656
+ .option('--json', 'Output as JSON')
1657
+ .action(async (options) => {
1658
+ const os = await import('node:os');
1659
+ const dataDir = process.env.AGENT_RELAY_DATA_DIR ||
1660
+ path.join(os.homedir(), '.local', 'share', 'agent-relay');
1661
+ const configPath = path.join(dataDir, 'cloud-config.json');
1662
+ if (!fs.existsSync(configPath)) {
1663
+ console.error('Not linked to cloud. Run `agent-relay cloud link` first.');
1664
+ process.exit(1);
1665
+ }
1666
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
1667
+ try {
1668
+ // Get agents from cloud
1669
+ const response = await fetch(`${config.cloudUrl}/api/daemons/agents`, {
1670
+ method: 'POST',
1671
+ headers: {
1672
+ 'Authorization': `Bearer ${config.apiKey}`,
1673
+ 'Content-Type': 'application/json',
1674
+ },
1675
+ body: JSON.stringify({ agents: [] }), // Report no agents, just fetch list
1676
+ });
1677
+ if (!response.ok) {
1678
+ const error = await response.text();
1679
+ console.error(`Failed to fetch agents: ${error}`);
1680
+ process.exit(1);
1681
+ }
1682
+ const data = await response.json();
1683
+ if (options.json) {
1684
+ console.log(JSON.stringify(data.allAgents, null, 2));
1685
+ return;
1686
+ }
1687
+ if (!data.allAgents.length) {
1688
+ console.log('No agents found across linked machines.');
1689
+ console.log('Make sure daemons are running on linked machines.');
1690
+ return;
1691
+ }
1692
+ console.log('');
1693
+ console.log('Agents across all linked machines:');
1694
+ console.log('');
1695
+ console.log('NAME STATUS DAEMON MACHINE');
1696
+ console.log('─'.repeat(65));
1697
+ // Group by daemon
1698
+ const byDaemon = new Map();
1699
+ for (const agent of data.allAgents) {
1700
+ const existing = byDaemon.get(agent.daemonName) || [];
1701
+ existing.push(agent);
1702
+ byDaemon.set(agent.daemonName, existing);
1703
+ }
1704
+ for (const [daemonName, agents] of byDaemon.entries()) {
1705
+ for (const agent of agents) {
1706
+ const name = agent.name.padEnd(15);
1707
+ const status = agent.status.padEnd(8);
1708
+ const daemon = daemonName.padEnd(18);
1709
+ const machine = agent.machineId.substring(0, 20);
1710
+ console.log(`${name} ${status} ${daemon} ${machine}`);
1711
+ }
1712
+ }
1713
+ console.log('');
1714
+ console.log(`Total: ${data.allAgents.length} agents on ${byDaemon.size} machines`);
1715
+ console.log('');
1716
+ }
1717
+ catch (err) {
1718
+ console.error(`Failed to fetch agents: ${err.message}`);
1719
+ process.exit(1);
1720
+ }
1721
+ });
1722
+ cloudCommand
1723
+ .command('send')
1724
+ .description('Send a message to an agent on any linked machine')
1725
+ .argument('<agent>', 'Target agent name')
1726
+ .argument('<message>', 'Message to send')
1727
+ .option('--from <name>', 'Sender name', 'cli')
1728
+ .action(async (agent, message, options) => {
1729
+ const os = await import('node:os');
1730
+ const dataDir = process.env.AGENT_RELAY_DATA_DIR ||
1731
+ path.join(os.homedir(), '.local', 'share', 'agent-relay');
1732
+ const configPath = path.join(dataDir, 'cloud-config.json');
1733
+ if (!fs.existsSync(configPath)) {
1734
+ console.error('Not linked to cloud. Run `agent-relay cloud link` first.');
1735
+ process.exit(1);
1736
+ }
1737
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
1738
+ console.log(`Sending message to ${agent}...`);
1739
+ try {
1740
+ // First, find which daemon the agent is on
1741
+ const agentsResponse = await fetch(`${config.cloudUrl}/api/daemons/agents`, {
1742
+ method: 'POST',
1743
+ headers: {
1744
+ 'Authorization': `Bearer ${config.apiKey}`,
1745
+ 'Content-Type': 'application/json',
1746
+ },
1747
+ body: JSON.stringify({ agents: [] }),
1748
+ });
1749
+ if (!agentsResponse.ok) {
1750
+ const error = await agentsResponse.text();
1751
+ console.error(`Failed to find agent: ${error}`);
1752
+ process.exit(1);
1753
+ }
1754
+ const agentsData = await agentsResponse.json();
1755
+ const targetAgent = agentsData.allAgents.find(a => a.name === agent);
1756
+ if (!targetAgent) {
1757
+ console.error(`Agent "${agent}" not found.`);
1758
+ console.log('Available agents:');
1759
+ for (const a of agentsData.allAgents) {
1760
+ console.log(` - ${a.name} (on ${a.daemonName})`);
1761
+ }
1762
+ process.exit(1);
1763
+ }
1764
+ // Send the message
1765
+ const sendResponse = await fetch(`${config.cloudUrl}/api/daemons/message`, {
1766
+ method: 'POST',
1767
+ headers: {
1768
+ 'Authorization': `Bearer ${config.apiKey}`,
1769
+ 'Content-Type': 'application/json',
1770
+ },
1771
+ body: JSON.stringify({
1772
+ targetDaemonId: targetAgent.daemonId,
1773
+ targetAgent: agent,
1774
+ message: {
1775
+ from: options.from,
1776
+ content: message,
1777
+ },
1778
+ }),
1779
+ });
1780
+ if (!sendResponse.ok) {
1781
+ const error = await sendResponse.text();
1782
+ console.error(`Failed to send message: ${error}`);
1783
+ process.exit(1);
1784
+ }
1785
+ console.log('');
1786
+ console.log(`✓ Message sent to ${agent} on ${targetAgent.daemonName}`);
1787
+ console.log('');
1788
+ }
1789
+ catch (err) {
1790
+ console.error(`Failed to send message: ${err.message}`);
1791
+ process.exit(1);
1792
+ }
1793
+ });
1794
+ cloudCommand
1795
+ .command('daemons')
1796
+ .description('List all linked daemon instances')
1797
+ .option('--json', 'Output as JSON')
1798
+ .action(async (options) => {
1799
+ const os = await import('node:os');
1800
+ const dataDir = process.env.AGENT_RELAY_DATA_DIR ||
1801
+ path.join(os.homedir(), '.local', 'share', 'agent-relay');
1802
+ const configPath = path.join(dataDir, 'cloud-config.json');
1803
+ if (!fs.existsSync(configPath)) {
1804
+ console.error('Not linked to cloud. Run `agent-relay cloud link` first.');
1805
+ process.exit(1);
1806
+ }
1807
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
1808
+ try {
1809
+ // Get daemons list (requires browser auth, so we use a workaround)
1810
+ // For now, just show what we know about our own daemon
1811
+ console.log('');
1812
+ console.log('Linked Daemon:');
1813
+ console.log('');
1814
+ console.log(` Machine: ${config.machineName}`);
1815
+ console.log(` ID: ${config.machineId}`);
1816
+ console.log(` Cloud: ${config.cloudUrl}`);
1817
+ console.log(` Linked: ${new Date(config.linkedAt).toLocaleString()}`);
1818
+ console.log('');
1819
+ console.log('Note: To see all linked daemons, visit your cloud dashboard.');
1820
+ console.log('');
1821
+ }
1822
+ catch (err) {
1823
+ console.error(`Failed: ${err.message}`);
1824
+ process.exit(1);
1825
+ }
1826
+ });
1042
1827
  program.parse();
1043
1828
  //# sourceMappingURL=index.js.map