agent-relay 1.3.2 → 1.5.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 (318) hide show
  1. package/README.md +130 -158
  2. package/bin/relay-pty +0 -0
  3. package/bin/relay-pty-darwin-arm64 +0 -0
  4. package/bin/relay-pty-darwin-x64 +0 -0
  5. package/bin/relay-pty-linux-x64 +0 -0
  6. package/deploy/workspace/entrypoint.sh +9 -0
  7. package/dist/bridge/spawner.d.ts +4 -4
  8. package/dist/bridge/spawner.js +58 -92
  9. package/dist/cli/index.d.ts +8 -6
  10. package/dist/cli/index.js +282 -47
  11. package/dist/cloud/api/daemons.js +13 -32
  12. package/dist/cloud/api/onboarding.js +2 -4
  13. package/dist/cloud/api/providers.js +6 -0
  14. package/dist/cloud/config.d.ts +1 -0
  15. package/dist/cloud/config.js +2 -0
  16. package/dist/cloud/db/bulk-ingest.d.ts +2 -1
  17. package/dist/cloud/db/drizzle.d.ts +21 -26
  18. package/dist/cloud/db/drizzle.js +87 -100
  19. package/dist/cloud/db/index.d.ts +6 -5
  20. package/dist/cloud/db/index.js +9 -8
  21. package/dist/cloud/db/schema.d.ts +1049 -1076
  22. package/dist/cloud/db/schema.js +59 -71
  23. package/dist/cloud/server.js +854 -18
  24. package/dist/cloud/services/persistence.d.ts +15 -15
  25. package/dist/cloud/services/persistence.js +14 -14
  26. package/dist/daemon/agent-manager.d.ts +6 -5
  27. package/dist/daemon/agent-manager.js +12 -8
  28. package/dist/daemon/channel-membership-store.d.ts +48 -0
  29. package/dist/daemon/channel-membership-store.js +149 -0
  30. package/dist/daemon/cloud-sync.d.ts +2 -0
  31. package/dist/daemon/cloud-sync.js +4 -0
  32. package/dist/daemon/connection.js +17 -9
  33. package/dist/daemon/router.d.ts +37 -0
  34. package/dist/daemon/router.js +318 -79
  35. package/dist/daemon/server.d.ts +15 -0
  36. package/dist/daemon/server.js +141 -3
  37. package/dist/dashboard/out/404.html +1 -0
  38. package/dist/dashboard/out/_next/static/IxxVRv94L1w3ReRGAiI-k/_buildManifest.js +1 -0
  39. package/dist/dashboard/out/_next/static/IxxVRv94L1w3ReRGAiI-k/_ssgManifest.js +1 -0
  40. package/dist/dashboard/out/_next/static/chunks/116-eacf84a131b80db9.js +1 -0
  41. package/dist/dashboard/out/_next/static/chunks/117-c8afed19e821a35d.js +2 -0
  42. package/dist/dashboard/out/_next/static/chunks/282-980c2eb8fff20123.js +1 -0
  43. package/dist/dashboard/out/_next/static/chunks/532-bace199897eeab37.js +9 -0
  44. package/dist/dashboard/out/_next/static/chunks/64-87ab9cd6bcf2f737.js +1 -0
  45. package/dist/dashboard/out/_next/static/chunks/648-acb2ff9f77cbfbd3.js +1 -0
  46. package/dist/dashboard/out/_next/static/chunks/766-aa7c8c9900ff5f53.js +1 -0
  47. package/dist/dashboard/out/_next/static/chunks/83-4f08122d4e7e79a6.js +1 -0
  48. package/dist/dashboard/out/_next/static/chunks/847-f1f467060f32afff.js +1 -0
  49. package/dist/dashboard/out/_next/static/chunks/891-a024fbe4b619cf6f.js +1 -0
  50. package/dist/dashboard/out/_next/static/chunks/app/_not-found/page-60501fddbafba9dc.js +1 -0
  51. package/dist/dashboard/out/_next/static/chunks/app/app/onboarding/page-f746f29e01fffc43.js +1 -0
  52. package/dist/dashboard/out/_next/static/chunks/app/app/page-ffad986adfcc8b31.js +1 -0
  53. package/dist/dashboard/out/_next/static/chunks/app/cloud/link/page-cfeb437f08a12ed9.js +1 -0
  54. package/dist/dashboard/out/_next/static/chunks/app/connect-repos/page-03ac6f35a6654ea6.js +1 -0
  55. package/dist/dashboard/out/_next/static/chunks/app/history/page-240f91e8b06ba8ac.js +1 -0
  56. package/dist/dashboard/out/_next/static/chunks/app/layout-c0d118c0f92d969c.js +1 -0
  57. package/dist/dashboard/out/_next/static/chunks/app/login/page-6ec54eee75877971.js +1 -0
  58. package/dist/dashboard/out/_next/static/chunks/app/metrics/page-82938ab8fcf44694.js +1 -0
  59. package/dist/dashboard/out/_next/static/chunks/app/page-671037943b2f2e43.js +1 -0
  60. package/dist/dashboard/out/_next/static/chunks/app/pricing/page-0efa024c28ba4597.js +1 -0
  61. package/dist/dashboard/out/_next/static/chunks/app/providers/page-57cbd738c6a73859.js +1 -0
  62. package/dist/dashboard/out/_next/static/chunks/app/providers/setup/[provider]/page-5ab0854472b402b0.js +1 -0
  63. package/dist/dashboard/out/_next/static/chunks/app/signup/page-18a4665665f6be11.js +1 -0
  64. package/dist/dashboard/out/_next/static/chunks/e868780c-48e5f147c90a3a41.js +18 -0
  65. package/dist/dashboard/out/_next/static/chunks/fd9d1056-609918ca7b6280bb.js +1 -0
  66. package/dist/dashboard/out/_next/static/chunks/framework-f66176bb897dc684.js +1 -0
  67. package/dist/dashboard/out/_next/static/chunks/main-5a40a5ae29646e1b.js +1 -0
  68. package/dist/dashboard/out/_next/static/chunks/main-app-6e8e8d3ef4e0192a.js +1 -0
  69. package/dist/dashboard/out/_next/static/chunks/pages/_app-72b849fbd24ac258.js +1 -0
  70. package/dist/dashboard/out/_next/static/chunks/pages/_error-7ba65e1336b92748.js +1 -0
  71. package/dist/dashboard/out/_next/static/chunks/polyfills-42372ed130431b0a.js +1 -0
  72. package/dist/dashboard/out/_next/static/chunks/webpack-1cdd8ed57114d5e1.js +1 -0
  73. package/dist/dashboard/out/_next/static/css/4034f236dd1a3178.css +1 -0
  74. package/dist/dashboard/out/_next/static/css/8f9ed310f454e5a5.css +1 -0
  75. package/dist/dashboard/out/alt-logos/agent-relay-logo-128.png +0 -0
  76. package/dist/dashboard/out/alt-logos/agent-relay-logo-256.png +0 -0
  77. package/dist/dashboard/out/alt-logos/agent-relay-logo-32.png +0 -0
  78. package/dist/dashboard/out/alt-logos/agent-relay-logo-512.png +0 -0
  79. package/dist/dashboard/out/alt-logos/agent-relay-logo-64.png +0 -0
  80. package/dist/dashboard/out/alt-logos/agent-relay-logo.svg +45 -0
  81. package/dist/dashboard/out/alt-logos/logo.svg +38 -0
  82. package/dist/dashboard/out/alt-logos/monogram-logo-128.png +0 -0
  83. package/dist/dashboard/out/alt-logos/monogram-logo-256.png +0 -0
  84. package/dist/dashboard/out/alt-logos/monogram-logo-32.png +0 -0
  85. package/dist/dashboard/out/alt-logos/monogram-logo-512.png +0 -0
  86. package/dist/dashboard/out/alt-logos/monogram-logo-64.png +0 -0
  87. package/dist/dashboard/out/alt-logos/monogram-logo.svg +38 -0
  88. package/dist/dashboard/out/app/onboarding.html +1 -0
  89. package/dist/dashboard/out/app/onboarding.txt +7 -0
  90. package/dist/dashboard/out/app.html +1 -0
  91. package/dist/dashboard/out/app.txt +7 -0
  92. package/dist/dashboard/out/apple-icon.png +0 -0
  93. package/dist/dashboard/out/cloud/link.html +1 -0
  94. package/dist/dashboard/out/cloud/link.txt +7 -0
  95. package/dist/dashboard/out/connect-repos.html +1 -0
  96. package/dist/dashboard/out/connect-repos.txt +7 -0
  97. package/dist/dashboard/out/history.html +1 -0
  98. package/dist/dashboard/out/history.txt +7 -0
  99. package/dist/dashboard/out/index.html +1 -0
  100. package/dist/dashboard/out/index.txt +7 -0
  101. package/dist/dashboard/out/login.html +5 -0
  102. package/dist/dashboard/out/login.txt +7 -0
  103. package/dist/dashboard/out/metrics.html +1 -0
  104. package/dist/dashboard/out/metrics.txt +7 -0
  105. package/dist/dashboard/out/pricing.html +13 -0
  106. package/dist/dashboard/out/pricing.txt +7 -0
  107. package/dist/dashboard/out/providers/setup/claude.html +1 -0
  108. package/dist/dashboard/out/providers/setup/claude.txt +8 -0
  109. package/dist/dashboard/out/providers/setup/codex.html +1 -0
  110. package/dist/dashboard/out/providers/setup/codex.txt +8 -0
  111. package/dist/dashboard/out/providers.html +1 -0
  112. package/dist/dashboard/out/providers.txt +7 -0
  113. package/dist/dashboard/out/signup.html +6 -0
  114. package/dist/dashboard/out/signup.txt +7 -0
  115. package/dist/dashboard-server/metrics.d.ts +105 -0
  116. package/dist/dashboard-server/metrics.js +193 -0
  117. package/dist/dashboard-server/needs-attention.d.ts +24 -0
  118. package/dist/dashboard-server/needs-attention.js +78 -0
  119. package/dist/dashboard-server/server.d.ts +15 -0
  120. package/dist/dashboard-server/server.js +4753 -0
  121. package/dist/dashboard-server/start.d.ts +6 -0
  122. package/dist/dashboard-server/start.js +13 -0
  123. package/dist/dashboard-server/user-bridge.d.ts +132 -0
  124. package/dist/dashboard-server/user-bridge.js +317 -0
  125. package/dist/protocol/channels.d.ts +14 -8
  126. package/dist/protocol/channels.js +1 -1
  127. package/dist/protocol/index.d.ts +1 -0
  128. package/dist/protocol/index.js +1 -0
  129. package/dist/protocol/relay-pty-schemas.d.ts +209 -0
  130. package/dist/protocol/relay-pty-schemas.js +60 -0
  131. package/dist/wrapper/auth-detection.js +8 -1
  132. package/dist/wrapper/base-wrapper.d.ts +11 -1
  133. package/dist/wrapper/base-wrapper.js +67 -6
  134. package/dist/wrapper/client.d.ts +49 -1
  135. package/dist/wrapper/client.js +167 -0
  136. package/dist/wrapper/parser.d.ts +0 -4
  137. package/dist/wrapper/parser.js +38 -10
  138. package/dist/wrapper/pty-wrapper.d.ts +12 -1
  139. package/dist/wrapper/pty-wrapper.js +104 -5
  140. package/dist/wrapper/relay-pty-orchestrator.d.ts +270 -0
  141. package/dist/wrapper/relay-pty-orchestrator.js +970 -0
  142. package/dist/wrapper/shared.d.ts +1 -1
  143. package/dist/wrapper/shared.js +14 -4
  144. package/dist/wrapper/tmux-wrapper.d.ts +13 -1
  145. package/dist/wrapper/tmux-wrapper.js +143 -29
  146. package/package.json +9 -4
  147. package/scripts/postinstall.js +101 -11
  148. package/.trajectories/active/traj_3yx9dy148mge.json +0 -42
  149. package/.trajectories/agent-relay-322-324.md +0 -17
  150. package/.trajectories/completed/2026-01/traj_03zupyv1s7b9.json +0 -49
  151. package/.trajectories/completed/2026-01/traj_03zupyv1s7b9.md +0 -31
  152. package/.trajectories/completed/2026-01/traj_0zacdjl1g4ht.json +0 -125
  153. package/.trajectories/completed/2026-01/traj_0zacdjl1g4ht.md +0 -62
  154. package/.trajectories/completed/2026-01/traj_1dviorhnkcb5.json +0 -65
  155. package/.trajectories/completed/2026-01/traj_1dviorhnkcb5.md +0 -37
  156. package/.trajectories/completed/2026-01/traj_1g7yx6qtg4ai.json +0 -49
  157. package/.trajectories/completed/2026-01/traj_1g7yx6qtg4ai.md +0 -31
  158. package/.trajectories/completed/2026-01/traj_1k5if5snst2e.json +0 -65
  159. package/.trajectories/completed/2026-01/traj_1k5if5snst2e.md +0 -37
  160. package/.trajectories/completed/2026-01/traj_1rp3rges5811.json +0 -49
  161. package/.trajectories/completed/2026-01/traj_1rp3rges5811.md +0 -31
  162. package/.trajectories/completed/2026-01/traj_22bhyulruouw.json +0 -113
  163. package/.trajectories/completed/2026-01/traj_22bhyulruouw.md +0 -57
  164. package/.trajectories/completed/2026-01/traj_2dao7ddgnta0.json +0 -53
  165. package/.trajectories/completed/2026-01/traj_2dao7ddgnta0.md +0 -32
  166. package/.trajectories/completed/2026-01/traj_33iuy72sezbk.json +0 -49
  167. package/.trajectories/completed/2026-01/traj_33iuy72sezbk.md +0 -31
  168. package/.trajectories/completed/2026-01/traj_3t0440mjeunc.json +0 -26
  169. package/.trajectories/completed/2026-01/traj_3t0440mjeunc.md +0 -6
  170. package/.trajectories/completed/2026-01/traj_45x9494d9xnr.json +0 -47
  171. package/.trajectories/completed/2026-01/traj_45x9494d9xnr.md +0 -32
  172. package/.trajectories/completed/2026-01/traj_4aa0bb77s4nh.json +0 -53
  173. package/.trajectories/completed/2026-01/traj_4aa0bb77s4nh.md +0 -32
  174. package/.trajectories/completed/2026-01/traj_4qwd4zmhfwp4.json +0 -49
  175. package/.trajectories/completed/2026-01/traj_4qwd4zmhfwp4.md +0 -31
  176. package/.trajectories/completed/2026-01/traj_5ammh5qtvklq.json +0 -77
  177. package/.trajectories/completed/2026-01/traj_5ammh5qtvklq.md +0 -42
  178. package/.trajectories/completed/2026-01/traj_5lhmzq8rxpqv.json +0 -59
  179. package/.trajectories/completed/2026-01/traj_5lhmzq8rxpqv.md +0 -33
  180. package/.trajectories/completed/2026-01/traj_5vr4e9erb1fs.json +0 -53
  181. package/.trajectories/completed/2026-01/traj_5vr4e9erb1fs.md +0 -32
  182. package/.trajectories/completed/2026-01/traj_6fgiwdoklvym.json +0 -48
  183. package/.trajectories/completed/2026-01/traj_6fgiwdoklvym.md +0 -24
  184. package/.trajectories/completed/2026-01/traj_6mieijqyvaag.json +0 -77
  185. package/.trajectories/completed/2026-01/traj_6mieijqyvaag.md +0 -42
  186. package/.trajectories/completed/2026-01/traj_6unwwmgyj5sq.json +0 -109
  187. package/.trajectories/completed/2026-01/traj_78ffm31jn3uk.json +0 -77
  188. package/.trajectories/completed/2026-01/traj_78ffm31jn3uk.md +0 -42
  189. package/.trajectories/completed/2026-01/traj_7ludwvz45veh.json +0 -209
  190. package/.trajectories/completed/2026-01/traj_7ludwvz45veh.md +0 -97
  191. package/.trajectories/completed/2026-01/traj_94gnp3k30goq.json +0 -66
  192. package/.trajectories/completed/2026-01/traj_94gnp3k30goq.md +0 -36
  193. package/.trajectories/completed/2026-01/traj_9921cuhel0pj.json +0 -48
  194. package/.trajectories/completed/2026-01/traj_9921cuhel0pj.md +0 -24
  195. package/.trajectories/completed/2026-01/traj_a0tqx8biw9c4.json +0 -49
  196. package/.trajectories/completed/2026-01/traj_a0tqx8biw9c4.md +0 -31
  197. package/.trajectories/completed/2026-01/traj_ajs7zqfux4wc.json +0 -49
  198. package/.trajectories/completed/2026-01/traj_ajs7zqfux4wc.md +0 -23
  199. package/.trajectories/completed/2026-01/traj_avqeghu6pz5a.json +0 -40
  200. package/.trajectories/completed/2026-01/traj_avqeghu6pz5a.md +0 -22
  201. package/.trajectories/completed/2026-01/traj_ax8uungxz2qh.json +0 -66
  202. package/.trajectories/completed/2026-01/traj_ax8uungxz2qh.md +0 -36
  203. package/.trajectories/completed/2026-01/traj_c9izbh2snpzf.json +0 -49
  204. package/.trajectories/completed/2026-01/traj_c9izbh2snpzf.md +0 -31
  205. package/.trajectories/completed/2026-01/traj_cpn70dw066nt.json +0 -65
  206. package/.trajectories/completed/2026-01/traj_cpn70dw066nt.md +0 -37
  207. package/.trajectories/completed/2026-01/traj_cvtqhlwcq9s0.json +0 -53
  208. package/.trajectories/completed/2026-01/traj_cvtqhlwcq9s0.md +0 -32
  209. package/.trajectories/completed/2026-01/traj_cxofprm2m2en.json +0 -49
  210. package/.trajectories/completed/2026-01/traj_cxofprm2m2en.md +0 -31
  211. package/.trajectories/completed/2026-01/traj_d2hhz3k0vrhn.json +0 -26
  212. package/.trajectories/completed/2026-01/traj_d2hhz3k0vrhn.md +0 -6
  213. package/.trajectories/completed/2026-01/traj_dcsp9s8y01ra.json +0 -121
  214. package/.trajectories/completed/2026-01/traj_dcsp9s8y01ra.md +0 -29
  215. package/.trajectories/completed/2026-01/traj_dfuvww9pege5.json +0 -59
  216. package/.trajectories/completed/2026-01/traj_dfuvww9pege5.md +0 -37
  217. package/.trajectories/completed/2026-01/traj_erglv2f8t9eh.json +0 -36
  218. package/.trajectories/completed/2026-01/traj_erglv2f8t9eh.md +0 -21
  219. package/.trajectories/completed/2026-01/traj_fhx9irlckht6.json +0 -53
  220. package/.trajectories/completed/2026-01/traj_fhx9irlckht6.md +0 -32
  221. package/.trajectories/completed/2026-01/traj_fqduidx3xbtp.json +0 -101
  222. package/.trajectories/completed/2026-01/traj_fqduidx3xbtp.md +0 -52
  223. package/.trajectories/completed/2026-01/traj_g0fisy9h51mf.json +0 -77
  224. package/.trajectories/completed/2026-01/traj_g0fisy9h51mf.md +0 -42
  225. package/.trajectories/completed/2026-01/traj_gjdre5voouod.json +0 -53
  226. package/.trajectories/completed/2026-01/traj_gjdre5voouod.md +0 -32
  227. package/.trajectories/completed/2026-01/traj_gtlyqtta3x8l.json +0 -25
  228. package/.trajectories/completed/2026-01/traj_gtlyqtta3x8l.md +0 -15
  229. package/.trajectories/completed/2026-01/traj_h4xijiuip3w4.json +0 -101
  230. package/.trajectories/completed/2026-01/traj_h4xijiuip3w4.md +0 -44
  231. package/.trajectories/completed/2026-01/traj_he75f24d1xfm.json +0 -101
  232. package/.trajectories/completed/2026-01/traj_he75f24d1xfm.md +0 -52
  233. package/.trajectories/completed/2026-01/traj_hf81ey93uz6t.json +0 -49
  234. package/.trajectories/completed/2026-01/traj_hf81ey93uz6t.md +0 -31
  235. package/.trajectories/completed/2026-01/traj_hfmki2jr9d4r.json +0 -65
  236. package/.trajectories/completed/2026-01/traj_hfmki2jr9d4r.md +0 -37
  237. package/.trajectories/completed/2026-01/traj_hhxte7w4gjjx.json +0 -22
  238. package/.trajectories/completed/2026-01/traj_hhxte7w4gjjx.md +0 -5
  239. package/.trajectories/completed/2026-01/traj_hpungyhoj6v5.json +0 -53
  240. package/.trajectories/completed/2026-01/traj_hpungyhoj6v5.md +0 -32
  241. package/.trajectories/completed/2026-01/traj_lgtodco7dp1n.json +0 -61
  242. package/.trajectories/completed/2026-01/traj_lgtodco7dp1n.md +0 -36
  243. package/.trajectories/completed/2026-01/traj_lq450ly148uw.json +0 -49
  244. package/.trajectories/completed/2026-01/traj_lq450ly148uw.md +0 -31
  245. package/.trajectories/completed/2026-01/traj_m2xkjv0w2sq7.json +0 -25
  246. package/.trajectories/completed/2026-01/traj_m2xkjv0w2sq7.md +0 -15
  247. package/.trajectories/completed/2026-01/traj_multi_server_arch.md +0 -101
  248. package/.trajectories/completed/2026-01/traj_noq5zbvnrdvz.json +0 -53
  249. package/.trajectories/completed/2026-01/traj_noq5zbvnrdvz.md +0 -32
  250. package/.trajectories/completed/2026-01/traj_ntbs6ppopf46.json +0 -53
  251. package/.trajectories/completed/2026-01/traj_ntbs6ppopf46.md +0 -32
  252. package/.trajectories/completed/2026-01/traj_oszg9flv74pk.json +0 -73
  253. package/.trajectories/completed/2026-01/traj_oszg9flv74pk.md +0 -41
  254. package/.trajectories/completed/2026-01/traj_ozd98si6a7ns.json +0 -48
  255. package/.trajectories/completed/2026-01/traj_ozd98si6a7ns.md +0 -24
  256. package/.trajectories/completed/2026-01/traj_prdza7a5cxp5.json +0 -53
  257. package/.trajectories/completed/2026-01/traj_prdza7a5cxp5.md +0 -32
  258. package/.trajectories/completed/2026-01/traj_psd9ob0j2ru3.json +0 -27
  259. package/.trajectories/completed/2026-01/traj_psd9ob0j2ru3.md +0 -14
  260. package/.trajectories/completed/2026-01/traj_pulomd3y8cvj.json +0 -77
  261. package/.trajectories/completed/2026-01/traj_pulomd3y8cvj.md +0 -42
  262. package/.trajectories/completed/2026-01/traj_qb3twvvywfwi.json +0 -77
  263. package/.trajectories/completed/2026-01/traj_qb3twvvywfwi.md +0 -42
  264. package/.trajectories/completed/2026-01/traj_qft54mi7nfor.json +0 -53
  265. package/.trajectories/completed/2026-01/traj_qft54mi7nfor.md +0 -32
  266. package/.trajectories/completed/2026-01/traj_qx9uhf8whhxo.json +0 -83
  267. package/.trajectories/completed/2026-01/traj_qx9uhf8whhxo.md +0 -47
  268. package/.trajectories/completed/2026-01/traj_rd9toccj18a0.json +0 -59
  269. package/.trajectories/completed/2026-01/traj_rd9toccj18a0.md +0 -37
  270. package/.trajectories/completed/2026-01/traj_rsavt0jipi3c.json +0 -109
  271. package/.trajectories/completed/2026-01/traj_rsavt0jipi3c.md +0 -56
  272. package/.trajectories/completed/2026-01/traj_rt4fiw3ecp50.json +0 -48
  273. package/.trajectories/completed/2026-01/traj_rt4fiw3ecp50.md +0 -16
  274. package/.trajectories/completed/2026-01/traj_st8j35b0hrlc.json +0 -59
  275. package/.trajectories/completed/2026-01/traj_st8j35b0hrlc.md +0 -37
  276. package/.trajectories/completed/2026-01/traj_t1yy8m7hbuxp.json +0 -53
  277. package/.trajectories/completed/2026-01/traj_t1yy8m7hbuxp.md +0 -32
  278. package/.trajectories/completed/2026-01/traj_tmux_orchestrator_analysis.json +0 -84
  279. package/.trajectories/completed/2026-01/traj_tmux_orchestrator_analysis.md +0 -109
  280. package/.trajectories/completed/2026-01/traj_u9n9eqasw16k.json +0 -53
  281. package/.trajectories/completed/2026-01/traj_u9n9eqasw16k.md +0 -32
  282. package/.trajectories/completed/2026-01/traj_ub8csuv3lcv4.json +0 -53
  283. package/.trajectories/completed/2026-01/traj_ub8csuv3lcv4.md +0 -32
  284. package/.trajectories/completed/2026-01/traj_uc29tlso8i9s.json +0 -186
  285. package/.trajectories/completed/2026-01/traj_uc29tlso8i9s.md +0 -86
  286. package/.trajectories/completed/2026-01/traj_ui9b4tqxoa7j.json +0 -77
  287. package/.trajectories/completed/2026-01/traj_ui9b4tqxoa7j.md +0 -42
  288. package/.trajectories/completed/2026-01/traj_v87hypnongqx.json +0 -71
  289. package/.trajectories/completed/2026-01/traj_v87hypnongqx.md +0 -42
  290. package/.trajectories/completed/2026-01/traj_v9dkdoxylyid.json +0 -89
  291. package/.trajectories/completed/2026-01/traj_v9dkdoxylyid.md +0 -47
  292. package/.trajectories/completed/2026-01/traj_wkp2fgzdyinb.json +0 -53
  293. package/.trajectories/completed/2026-01/traj_wkp2fgzdyinb.md +0 -32
  294. package/.trajectories/completed/2026-01/traj_x14t8w8rn7xg.json +0 -20
  295. package/.trajectories/completed/2026-01/traj_x14t8w8rn7xg.md +0 -6
  296. package/.trajectories/completed/2026-01/traj_x721m1j9rzup.json +0 -113
  297. package/.trajectories/completed/2026-01/traj_x721m1j9rzup.md +0 -57
  298. package/.trajectories/completed/2026-01/traj_xjqvmep5ed3h.json +0 -61
  299. package/.trajectories/completed/2026-01/traj_xjqvmep5ed3h.md +0 -36
  300. package/.trajectories/completed/2026-01/traj_xnwbznkvv8ua.json +0 -175
  301. package/.trajectories/completed/2026-01/traj_xnwbznkvv8ua.md +0 -82
  302. package/.trajectories/completed/2026-01/traj_xy9vifpqet80.json +0 -65
  303. package/.trajectories/completed/2026-01/traj_xy9vifpqet80.md +0 -37
  304. package/.trajectories/completed/2026-01/traj_y7aiwijyfmmv.json +0 -49
  305. package/.trajectories/completed/2026-01/traj_y7aiwijyfmmv.md +0 -31
  306. package/.trajectories/completed/2026-01/traj_y7n6hfbf7dmg.json +0 -49
  307. package/.trajectories/completed/2026-01/traj_y7n6hfbf7dmg.md +0 -31
  308. package/.trajectories/completed/2026-01/traj_ysjc8zaeqtd3.json +0 -47
  309. package/.trajectories/completed/2026-01/traj_ysjc8zaeqtd3.md +0 -32
  310. package/.trajectories/completed/2026-01/traj_yvdadtvdgnz3.json +0 -59
  311. package/.trajectories/completed/2026-01/traj_yvdadtvdgnz3.md +0 -37
  312. package/.trajectories/completed/2026-01/traj_yvfkwnkdiso2.json +0 -49
  313. package/.trajectories/completed/2026-01/traj_yvfkwnkdiso2.md +0 -31
  314. package/.trajectories/completed/2026-01/traj_z0vcw1wrzide.json +0 -53
  315. package/.trajectories/completed/2026-01/traj_z0vcw1wrzide.md +0 -32
  316. package/.trajectories/consolidate-settings-panel.md +0 -24
  317. package/.trajectories/gh-cli-user-token.md +0 -26
  318. package/.trajectories/index.json +0 -607
@@ -43,7 +43,7 @@ import { validateSshSecurityConfig } from './services/ssh-security.js';
43
43
  /**
44
44
  * Proxy a request to the user's primary running workspace
45
45
  */
46
- async function proxyToUserWorkspace(req, res, path) {
46
+ async function proxyToUserWorkspace(req, res, path, options) {
47
47
  const userId = req.session.userId;
48
48
  if (!userId) {
49
49
  res.status(401).json({ error: 'Unauthorized' });
@@ -59,12 +59,29 @@ async function proxyToUserWorkspace(req, res, path) {
59
59
  }
60
60
  // Proxy to workspace
61
61
  const targetUrl = `${runningWorkspace.publicUrl}${path}`;
62
- const proxyRes = await fetch(targetUrl);
62
+ console.log(`[workspace-proxy] ${options?.method || 'GET'} ${targetUrl}`);
63
+ const fetchOptions = {
64
+ method: options?.method || 'GET',
65
+ headers: { 'Content-Type': 'application/json' },
66
+ };
67
+ if (options?.body) {
68
+ fetchOptions.body = JSON.stringify(options.body);
69
+ }
70
+ const proxyRes = await fetch(targetUrl, fetchOptions);
71
+ const contentType = proxyRes.headers.get('content-type') || '';
72
+ console.log(`[workspace-proxy] Response: ${proxyRes.status} ${proxyRes.statusText}, content-type: ${contentType}`);
73
+ // Check if response is JSON
74
+ if (!contentType.includes('application/json')) {
75
+ const text = await proxyRes.text();
76
+ console.error(`[workspace-proxy] Non-JSON response: ${text.substring(0, 200)}`);
77
+ res.status(502).json({ error: 'Workspace returned non-JSON response', success: false });
78
+ return;
79
+ }
63
80
  const data = await proxyRes.json();
64
81
  res.status(proxyRes.status).json(data);
65
82
  }
66
83
  catch (error) {
67
- console.error('[trajectory-proxy] Error:', error);
84
+ console.error('[workspace-proxy] Error:', error);
68
85
  res.status(500).json({ error: 'Failed to proxy request to workspace', success: false });
69
86
  }
70
87
  }
@@ -183,6 +200,7 @@ export async function createServer() {
183
200
  '/api/auth/nango/webhook',
184
201
  '/api/auth/codex-helper/callback',
185
202
  '/api/admin/', // Admin API uses X-Admin-Secret header auth
203
+ '/api/channels/', // Channels API routes to local daemon, not cloud
186
204
  ];
187
205
  // Additional pattern for workspace proxy routes (contains /proxy/)
188
206
  const isWorkspaceProxyRoute = (path) => /^\/api\/workspaces\/[^/]+\/proxy\//.test(path);
@@ -262,17 +280,9 @@ export async function createServer() {
262
280
  app.use('/api/usage', usageRouter);
263
281
  app.use('/api/project-groups', coordinatorsRouter);
264
282
  app.use('/api/github-app', githubAppRouter);
265
- // Test helper routes (only available in non-production)
266
- // MUST be before teamsRouter to avoid auth interception
267
- if (process.env.NODE_ENV !== 'production') {
268
- app.use('/api/test', testHelpersRouter);
269
- console.log('[cloud] Test helper routes enabled (non-production mode)');
270
- }
271
- // Teams router - MUST BE LAST among /api routes
272
- // Handles /workspaces/:id/members and /invites with requireAuth on all routes
273
- app.use('/api', teamsRouter);
274
283
  // Trajectory proxy routes - auto-detect user's workspace and forward
275
284
  // These are convenience routes so the dashboard doesn't need to know the workspace ID
285
+ // MUST be before teamsRouter to avoid being caught by its catch-all
276
286
  app.get('/api/trajectory', requireAuth, async (req, res) => {
277
287
  await proxyToUserWorkspace(req, res, '/api/trajectory');
278
288
  });
@@ -285,6 +295,598 @@ export async function createServer() {
285
295
  app.get('/api/trajectory/history', requireAuth, async (req, res) => {
286
296
  await proxyToUserWorkspace(req, res, '/api/trajectory/history');
287
297
  });
298
+ // Channel proxy routes - forward to local dashboard-server (not workspace)
299
+ // Channels talk to the local daemon, so they need the local dashboard-server
300
+ // MUST be before teamsRouter to avoid being caught by its catch-all
301
+ // Auto-detect local dashboard URL if not configured
302
+ let localDashboardUrl = config.localDashboardUrl;
303
+ const defaultPorts = [3889, 3888, 3890]; // 3889 first (common alternate port)
304
+ async function detectLocalDashboard() {
305
+ console.log('[channel-proxy] Auto-detecting local dashboard...');
306
+ for (const port of defaultPorts) {
307
+ try {
308
+ const controller = new AbortController();
309
+ const timeout = setTimeout(() => controller.abort(), 2000);
310
+ const res = await fetch(`http://localhost:${port}/health`, {
311
+ method: 'GET',
312
+ signal: controller.signal,
313
+ });
314
+ clearTimeout(timeout);
315
+ if (res.ok) {
316
+ console.log(`[channel-proxy] Detected local dashboard at http://localhost:${port}`);
317
+ return `http://localhost:${port}`;
318
+ }
319
+ console.log(`[channel-proxy] Port ${port}: responded but not OK (${res.status})`);
320
+ }
321
+ catch (err) {
322
+ const msg = err instanceof Error ? err.message : String(err);
323
+ console.log(`[channel-proxy] Port ${port}: ${msg}`);
324
+ }
325
+ }
326
+ console.log('[channel-proxy] No local dashboard detected, using fallback');
327
+ return null;
328
+ }
329
+ // Detect at startup if not configured - use a promise to ensure detection completes before first use
330
+ let detectionPromise = null;
331
+ if (localDashboardUrl) {
332
+ console.log(`[channel-proxy] Using configured dashboard URL: ${localDashboardUrl}`);
333
+ }
334
+ else {
335
+ // Start detection immediately
336
+ detectionPromise = detectLocalDashboard().then((detected) => {
337
+ if (detected) {
338
+ localDashboardUrl = detected;
339
+ }
340
+ else {
341
+ localDashboardUrl = 'http://localhost:3889';
342
+ console.log(`[channel-proxy] Falling back to ${localDashboardUrl}`);
343
+ }
344
+ });
345
+ }
346
+ async function getLocalDashboardUrl() {
347
+ // Wait for detection to complete if it's in progress
348
+ if (detectionPromise) {
349
+ await detectionPromise;
350
+ detectionPromise = null;
351
+ }
352
+ // If still not set (shouldn't happen), detect now
353
+ if (!localDashboardUrl) {
354
+ const detected = await detectLocalDashboard();
355
+ localDashboardUrl = detected || 'http://localhost:3889';
356
+ }
357
+ return localDashboardUrl;
358
+ }
359
+ async function proxyToLocalDashboard(req, res, path, options) {
360
+ try {
361
+ const dashboardUrl = await getLocalDashboardUrl();
362
+ const targetUrl = `${dashboardUrl}${path}`;
363
+ console.log(`[channel-proxy] ${options?.method || 'GET'} ${targetUrl}`);
364
+ const fetchOptions = {
365
+ method: options?.method || 'GET',
366
+ headers: { 'Content-Type': 'application/json' },
367
+ };
368
+ if (options?.body) {
369
+ fetchOptions.body = JSON.stringify(options.body);
370
+ }
371
+ const proxyRes = await fetch(targetUrl, fetchOptions);
372
+ const contentType = proxyRes.headers.get('content-type') || '';
373
+ if (!contentType.includes('application/json')) {
374
+ const text = await proxyRes.text();
375
+ console.error(`[channel-proxy] Non-JSON response from ${targetUrl}: ${text.substring(0, 100)}`);
376
+ res.status(502).json({
377
+ error: 'Local dashboard not available or returned non-JSON response',
378
+ hint: 'Make sure the dashboard-server is running (agent-relay start)',
379
+ });
380
+ return;
381
+ }
382
+ const data = await proxyRes.json();
383
+ res.status(proxyRes.status).json(data);
384
+ }
385
+ catch (error) {
386
+ console.error('[channel-proxy] Error:', error);
387
+ res.status(502).json({
388
+ error: 'Failed to connect to local dashboard',
389
+ hint: 'Make sure the dashboard-server is running (agent-relay start)',
390
+ });
391
+ }
392
+ }
393
+ // =========================================================================
394
+ // Channel metadata endpoints (stored in cloud PostgreSQL)
395
+ // =========================================================================
396
+ /**
397
+ * GET /api/channels - List channels for a workspace
398
+ * Channels are workspace-scoped, not user-scoped
399
+ */
400
+ app.get('/api/channels', requireAuth, async (req, res) => {
401
+ try {
402
+ const workspaceId = req.query.workspaceId;
403
+ if (!workspaceId) {
404
+ return res.status(400).json({ error: 'workspaceId query param required' });
405
+ }
406
+ // Verify user has access to this workspace
407
+ const userId = req.session.userId;
408
+ const workspace = await db.workspaces.findById(workspaceId);
409
+ if (!workspace) {
410
+ return res.status(404).json({ error: 'Workspace not found' });
411
+ }
412
+ if (workspace.userId !== userId) {
413
+ const membership = await db.workspaceMembers.findMembership(workspaceId, userId);
414
+ if (!membership || !membership.acceptedAt) {
415
+ return res.status(403).json({ error: 'Access denied' });
416
+ }
417
+ }
418
+ const allChannels = await db.channels.findByWorkspaceId(workspaceId);
419
+ const activeChannels = allChannels.filter(c => c.status === 'active');
420
+ const archivedChannels = allChannels.filter(c => c.status === 'archived');
421
+ // Get member counts for all channels in one query
422
+ const channelUuids = allChannels.map(c => c.id);
423
+ const memberCounts = await db.channelMembers.countByChannelIds(channelUuids);
424
+ // Transform to API response format
425
+ const mapChannel = (c) => ({
426
+ id: c.channelId,
427
+ name: c.name,
428
+ description: c.description,
429
+ visibility: c.visibility,
430
+ status: c.status,
431
+ createdAt: c.createdAt.toISOString(),
432
+ createdBy: c.createdBy || '__system__',
433
+ lastActivityAt: c.lastActivityAt?.toISOString(),
434
+ memberCount: memberCounts.get(c.id) ?? 0,
435
+ unreadCount: 0,
436
+ hasMentions: false,
437
+ isDm: c.channelId.startsWith('dm:'),
438
+ });
439
+ res.json({
440
+ channels: activeChannels.map(mapChannel),
441
+ archivedChannels: archivedChannels.map(mapChannel),
442
+ });
443
+ }
444
+ catch (error) {
445
+ console.error('[channels] Error listing channels:', error);
446
+ res.status(500).json({ error: 'Failed to list channels' });
447
+ }
448
+ });
449
+ /**
450
+ * POST /api/channels - Create a new channel
451
+ */
452
+ app.post('/api/channels', requireAuth, express.json(), async (req, res) => {
453
+ try {
454
+ const { name, description, isPrivate, workspaceId, invites } = req.body;
455
+ if (!name || !workspaceId) {
456
+ return res.status(400).json({ error: 'name and workspaceId are required' });
457
+ }
458
+ // Verify user has access to this workspace
459
+ const userId = req.session.userId;
460
+ const workspace = await db.workspaces.findById(workspaceId);
461
+ if (!workspace) {
462
+ return res.status(404).json({ error: 'Workspace not found' });
463
+ }
464
+ if (workspace.userId !== userId) {
465
+ const membership = await db.workspaceMembers.findMembership(workspaceId, userId);
466
+ if (!membership || !membership.acceptedAt) {
467
+ return res.status(403).json({ error: 'Access denied' });
468
+ }
469
+ }
470
+ // Get creator username from session
471
+ const user = await db.users.findById(userId);
472
+ const createdBy = user?.githubUsername || 'unknown';
473
+ // Normalize channel name (remove # prefix if present)
474
+ const channelId = name.startsWith('#') ? name.slice(1) : name;
475
+ const displayName = channelId;
476
+ // Check if channel already exists
477
+ const existing = await db.channels.findByWorkspaceAndChannelId(workspaceId, channelId);
478
+ if (existing) {
479
+ return res.status(409).json({ error: 'Channel already exists' });
480
+ }
481
+ // Create the channel
482
+ console.log('[channels] Creating channel:', { workspaceId, channelId, displayName, createdBy });
483
+ let channel;
484
+ try {
485
+ channel = await db.channels.create({
486
+ workspaceId,
487
+ channelId,
488
+ name: displayName,
489
+ description,
490
+ visibility: isPrivate ? 'private' : 'public',
491
+ status: 'active',
492
+ createdBy,
493
+ });
494
+ console.log('[channels] Channel created:', channel.id);
495
+ }
496
+ catch (createError) {
497
+ const err = createError;
498
+ console.error('[channels] Failed to create channel in database:', {
499
+ message: err.message,
500
+ stack: err.stack,
501
+ });
502
+ throw createError;
503
+ }
504
+ // Add creator as owner
505
+ try {
506
+ await db.channelMembers.addMember({
507
+ channelId: channel.id,
508
+ memberId: createdBy,
509
+ memberType: 'user',
510
+ role: 'owner',
511
+ });
512
+ console.log('[channels] Added creator as owner:', createdBy);
513
+ }
514
+ catch (memberError) {
515
+ const err = memberError;
516
+ console.error('[channels] Failed to add channel member:', {
517
+ message: err.message,
518
+ stack: err.stack,
519
+ channelId: channel.id,
520
+ memberId: createdBy,
521
+ });
522
+ throw memberError;
523
+ }
524
+ // Handle invites if provided
525
+ // Supports: comma-separated string, array of strings, or array of {id, type} objects
526
+ const addedMembers = [
527
+ { id: createdBy, type: 'user', role: 'owner' },
528
+ ];
529
+ const memberWarnings = [];
530
+ if (invites) {
531
+ let inviteList = [];
532
+ if (typeof invites === 'string') {
533
+ // Comma-separated string: "alice,bob" -> all as users
534
+ inviteList = invites.split(',')
535
+ .map((s) => s.trim())
536
+ .filter(Boolean)
537
+ .map(id => ({ id, type: 'user' }));
538
+ }
539
+ else if (Array.isArray(invites)) {
540
+ // Array of strings or objects
541
+ inviteList = invites.map((inv) => {
542
+ if (typeof inv === 'string') {
543
+ return { id: inv, type: 'user' };
544
+ }
545
+ return {
546
+ id: inv.id,
547
+ type: (inv.type === 'agent' ? 'agent' : 'user'),
548
+ };
549
+ });
550
+ }
551
+ for (const invitee of inviteList) {
552
+ await db.channelMembers.addMember({
553
+ channelId: channel.id,
554
+ memberId: invitee.id,
555
+ memberType: invitee.type,
556
+ role: 'member',
557
+ invitedBy: createdBy,
558
+ });
559
+ addedMembers.push({ id: invitee.id, type: invitee.type, role: 'member' });
560
+ // For agent members, sync to local daemon's in-memory channel membership
561
+ if (invitee.type === 'agent') {
562
+ try {
563
+ const channelName = channelId.startsWith('#') ? channelId : `#${channelId}`;
564
+ // Route to local dashboard where the daemon and channel routing lives
565
+ const dashboardUrl = await getLocalDashboardUrl();
566
+ const joinResponse = await fetch(`${dashboardUrl}/api/channels/admin-join`, {
567
+ method: 'POST',
568
+ headers: { 'Content-Type': 'application/json' },
569
+ body: JSON.stringify({ channel: channelName, member: invitee.id, workspaceId }),
570
+ });
571
+ const joinResult = await joinResponse.json();
572
+ console.log(`[channels] Synced agent ${invitee.id} to channel ${channelName} via local dashboard`);
573
+ // Check for warning about unconnected agent
574
+ if (joinResult.warning) {
575
+ memberWarnings.push({ member: invitee.id, warning: joinResult.warning });
576
+ console.log(`[channels] Warning for ${invitee.id}: ${joinResult.warning}`);
577
+ }
578
+ }
579
+ catch (err) {
580
+ // Non-fatal - daemon sync is best-effort
581
+ console.warn(`[channels] Failed to sync agent ${invitee.id} to daemon:`, err);
582
+ }
583
+ }
584
+ }
585
+ }
586
+ res.status(201).json({
587
+ success: true,
588
+ channel: {
589
+ id: channel.channelId,
590
+ name: channel.name,
591
+ description: channel.description,
592
+ visibility: channel.visibility,
593
+ status: channel.status,
594
+ createdAt: channel.createdAt.toISOString(),
595
+ createdBy: channel.createdBy,
596
+ members: addedMembers,
597
+ },
598
+ warnings: memberWarnings.length > 0 ? memberWarnings : undefined,
599
+ });
600
+ }
601
+ catch (error) {
602
+ const err = error;
603
+ console.error('[channels] Error creating channel:', {
604
+ message: err.message,
605
+ stack: err.stack,
606
+ name: err.name,
607
+ workspaceId: req.body.workspaceId,
608
+ channelName: req.body.name,
609
+ });
610
+ // Include error message for debugging (safe since this is authenticated)
611
+ res.status(500).json({
612
+ error: 'Failed to create channel',
613
+ message: err.message,
614
+ });
615
+ }
616
+ });
617
+ /**
618
+ * POST /api/channels/join - Join a channel
619
+ */
620
+ app.post('/api/channels/join', requireAuth, express.json(), async (req, res) => {
621
+ try {
622
+ const { channel: rawChannelId, workspaceId, username } = req.body;
623
+ if (!rawChannelId || !workspaceId) {
624
+ return res.status(400).json({ error: 'channel and workspaceId are required' });
625
+ }
626
+ // Normalize channel ID (remove # prefix if present)
627
+ const channelId = rawChannelId.startsWith('#') ? rawChannelId.slice(1) : rawChannelId;
628
+ const userId = req.session.userId;
629
+ const user = await db.users.findById(userId);
630
+ const memberId = username || user?.githubUsername || 'unknown';
631
+ // Find the channel
632
+ const channel = await db.channels.findByWorkspaceAndChannelId(workspaceId, channelId);
633
+ if (!channel) {
634
+ return res.status(404).json({ error: 'Channel not found' });
635
+ }
636
+ // Check if already a member
637
+ const existing = await db.channelMembers.findMembership(channel.id, memberId);
638
+ if (!existing) {
639
+ await db.channelMembers.addMember({
640
+ channelId: channel.id,
641
+ memberId,
642
+ memberType: 'user',
643
+ role: 'member',
644
+ });
645
+ }
646
+ // Also subscribe the user on the daemon side for real-time messages
647
+ try {
648
+ const dashboardUrl = await getLocalDashboardUrl();
649
+ const channelWithHash = rawChannelId.startsWith('#') ? rawChannelId : `#${rawChannelId}`;
650
+ await fetch(`${dashboardUrl}/api/channels/subscribe`, {
651
+ method: 'POST',
652
+ headers: { 'Content-Type': 'application/json' },
653
+ body: JSON.stringify({
654
+ username: memberId,
655
+ channels: [channelWithHash],
656
+ workspaceId,
657
+ }),
658
+ });
659
+ console.log(`[cloud] Subscribed ${memberId} to ${channelWithHash} on local daemon`);
660
+ }
661
+ catch (err) {
662
+ // Non-fatal - daemon sync is best-effort
663
+ console.warn(`[cloud] Failed to sync join to daemon:`, err);
664
+ }
665
+ res.json({ success: true, channel: channelId });
666
+ }
667
+ catch (error) {
668
+ console.error('[channels] Error joining channel:', error);
669
+ res.status(500).json({ error: 'Failed to join channel' });
670
+ }
671
+ });
672
+ /**
673
+ * POST /api/channels/leave - Leave a channel
674
+ */
675
+ app.post('/api/channels/leave', requireAuth, express.json(), async (req, res) => {
676
+ try {
677
+ const { channel: rawChannelId, workspaceId, username } = req.body;
678
+ if (!rawChannelId || !workspaceId) {
679
+ return res.status(400).json({ error: 'channel and workspaceId are required' });
680
+ }
681
+ // Normalize channel ID (remove # prefix if present)
682
+ const channelId = rawChannelId.startsWith('#') ? rawChannelId.slice(1) : rawChannelId;
683
+ const userId = req.session.userId;
684
+ const user = await db.users.findById(userId);
685
+ const memberId = username || user?.githubUsername || 'unknown';
686
+ const channel = await db.channels.findByWorkspaceAndChannelId(workspaceId, channelId);
687
+ if (!channel) {
688
+ return res.status(404).json({ error: 'Channel not found' });
689
+ }
690
+ await db.channelMembers.removeMember(channel.id, memberId);
691
+ res.json({ success: true, channel: channelId });
692
+ }
693
+ catch (error) {
694
+ console.error('[channels] Error leaving channel:', error);
695
+ res.status(500).json({ error: 'Failed to leave channel' });
696
+ }
697
+ });
698
+ /**
699
+ * POST /api/channels/invite - Invite users to a channel
700
+ */
701
+ app.post('/api/channels/invite', requireAuth, express.json(), async (req, res) => {
702
+ try {
703
+ const { channel: rawChannelId, workspaceId, invites, invitedBy } = req.body;
704
+ if (!rawChannelId || !workspaceId || !invites) {
705
+ return res.status(400).json({ error: 'channel, workspaceId, and invites are required' });
706
+ }
707
+ // Normalize channel ID (remove # prefix if present)
708
+ const channelId = rawChannelId.startsWith('#') ? rawChannelId.slice(1) : rawChannelId;
709
+ const channel = await db.channels.findByWorkspaceAndChannelId(workspaceId, channelId);
710
+ if (!channel) {
711
+ return res.status(404).json({ error: 'Channel not found' });
712
+ }
713
+ const inviteList = typeof invites === 'string'
714
+ ? invites.split(',').map((s) => s.trim()).filter(Boolean)
715
+ : invites;
716
+ const results = [];
717
+ for (const invitee of inviteList) {
718
+ const existing = await db.channelMembers.findMembership(channel.id, invitee);
719
+ if (!existing) {
720
+ await db.channelMembers.addMember({
721
+ channelId: channel.id,
722
+ memberId: invitee,
723
+ memberType: 'user',
724
+ role: 'member',
725
+ invitedBy,
726
+ });
727
+ results.push({ username: invitee, success: true });
728
+ }
729
+ else {
730
+ results.push({ username: invitee, success: true, reason: 'already_member' });
731
+ }
732
+ }
733
+ res.json({ channel: channelId, invited: results });
734
+ }
735
+ catch (error) {
736
+ console.error('[channels] Error inviting to channel:', error);
737
+ res.status(500).json({ error: 'Failed to invite to channel' });
738
+ }
739
+ });
740
+ /**
741
+ * POST /api/channels/archive - Archive a channel
742
+ */
743
+ app.post('/api/channels/archive', requireAuth, express.json(), async (req, res) => {
744
+ try {
745
+ const { channel: rawChannelId, workspaceId } = req.body;
746
+ if (!rawChannelId || !workspaceId) {
747
+ return res.status(400).json({ error: 'channel and workspaceId are required' });
748
+ }
749
+ // Normalize channel ID (remove # prefix if present)
750
+ const channelId = rawChannelId.startsWith('#') ? rawChannelId.slice(1) : rawChannelId;
751
+ const channel = await db.channels.findByWorkspaceAndChannelId(workspaceId, channelId);
752
+ if (!channel) {
753
+ return res.status(404).json({ error: 'Channel not found' });
754
+ }
755
+ await db.channels.archive(channel.id);
756
+ res.json({ success: true, channel: channelId, status: 'archived' });
757
+ }
758
+ catch (error) {
759
+ console.error('[channels] Error archiving channel:', error);
760
+ res.status(500).json({ error: 'Failed to archive channel' });
761
+ }
762
+ });
763
+ /**
764
+ * POST /api/channels/unarchive - Unarchive a channel
765
+ */
766
+ app.post('/api/channels/unarchive', requireAuth, express.json(), async (req, res) => {
767
+ try {
768
+ const { channel: rawChannelId, workspaceId } = req.body;
769
+ if (!rawChannelId || !workspaceId) {
770
+ return res.status(400).json({ error: 'channel and workspaceId are required' });
771
+ }
772
+ // Normalize channel ID (remove # prefix if present)
773
+ const channelId = rawChannelId.startsWith('#') ? rawChannelId.slice(1) : rawChannelId;
774
+ const channel = await db.channels.findByWorkspaceAndChannelId(workspaceId, channelId);
775
+ if (!channel) {
776
+ return res.status(404).json({ error: 'Channel not found' });
777
+ }
778
+ await db.channels.unarchive(channel.id);
779
+ res.json({ success: true, channel: channelId, status: 'active' });
780
+ }
781
+ catch (error) {
782
+ console.error('[channels] Error unarchiving channel:', error);
783
+ res.status(500).json({ error: 'Failed to unarchive channel' });
784
+ }
785
+ });
786
+ // =========================================================================
787
+ // Channel message endpoints (proxied to workspace container)
788
+ // Messages are stored in the daemon's SQLite for real-time performance
789
+ // =========================================================================
790
+ app.post('/api/channels/message', requireAuth, express.json(), async (req, res) => {
791
+ // Route to local dashboard where relay daemon and channel routing lives
792
+ await proxyToLocalDashboard(req, res, '/api/channels/message', { method: 'POST', body: req.body });
793
+ });
794
+ app.get('/api/channels/:channel/messages', requireAuth, async (req, res) => {
795
+ const channel = encodeURIComponent(req.params.channel);
796
+ const params = new URLSearchParams();
797
+ if (req.query.limit)
798
+ params.set('limit', req.query.limit);
799
+ if (req.query.before)
800
+ params.set('before', req.query.before);
801
+ const queryString = params.toString() ? `?${params.toString()}` : '';
802
+ await proxyToLocalDashboard(req, res, `/api/channels/${channel}/messages${queryString}`);
803
+ });
804
+ /**
805
+ * GET /api/channels/:channel/members - Get members of a channel
806
+ */
807
+ app.get('/api/channels/:channel/members', requireAuth, async (req, res) => {
808
+ const channel = encodeURIComponent(req.params.channel);
809
+ await proxyToLocalDashboard(req, res, `/api/channels/${channel}/members`);
810
+ });
811
+ /**
812
+ * GET /api/channels/available-members - Get available members for channel invites
813
+ * Returns workspace members (humans) and agents from linked daemons
814
+ */
815
+ app.get('/api/channels/available-members', requireAuth, async (req, res) => {
816
+ try {
817
+ const userId = req.session.userId;
818
+ const workspaceId = req.query.workspaceId;
819
+ // Get workspace ID - either from query param or user's default workspace
820
+ let targetWorkspaceId = workspaceId;
821
+ if (!targetWorkspaceId) {
822
+ // Find user's default or first workspace
823
+ const memberships = await db.workspaceMembers.findByUserId(userId);
824
+ if (memberships.length > 0) {
825
+ targetWorkspaceId = memberships[0].workspaceId;
826
+ }
827
+ }
828
+ if (!targetWorkspaceId) {
829
+ return res.json({ members: [], agents: [] });
830
+ }
831
+ // Verify user has access to this workspace
832
+ const canView = await db.workspaceMembers.canView(targetWorkspaceId, userId);
833
+ if (!canView) {
834
+ const workspace = await db.workspaces.findById(targetWorkspaceId);
835
+ if (!workspace || workspace.userId !== userId) {
836
+ return res.status(403).json({ error: 'Access denied' });
837
+ }
838
+ }
839
+ // Get workspace members (humans)
840
+ const workspaceMembers = await db.workspaceMembers.findByWorkspaceId(targetWorkspaceId);
841
+ const members = await Promise.all(workspaceMembers.map(async (m) => {
842
+ const user = await db.users.findById(m.userId);
843
+ return {
844
+ id: user?.githubUsername || m.userId,
845
+ displayName: user?.githubUsername || 'Unknown',
846
+ type: 'user',
847
+ avatarUrl: user?.avatarUrl ?? undefined,
848
+ };
849
+ }));
850
+ // Get agents from linked daemons for this workspace
851
+ const daemons = await db.linkedDaemons.findByWorkspaceId(targetWorkspaceId);
852
+ const agents = [];
853
+ for (const daemon of daemons) {
854
+ const metadata = daemon.metadata;
855
+ const daemonAgents = metadata?.agents || [];
856
+ for (const agent of daemonAgents) {
857
+ // Skip human users from daemon agent list (they're in workspace members)
858
+ if (agent.isHuman)
859
+ continue;
860
+ // Avoid duplicates
861
+ if (!agents.some((a) => a.id === agent.name)) {
862
+ agents.push({
863
+ id: agent.name,
864
+ displayName: agent.name,
865
+ type: 'agent',
866
+ status: agent.status,
867
+ });
868
+ }
869
+ }
870
+ }
871
+ res.json({ members, agents });
872
+ }
873
+ catch (error) {
874
+ console.error('[channels] Error getting available members:', error);
875
+ res.status(500).json({ error: 'Failed to get available members' });
876
+ }
877
+ });
878
+ app.get('/api/channels/users', requireAuth, async (req, res) => {
879
+ await proxyToLocalDashboard(req, res, '/api/channels/users');
880
+ });
881
+ // Test helper routes (only available in non-production)
882
+ // MUST be before teamsRouter to avoid auth interception
883
+ if (process.env.NODE_ENV !== 'production') {
884
+ app.use('/api/test', testHelpersRouter);
885
+ console.log('[cloud] Test helper routes enabled (non-production mode)');
886
+ }
887
+ // Teams router - MUST BE LAST among /api routes
888
+ // Handles /workspaces/:id/members and /invites with requireAuth on all routes
889
+ app.use('/api', teamsRouter);
288
890
  // Serve static dashboard files (Next.js static export)
289
891
  // Path: dist/cloud/server.js -> ../../src/dashboard/out
290
892
  const dashboardPath = path.join(__dirname, '../../src/dashboard/out');
@@ -360,21 +962,25 @@ export async function createServer() {
360
962
  };
361
963
  // WebSocket server for agent logs (proxied to workspace daemon)
362
964
  const wssLogs = new WebSocketServer({ noServer: true, perMessageDeflate: false });
965
+ // WebSocket server for channel messages (proxied to workspace daemon)
966
+ const wssChannels = new WebSocketServer({ noServer: true, perMessageDeflate: false });
363
967
  // Handle agent logs WebSocket connections
364
968
  wssLogs.on('connection', async (clientWs, workspaceId, agentName) => {
365
969
  console.log(`[ws/logs] Client connected for workspace=${workspaceId} agent=${agentName}`);
366
970
  let daemonWs = null;
367
971
  try {
368
- // Find the workspace
972
+ // Find the workspace (needed to verify it exists and get its URL)
369
973
  const workspace = await db.workspaces.findById(workspaceId);
370
- if (!workspace || !workspace.publicUrl) {
371
- clientWs.send(JSON.stringify({ type: 'error', message: 'Workspace not found or not running' }));
974
+ if (!workspace) {
975
+ clientWs.send(JSON.stringify({ type: 'error', message: 'Workspace not found' }));
372
976
  clientWs.close();
373
977
  return;
374
978
  }
375
- // Connect to workspace daemon WebSocket
376
- // The workspace runs the dashboard server which expects /ws/logs path
377
- const baseUrl = workspace.publicUrl.replace(/^http/, 'ws').replace(/\/$/, '');
979
+ // Connect to the workspace's dashboard where the agent was spawned
980
+ // IMPORTANT: Must use workspace.publicUrl (not getLocalDashboardUrl) because
981
+ // agents are spawned on the workspace server, so logs must connect there too
982
+ const dashboardUrl = workspace.publicUrl || await getLocalDashboardUrl();
983
+ const baseUrl = dashboardUrl.replace(/^http/, 'ws').replace(/\/$/, '');
378
984
  const daemonWsUrl = `${baseUrl}/ws/logs/${encodeURIComponent(agentName)}`;
379
985
  console.log(`[ws/logs] Connecting to daemon: ${daemonWsUrl}`);
380
986
  daemonWs = new WebSocket(daemonWsUrl, { perMessageDeflate: false });
@@ -429,6 +1035,101 @@ export async function createServer() {
429
1035
  }
430
1036
  }
431
1037
  });
1038
+ // Handle channel WebSocket connections (proxied to workspace daemon)
1039
+ // This allows cloud users to receive real-time channel messages
1040
+ wssChannels.on('connection', async (clientWs, workspaceId, username) => {
1041
+ console.log(`[ws/channels] Client connected for workspace=${workspaceId} user=${username}`);
1042
+ let daemonWs = null;
1043
+ try {
1044
+ // Find the workspace (needed to verify it exists)
1045
+ const workspace = await db.workspaces.findById(workspaceId);
1046
+ if (!workspace) {
1047
+ clientWs.send(JSON.stringify({ type: 'error', message: 'Workspace not found' }));
1048
+ clientWs.close();
1049
+ return;
1050
+ }
1051
+ // Connect to local dashboard where the daemon actually runs
1052
+ const dashboardUrl = await getLocalDashboardUrl();
1053
+ const baseUrl = dashboardUrl.replace(/^http/, 'ws').replace(/\/$/, '');
1054
+ const daemonWsUrl = `${baseUrl}/ws/presence`;
1055
+ console.log(`[ws/channels] Connecting to daemon: ${daemonWsUrl}`);
1056
+ daemonWs = new WebSocket(daemonWsUrl, { perMessageDeflate: false });
1057
+ daemonWs.on('open', () => {
1058
+ console.log(`[ws/channels] Connected to daemon for ${username}`);
1059
+ // Register with the daemon's presence system
1060
+ daemonWs.send(JSON.stringify({
1061
+ type: 'presence',
1062
+ action: 'join',
1063
+ user: { username },
1064
+ }));
1065
+ });
1066
+ daemonWs.on('message', (data) => {
1067
+ // Forward daemon messages to client
1068
+ // Only forward channel_message type messages for this user
1069
+ try {
1070
+ const msg = JSON.parse(data.toString());
1071
+ if (msg.type === 'channel_message') {
1072
+ // Only forward if this message is for this user
1073
+ if (msg.targetUser === username) {
1074
+ console.log(`[ws/channels] Forwarding channel message to ${username}: ${msg.from} -> ${msg.channel}`);
1075
+ clientWs.send(data.toString());
1076
+ }
1077
+ }
1078
+ // Also forward presence updates so client stays in sync
1079
+ if (msg.type === 'presence_join' || msg.type === 'presence_leave' || msg.type === 'presence_list') {
1080
+ clientWs.send(data.toString());
1081
+ }
1082
+ }
1083
+ catch {
1084
+ // Non-JSON message, skip
1085
+ }
1086
+ });
1087
+ daemonWs.on('close', () => {
1088
+ console.log(`[ws/channels] Daemon connection closed for ${username}`);
1089
+ if (clientWs.readyState === WebSocket.OPEN) {
1090
+ clientWs.close();
1091
+ }
1092
+ });
1093
+ daemonWs.on('error', (err) => {
1094
+ console.error(`[ws/channels] Daemon WebSocket error:`, err);
1095
+ if (clientWs.readyState === WebSocket.OPEN) {
1096
+ clientWs.send(JSON.stringify({ type: 'error', message: 'Daemon connection error' }));
1097
+ clientWs.close();
1098
+ }
1099
+ });
1100
+ // Forward client messages to daemon (for sending channel messages)
1101
+ clientWs.on('message', (data) => {
1102
+ if (daemonWs && daemonWs.readyState === WebSocket.OPEN) {
1103
+ daemonWs.send(data.toString());
1104
+ }
1105
+ });
1106
+ clientWs.on('close', () => {
1107
+ console.log(`[ws/channels] Client disconnected for ${username}`);
1108
+ // Send leave message to daemon
1109
+ if (daemonWs && daemonWs.readyState === WebSocket.OPEN) {
1110
+ daemonWs.send(JSON.stringify({
1111
+ type: 'presence',
1112
+ action: 'leave',
1113
+ username,
1114
+ }));
1115
+ daemonWs.close();
1116
+ }
1117
+ });
1118
+ clientWs.on('error', (err) => {
1119
+ console.error(`[ws/channels] Client WebSocket error:`, err);
1120
+ if (daemonWs && daemonWs.readyState === WebSocket.OPEN) {
1121
+ daemonWs.close();
1122
+ }
1123
+ });
1124
+ }
1125
+ catch (err) {
1126
+ console.error(`[ws/channels] Setup error:`, err);
1127
+ if (clientWs.readyState === WebSocket.OPEN) {
1128
+ clientWs.send(JSON.stringify({ type: 'error', message: 'Failed to connect to workspace' }));
1129
+ clientWs.close();
1130
+ }
1131
+ }
1132
+ });
432
1133
  // Handle HTTP upgrade for WebSocket
433
1134
  httpServer.on('upgrade', (request, socket, head) => {
434
1135
  const pathname = new URL(request.url || '', `http://${request.headers.host}`).pathname;
@@ -451,6 +1152,20 @@ export async function createServer() {
451
1152
  socket.destroy();
452
1153
  }
453
1154
  }
1155
+ else if (pathname.startsWith('/ws/channels/')) {
1156
+ // Parse /ws/channels/:workspaceId/:username
1157
+ const parts = pathname.split('/').filter(Boolean);
1158
+ if (parts.length >= 4) {
1159
+ const workspaceId = decodeURIComponent(parts[2]);
1160
+ const username = decodeURIComponent(parts[3]);
1161
+ wssChannels.handleUpgrade(request, socket, head, (ws) => {
1162
+ wssChannels.emit('connection', ws, workspaceId, username);
1163
+ });
1164
+ }
1165
+ else {
1166
+ socket.destroy();
1167
+ }
1168
+ }
454
1169
  else {
455
1170
  // Unknown WebSocket path - destroy socket
456
1171
  socket.destroy();
@@ -499,6 +1214,96 @@ export async function createServer() {
499
1214
  wssPresence.on('close', () => {
500
1215
  clearInterval(presenceHeartbeat);
501
1216
  });
1217
+ // Track daemon proxy connections for channel message forwarding
1218
+ const daemonProxies = new Map(); // clientWs -> workspaceId -> daemonWs
1219
+ // Set up daemon proxy for channel messages
1220
+ async function setupDaemonChannelProxy(clientWs, workspaceId, username) {
1221
+ // Check if already have a proxy for this workspace
1222
+ const clientProxies = daemonProxies.get(clientWs) || new Map();
1223
+ if (clientProxies.has(workspaceId)) {
1224
+ return; // Already connected
1225
+ }
1226
+ try {
1227
+ const workspace = await db.workspaces.findById(workspaceId);
1228
+ if (!workspace) {
1229
+ console.log(`[cloud] Workspace ${workspaceId} not found`);
1230
+ return;
1231
+ }
1232
+ // Use local dashboard URL where the daemon actually runs
1233
+ const dashboardUrl = await getLocalDashboardUrl();
1234
+ const daemonWsUrl = dashboardUrl.replace(/^http/, 'ws').replace(/\/$/, '') + '/ws/presence';
1235
+ console.log(`[cloud] Connecting channel proxy to daemon: ${daemonWsUrl} for ${username}`);
1236
+ // First, register the user for channel messages on the daemon side
1237
+ // This creates a relay client for them so they receive channel messages
1238
+ try {
1239
+ const subscribeRes = await fetch(`${dashboardUrl}/api/channels/subscribe`, {
1240
+ method: 'POST',
1241
+ headers: { 'Content-Type': 'application/json' },
1242
+ body: JSON.stringify({
1243
+ username,
1244
+ channels: ['#general'], // Start with general, others can be joined later
1245
+ workspaceId,
1246
+ }),
1247
+ });
1248
+ if (subscribeRes.ok) {
1249
+ const result = (await subscribeRes.json());
1250
+ console.log(`[cloud] Subscribed ${username} to channels: ${result.channels?.join(', ')}`);
1251
+ }
1252
+ else {
1253
+ console.warn(`[cloud] Failed to subscribe ${username} to channels: ${subscribeRes.status}`);
1254
+ }
1255
+ }
1256
+ catch (err) {
1257
+ console.warn(`[cloud] Error subscribing ${username} to channels:`, err);
1258
+ // Continue anyway - we can still set up the proxy
1259
+ }
1260
+ const daemonWs = new WebSocket(daemonWsUrl, { perMessageDeflate: false });
1261
+ daemonWs.on('open', () => {
1262
+ console.log(`[cloud] Channel proxy connected for ${username} in workspace ${workspaceId}`);
1263
+ });
1264
+ daemonWs.on('message', (data) => {
1265
+ try {
1266
+ const msg = JSON.parse(data.toString());
1267
+ // Forward channel messages targeted at this user
1268
+ if (msg.type === 'channel_message' && msg.targetUser === username) {
1269
+ console.log(`[cloud] Forwarding channel message to ${username}: ${msg.from} -> ${msg.channel}`);
1270
+ if (clientWs.readyState === WebSocket.OPEN) {
1271
+ clientWs.send(data.toString());
1272
+ }
1273
+ }
1274
+ }
1275
+ catch {
1276
+ // Non-JSON, ignore
1277
+ }
1278
+ });
1279
+ daemonWs.on('close', () => {
1280
+ console.log(`[cloud] Channel proxy closed for ${username} in workspace ${workspaceId}`);
1281
+ clientProxies.delete(workspaceId);
1282
+ });
1283
+ daemonWs.on('error', (err) => {
1284
+ console.error(`[cloud] Channel proxy error for ${username}:`, err);
1285
+ clientProxies.delete(workspaceId);
1286
+ });
1287
+ clientProxies.set(workspaceId, daemonWs);
1288
+ daemonProxies.set(clientWs, clientProxies);
1289
+ }
1290
+ catch (err) {
1291
+ console.error(`[cloud] Failed to setup channel proxy for ${username}:`, err);
1292
+ }
1293
+ }
1294
+ // Clean up daemon proxies for a client
1295
+ function cleanupDaemonProxies(clientWs) {
1296
+ const clientProxies = daemonProxies.get(clientWs);
1297
+ if (clientProxies) {
1298
+ for (const [workspaceId, daemonWs] of clientProxies) {
1299
+ console.log(`[cloud] Cleaning up channel proxy for workspace ${workspaceId}`);
1300
+ if (daemonWs.readyState === WebSocket.OPEN) {
1301
+ daemonWs.close();
1302
+ }
1303
+ }
1304
+ daemonProxies.delete(clientWs);
1305
+ }
1306
+ }
502
1307
  // Handle presence connections
503
1308
  wssPresence.on('connection', (ws) => {
504
1309
  // Initialize health tracking (no log - too noisy)
@@ -582,12 +1387,43 @@ export async function createServer() {
582
1387
  isTyping: msg.isTyping,
583
1388
  }, ws);
584
1389
  }
1390
+ else if (msg.type === 'subscribe_channels') {
1391
+ // Subscribe to channel messages for a specific workspace
1392
+ if (!clientUsername) {
1393
+ console.warn(`[cloud] subscribe_channels from unauthenticated client`);
1394
+ return;
1395
+ }
1396
+ if (!msg.workspaceId || typeof msg.workspaceId !== 'string') {
1397
+ console.warn(`[cloud] subscribe_channels missing workspaceId`);
1398
+ return;
1399
+ }
1400
+ console.log(`[cloud] User ${clientUsername} subscribing to channels in workspace ${msg.workspaceId}`);
1401
+ setupDaemonChannelProxy(ws, msg.workspaceId, clientUsername).catch((err) => {
1402
+ console.error(`[cloud] Failed to setup channel subscription:`, err);
1403
+ });
1404
+ }
1405
+ else if (msg.type === 'channel_message') {
1406
+ // Proxy channel message to daemon via HTTP API
1407
+ if (!clientUsername) {
1408
+ console.warn(`[cloud] channel_message from unauthenticated client`);
1409
+ return;
1410
+ }
1411
+ if (!msg.channel || !msg.body) {
1412
+ console.warn(`[cloud] channel_message missing channel or body`);
1413
+ return;
1414
+ }
1415
+ // Note: This should be handled by the HTTP API, but support WebSocket too
1416
+ console.log(`[cloud] Channel message via WebSocket from ${clientUsername} to ${msg.channel}`);
1417
+ // The HTTP proxy will handle actual sending - just log for now
1418
+ }
585
1419
  }
586
1420
  catch (err) {
587
1421
  console.error('[cloud] Invalid presence message:', err);
588
1422
  }
589
1423
  });
590
1424
  ws.on('close', () => {
1425
+ // Clean up daemon proxies
1426
+ cleanupDaemonProxies(ws);
591
1427
  if (clientUsername) {
592
1428
  const userState = onlineUsers.get(clientUsername);
593
1429
  if (userState) {