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.
- package/README.md +130 -158
- package/bin/relay-pty +0 -0
- package/bin/relay-pty-darwin-arm64 +0 -0
- package/bin/relay-pty-darwin-x64 +0 -0
- package/bin/relay-pty-linux-x64 +0 -0
- package/deploy/workspace/entrypoint.sh +9 -0
- package/dist/bridge/spawner.d.ts +4 -4
- package/dist/bridge/spawner.js +58 -92
- package/dist/cli/index.d.ts +8 -6
- package/dist/cli/index.js +282 -47
- package/dist/cloud/api/daemons.js +13 -32
- package/dist/cloud/api/onboarding.js +2 -4
- package/dist/cloud/api/providers.js +6 -0
- package/dist/cloud/config.d.ts +1 -0
- package/dist/cloud/config.js +2 -0
- package/dist/cloud/db/bulk-ingest.d.ts +2 -1
- package/dist/cloud/db/drizzle.d.ts +21 -26
- package/dist/cloud/db/drizzle.js +87 -100
- package/dist/cloud/db/index.d.ts +6 -5
- package/dist/cloud/db/index.js +9 -8
- package/dist/cloud/db/schema.d.ts +1049 -1076
- package/dist/cloud/db/schema.js +59 -71
- package/dist/cloud/server.js +854 -18
- package/dist/cloud/services/persistence.d.ts +15 -15
- package/dist/cloud/services/persistence.js +14 -14
- package/dist/daemon/agent-manager.d.ts +6 -5
- package/dist/daemon/agent-manager.js +12 -8
- package/dist/daemon/channel-membership-store.d.ts +48 -0
- package/dist/daemon/channel-membership-store.js +149 -0
- package/dist/daemon/cloud-sync.d.ts +2 -0
- package/dist/daemon/cloud-sync.js +4 -0
- package/dist/daemon/connection.js +17 -9
- package/dist/daemon/router.d.ts +37 -0
- package/dist/daemon/router.js +318 -79
- package/dist/daemon/server.d.ts +15 -0
- package/dist/daemon/server.js +141 -3
- package/dist/dashboard/out/404.html +1 -0
- package/dist/dashboard/out/_next/static/IxxVRv94L1w3ReRGAiI-k/_buildManifest.js +1 -0
- package/dist/dashboard/out/_next/static/IxxVRv94L1w3ReRGAiI-k/_ssgManifest.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/116-eacf84a131b80db9.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/117-c8afed19e821a35d.js +2 -0
- package/dist/dashboard/out/_next/static/chunks/282-980c2eb8fff20123.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/532-bace199897eeab37.js +9 -0
- package/dist/dashboard/out/_next/static/chunks/64-87ab9cd6bcf2f737.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/648-acb2ff9f77cbfbd3.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/766-aa7c8c9900ff5f53.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/83-4f08122d4e7e79a6.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/847-f1f467060f32afff.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/891-a024fbe4b619cf6f.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/app/_not-found/page-60501fddbafba9dc.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/app/app/onboarding/page-f746f29e01fffc43.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/app/app/page-ffad986adfcc8b31.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/app/cloud/link/page-cfeb437f08a12ed9.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/app/connect-repos/page-03ac6f35a6654ea6.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/app/history/page-240f91e8b06ba8ac.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/app/layout-c0d118c0f92d969c.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/app/login/page-6ec54eee75877971.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/app/metrics/page-82938ab8fcf44694.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/app/page-671037943b2f2e43.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/app/pricing/page-0efa024c28ba4597.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/app/providers/page-57cbd738c6a73859.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/app/providers/setup/[provider]/page-5ab0854472b402b0.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/app/signup/page-18a4665665f6be11.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/e868780c-48e5f147c90a3a41.js +18 -0
- package/dist/dashboard/out/_next/static/chunks/fd9d1056-609918ca7b6280bb.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/framework-f66176bb897dc684.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/main-5a40a5ae29646e1b.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/main-app-6e8e8d3ef4e0192a.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/pages/_app-72b849fbd24ac258.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/pages/_error-7ba65e1336b92748.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/polyfills-42372ed130431b0a.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/webpack-1cdd8ed57114d5e1.js +1 -0
- package/dist/dashboard/out/_next/static/css/4034f236dd1a3178.css +1 -0
- package/dist/dashboard/out/_next/static/css/8f9ed310f454e5a5.css +1 -0
- package/dist/dashboard/out/alt-logos/agent-relay-logo-128.png +0 -0
- package/dist/dashboard/out/alt-logos/agent-relay-logo-256.png +0 -0
- package/dist/dashboard/out/alt-logos/agent-relay-logo-32.png +0 -0
- package/dist/dashboard/out/alt-logos/agent-relay-logo-512.png +0 -0
- package/dist/dashboard/out/alt-logos/agent-relay-logo-64.png +0 -0
- package/dist/dashboard/out/alt-logos/agent-relay-logo.svg +45 -0
- package/dist/dashboard/out/alt-logos/logo.svg +38 -0
- package/dist/dashboard/out/alt-logos/monogram-logo-128.png +0 -0
- package/dist/dashboard/out/alt-logos/monogram-logo-256.png +0 -0
- package/dist/dashboard/out/alt-logos/monogram-logo-32.png +0 -0
- package/dist/dashboard/out/alt-logos/monogram-logo-512.png +0 -0
- package/dist/dashboard/out/alt-logos/monogram-logo-64.png +0 -0
- package/dist/dashboard/out/alt-logos/monogram-logo.svg +38 -0
- package/dist/dashboard/out/app/onboarding.html +1 -0
- package/dist/dashboard/out/app/onboarding.txt +7 -0
- package/dist/dashboard/out/app.html +1 -0
- package/dist/dashboard/out/app.txt +7 -0
- package/dist/dashboard/out/apple-icon.png +0 -0
- package/dist/dashboard/out/cloud/link.html +1 -0
- package/dist/dashboard/out/cloud/link.txt +7 -0
- package/dist/dashboard/out/connect-repos.html +1 -0
- package/dist/dashboard/out/connect-repos.txt +7 -0
- package/dist/dashboard/out/history.html +1 -0
- package/dist/dashboard/out/history.txt +7 -0
- package/dist/dashboard/out/index.html +1 -0
- package/dist/dashboard/out/index.txt +7 -0
- package/dist/dashboard/out/login.html +5 -0
- package/dist/dashboard/out/login.txt +7 -0
- package/dist/dashboard/out/metrics.html +1 -0
- package/dist/dashboard/out/metrics.txt +7 -0
- package/dist/dashboard/out/pricing.html +13 -0
- package/dist/dashboard/out/pricing.txt +7 -0
- package/dist/dashboard/out/providers/setup/claude.html +1 -0
- package/dist/dashboard/out/providers/setup/claude.txt +8 -0
- package/dist/dashboard/out/providers/setup/codex.html +1 -0
- package/dist/dashboard/out/providers/setup/codex.txt +8 -0
- package/dist/dashboard/out/providers.html +1 -0
- package/dist/dashboard/out/providers.txt +7 -0
- package/dist/dashboard/out/signup.html +6 -0
- package/dist/dashboard/out/signup.txt +7 -0
- package/dist/dashboard-server/metrics.d.ts +105 -0
- package/dist/dashboard-server/metrics.js +193 -0
- package/dist/dashboard-server/needs-attention.d.ts +24 -0
- package/dist/dashboard-server/needs-attention.js +78 -0
- package/dist/dashboard-server/server.d.ts +15 -0
- package/dist/dashboard-server/server.js +4753 -0
- package/dist/dashboard-server/start.d.ts +6 -0
- package/dist/dashboard-server/start.js +13 -0
- package/dist/dashboard-server/user-bridge.d.ts +132 -0
- package/dist/dashboard-server/user-bridge.js +317 -0
- package/dist/protocol/channels.d.ts +14 -8
- package/dist/protocol/channels.js +1 -1
- package/dist/protocol/index.d.ts +1 -0
- package/dist/protocol/index.js +1 -0
- package/dist/protocol/relay-pty-schemas.d.ts +209 -0
- package/dist/protocol/relay-pty-schemas.js +60 -0
- package/dist/wrapper/auth-detection.js +8 -1
- package/dist/wrapper/base-wrapper.d.ts +11 -1
- package/dist/wrapper/base-wrapper.js +67 -6
- package/dist/wrapper/client.d.ts +49 -1
- package/dist/wrapper/client.js +167 -0
- package/dist/wrapper/parser.d.ts +0 -4
- package/dist/wrapper/parser.js +38 -10
- package/dist/wrapper/pty-wrapper.d.ts +12 -1
- package/dist/wrapper/pty-wrapper.js +104 -5
- package/dist/wrapper/relay-pty-orchestrator.d.ts +270 -0
- package/dist/wrapper/relay-pty-orchestrator.js +970 -0
- package/dist/wrapper/shared.d.ts +1 -1
- package/dist/wrapper/shared.js +14 -4
- package/dist/wrapper/tmux-wrapper.d.ts +13 -1
- package/dist/wrapper/tmux-wrapper.js +143 -29
- package/package.json +9 -4
- package/scripts/postinstall.js +101 -11
- package/.trajectories/active/traj_3yx9dy148mge.json +0 -42
- package/.trajectories/agent-relay-322-324.md +0 -17
- package/.trajectories/completed/2026-01/traj_03zupyv1s7b9.json +0 -49
- package/.trajectories/completed/2026-01/traj_03zupyv1s7b9.md +0 -31
- package/.trajectories/completed/2026-01/traj_0zacdjl1g4ht.json +0 -125
- package/.trajectories/completed/2026-01/traj_0zacdjl1g4ht.md +0 -62
- package/.trajectories/completed/2026-01/traj_1dviorhnkcb5.json +0 -65
- package/.trajectories/completed/2026-01/traj_1dviorhnkcb5.md +0 -37
- package/.trajectories/completed/2026-01/traj_1g7yx6qtg4ai.json +0 -49
- package/.trajectories/completed/2026-01/traj_1g7yx6qtg4ai.md +0 -31
- package/.trajectories/completed/2026-01/traj_1k5if5snst2e.json +0 -65
- package/.trajectories/completed/2026-01/traj_1k5if5snst2e.md +0 -37
- package/.trajectories/completed/2026-01/traj_1rp3rges5811.json +0 -49
- package/.trajectories/completed/2026-01/traj_1rp3rges5811.md +0 -31
- package/.trajectories/completed/2026-01/traj_22bhyulruouw.json +0 -113
- package/.trajectories/completed/2026-01/traj_22bhyulruouw.md +0 -57
- package/.trajectories/completed/2026-01/traj_2dao7ddgnta0.json +0 -53
- package/.trajectories/completed/2026-01/traj_2dao7ddgnta0.md +0 -32
- package/.trajectories/completed/2026-01/traj_33iuy72sezbk.json +0 -49
- package/.trajectories/completed/2026-01/traj_33iuy72sezbk.md +0 -31
- package/.trajectories/completed/2026-01/traj_3t0440mjeunc.json +0 -26
- package/.trajectories/completed/2026-01/traj_3t0440mjeunc.md +0 -6
- package/.trajectories/completed/2026-01/traj_45x9494d9xnr.json +0 -47
- package/.trajectories/completed/2026-01/traj_45x9494d9xnr.md +0 -32
- package/.trajectories/completed/2026-01/traj_4aa0bb77s4nh.json +0 -53
- package/.trajectories/completed/2026-01/traj_4aa0bb77s4nh.md +0 -32
- package/.trajectories/completed/2026-01/traj_4qwd4zmhfwp4.json +0 -49
- package/.trajectories/completed/2026-01/traj_4qwd4zmhfwp4.md +0 -31
- package/.trajectories/completed/2026-01/traj_5ammh5qtvklq.json +0 -77
- package/.trajectories/completed/2026-01/traj_5ammh5qtvklq.md +0 -42
- package/.trajectories/completed/2026-01/traj_5lhmzq8rxpqv.json +0 -59
- package/.trajectories/completed/2026-01/traj_5lhmzq8rxpqv.md +0 -33
- package/.trajectories/completed/2026-01/traj_5vr4e9erb1fs.json +0 -53
- package/.trajectories/completed/2026-01/traj_5vr4e9erb1fs.md +0 -32
- package/.trajectories/completed/2026-01/traj_6fgiwdoklvym.json +0 -48
- package/.trajectories/completed/2026-01/traj_6fgiwdoklvym.md +0 -24
- package/.trajectories/completed/2026-01/traj_6mieijqyvaag.json +0 -77
- package/.trajectories/completed/2026-01/traj_6mieijqyvaag.md +0 -42
- package/.trajectories/completed/2026-01/traj_6unwwmgyj5sq.json +0 -109
- package/.trajectories/completed/2026-01/traj_78ffm31jn3uk.json +0 -77
- package/.trajectories/completed/2026-01/traj_78ffm31jn3uk.md +0 -42
- package/.trajectories/completed/2026-01/traj_7ludwvz45veh.json +0 -209
- package/.trajectories/completed/2026-01/traj_7ludwvz45veh.md +0 -97
- package/.trajectories/completed/2026-01/traj_94gnp3k30goq.json +0 -66
- package/.trajectories/completed/2026-01/traj_94gnp3k30goq.md +0 -36
- package/.trajectories/completed/2026-01/traj_9921cuhel0pj.json +0 -48
- package/.trajectories/completed/2026-01/traj_9921cuhel0pj.md +0 -24
- package/.trajectories/completed/2026-01/traj_a0tqx8biw9c4.json +0 -49
- package/.trajectories/completed/2026-01/traj_a0tqx8biw9c4.md +0 -31
- package/.trajectories/completed/2026-01/traj_ajs7zqfux4wc.json +0 -49
- package/.trajectories/completed/2026-01/traj_ajs7zqfux4wc.md +0 -23
- package/.trajectories/completed/2026-01/traj_avqeghu6pz5a.json +0 -40
- package/.trajectories/completed/2026-01/traj_avqeghu6pz5a.md +0 -22
- package/.trajectories/completed/2026-01/traj_ax8uungxz2qh.json +0 -66
- package/.trajectories/completed/2026-01/traj_ax8uungxz2qh.md +0 -36
- package/.trajectories/completed/2026-01/traj_c9izbh2snpzf.json +0 -49
- package/.trajectories/completed/2026-01/traj_c9izbh2snpzf.md +0 -31
- package/.trajectories/completed/2026-01/traj_cpn70dw066nt.json +0 -65
- package/.trajectories/completed/2026-01/traj_cpn70dw066nt.md +0 -37
- package/.trajectories/completed/2026-01/traj_cvtqhlwcq9s0.json +0 -53
- package/.trajectories/completed/2026-01/traj_cvtqhlwcq9s0.md +0 -32
- package/.trajectories/completed/2026-01/traj_cxofprm2m2en.json +0 -49
- package/.trajectories/completed/2026-01/traj_cxofprm2m2en.md +0 -31
- package/.trajectories/completed/2026-01/traj_d2hhz3k0vrhn.json +0 -26
- package/.trajectories/completed/2026-01/traj_d2hhz3k0vrhn.md +0 -6
- package/.trajectories/completed/2026-01/traj_dcsp9s8y01ra.json +0 -121
- package/.trajectories/completed/2026-01/traj_dcsp9s8y01ra.md +0 -29
- package/.trajectories/completed/2026-01/traj_dfuvww9pege5.json +0 -59
- package/.trajectories/completed/2026-01/traj_dfuvww9pege5.md +0 -37
- package/.trajectories/completed/2026-01/traj_erglv2f8t9eh.json +0 -36
- package/.trajectories/completed/2026-01/traj_erglv2f8t9eh.md +0 -21
- package/.trajectories/completed/2026-01/traj_fhx9irlckht6.json +0 -53
- package/.trajectories/completed/2026-01/traj_fhx9irlckht6.md +0 -32
- package/.trajectories/completed/2026-01/traj_fqduidx3xbtp.json +0 -101
- package/.trajectories/completed/2026-01/traj_fqduidx3xbtp.md +0 -52
- package/.trajectories/completed/2026-01/traj_g0fisy9h51mf.json +0 -77
- package/.trajectories/completed/2026-01/traj_g0fisy9h51mf.md +0 -42
- package/.trajectories/completed/2026-01/traj_gjdre5voouod.json +0 -53
- package/.trajectories/completed/2026-01/traj_gjdre5voouod.md +0 -32
- package/.trajectories/completed/2026-01/traj_gtlyqtta3x8l.json +0 -25
- package/.trajectories/completed/2026-01/traj_gtlyqtta3x8l.md +0 -15
- package/.trajectories/completed/2026-01/traj_h4xijiuip3w4.json +0 -101
- package/.trajectories/completed/2026-01/traj_h4xijiuip3w4.md +0 -44
- package/.trajectories/completed/2026-01/traj_he75f24d1xfm.json +0 -101
- package/.trajectories/completed/2026-01/traj_he75f24d1xfm.md +0 -52
- package/.trajectories/completed/2026-01/traj_hf81ey93uz6t.json +0 -49
- package/.trajectories/completed/2026-01/traj_hf81ey93uz6t.md +0 -31
- package/.trajectories/completed/2026-01/traj_hfmki2jr9d4r.json +0 -65
- package/.trajectories/completed/2026-01/traj_hfmki2jr9d4r.md +0 -37
- package/.trajectories/completed/2026-01/traj_hhxte7w4gjjx.json +0 -22
- package/.trajectories/completed/2026-01/traj_hhxte7w4gjjx.md +0 -5
- package/.trajectories/completed/2026-01/traj_hpungyhoj6v5.json +0 -53
- package/.trajectories/completed/2026-01/traj_hpungyhoj6v5.md +0 -32
- package/.trajectories/completed/2026-01/traj_lgtodco7dp1n.json +0 -61
- package/.trajectories/completed/2026-01/traj_lgtodco7dp1n.md +0 -36
- package/.trajectories/completed/2026-01/traj_lq450ly148uw.json +0 -49
- package/.trajectories/completed/2026-01/traj_lq450ly148uw.md +0 -31
- package/.trajectories/completed/2026-01/traj_m2xkjv0w2sq7.json +0 -25
- package/.trajectories/completed/2026-01/traj_m2xkjv0w2sq7.md +0 -15
- package/.trajectories/completed/2026-01/traj_multi_server_arch.md +0 -101
- package/.trajectories/completed/2026-01/traj_noq5zbvnrdvz.json +0 -53
- package/.trajectories/completed/2026-01/traj_noq5zbvnrdvz.md +0 -32
- package/.trajectories/completed/2026-01/traj_ntbs6ppopf46.json +0 -53
- package/.trajectories/completed/2026-01/traj_ntbs6ppopf46.md +0 -32
- package/.trajectories/completed/2026-01/traj_oszg9flv74pk.json +0 -73
- package/.trajectories/completed/2026-01/traj_oszg9flv74pk.md +0 -41
- package/.trajectories/completed/2026-01/traj_ozd98si6a7ns.json +0 -48
- package/.trajectories/completed/2026-01/traj_ozd98si6a7ns.md +0 -24
- package/.trajectories/completed/2026-01/traj_prdza7a5cxp5.json +0 -53
- package/.trajectories/completed/2026-01/traj_prdza7a5cxp5.md +0 -32
- package/.trajectories/completed/2026-01/traj_psd9ob0j2ru3.json +0 -27
- package/.trajectories/completed/2026-01/traj_psd9ob0j2ru3.md +0 -14
- package/.trajectories/completed/2026-01/traj_pulomd3y8cvj.json +0 -77
- package/.trajectories/completed/2026-01/traj_pulomd3y8cvj.md +0 -42
- package/.trajectories/completed/2026-01/traj_qb3twvvywfwi.json +0 -77
- package/.trajectories/completed/2026-01/traj_qb3twvvywfwi.md +0 -42
- package/.trajectories/completed/2026-01/traj_qft54mi7nfor.json +0 -53
- package/.trajectories/completed/2026-01/traj_qft54mi7nfor.md +0 -32
- package/.trajectories/completed/2026-01/traj_qx9uhf8whhxo.json +0 -83
- package/.trajectories/completed/2026-01/traj_qx9uhf8whhxo.md +0 -47
- package/.trajectories/completed/2026-01/traj_rd9toccj18a0.json +0 -59
- package/.trajectories/completed/2026-01/traj_rd9toccj18a0.md +0 -37
- package/.trajectories/completed/2026-01/traj_rsavt0jipi3c.json +0 -109
- package/.trajectories/completed/2026-01/traj_rsavt0jipi3c.md +0 -56
- package/.trajectories/completed/2026-01/traj_rt4fiw3ecp50.json +0 -48
- package/.trajectories/completed/2026-01/traj_rt4fiw3ecp50.md +0 -16
- package/.trajectories/completed/2026-01/traj_st8j35b0hrlc.json +0 -59
- package/.trajectories/completed/2026-01/traj_st8j35b0hrlc.md +0 -37
- package/.trajectories/completed/2026-01/traj_t1yy8m7hbuxp.json +0 -53
- package/.trajectories/completed/2026-01/traj_t1yy8m7hbuxp.md +0 -32
- package/.trajectories/completed/2026-01/traj_tmux_orchestrator_analysis.json +0 -84
- package/.trajectories/completed/2026-01/traj_tmux_orchestrator_analysis.md +0 -109
- package/.trajectories/completed/2026-01/traj_u9n9eqasw16k.json +0 -53
- package/.trajectories/completed/2026-01/traj_u9n9eqasw16k.md +0 -32
- package/.trajectories/completed/2026-01/traj_ub8csuv3lcv4.json +0 -53
- package/.trajectories/completed/2026-01/traj_ub8csuv3lcv4.md +0 -32
- package/.trajectories/completed/2026-01/traj_uc29tlso8i9s.json +0 -186
- package/.trajectories/completed/2026-01/traj_uc29tlso8i9s.md +0 -86
- package/.trajectories/completed/2026-01/traj_ui9b4tqxoa7j.json +0 -77
- package/.trajectories/completed/2026-01/traj_ui9b4tqxoa7j.md +0 -42
- package/.trajectories/completed/2026-01/traj_v87hypnongqx.json +0 -71
- package/.trajectories/completed/2026-01/traj_v87hypnongqx.md +0 -42
- package/.trajectories/completed/2026-01/traj_v9dkdoxylyid.json +0 -89
- package/.trajectories/completed/2026-01/traj_v9dkdoxylyid.md +0 -47
- package/.trajectories/completed/2026-01/traj_wkp2fgzdyinb.json +0 -53
- package/.trajectories/completed/2026-01/traj_wkp2fgzdyinb.md +0 -32
- package/.trajectories/completed/2026-01/traj_x14t8w8rn7xg.json +0 -20
- package/.trajectories/completed/2026-01/traj_x14t8w8rn7xg.md +0 -6
- package/.trajectories/completed/2026-01/traj_x721m1j9rzup.json +0 -113
- package/.trajectories/completed/2026-01/traj_x721m1j9rzup.md +0 -57
- package/.trajectories/completed/2026-01/traj_xjqvmep5ed3h.json +0 -61
- package/.trajectories/completed/2026-01/traj_xjqvmep5ed3h.md +0 -36
- package/.trajectories/completed/2026-01/traj_xnwbznkvv8ua.json +0 -175
- package/.trajectories/completed/2026-01/traj_xnwbznkvv8ua.md +0 -82
- package/.trajectories/completed/2026-01/traj_xy9vifpqet80.json +0 -65
- package/.trajectories/completed/2026-01/traj_xy9vifpqet80.md +0 -37
- package/.trajectories/completed/2026-01/traj_y7aiwijyfmmv.json +0 -49
- package/.trajectories/completed/2026-01/traj_y7aiwijyfmmv.md +0 -31
- package/.trajectories/completed/2026-01/traj_y7n6hfbf7dmg.json +0 -49
- package/.trajectories/completed/2026-01/traj_y7n6hfbf7dmg.md +0 -31
- package/.trajectories/completed/2026-01/traj_ysjc8zaeqtd3.json +0 -47
- package/.trajectories/completed/2026-01/traj_ysjc8zaeqtd3.md +0 -32
- package/.trajectories/completed/2026-01/traj_yvdadtvdgnz3.json +0 -59
- package/.trajectories/completed/2026-01/traj_yvdadtvdgnz3.md +0 -37
- package/.trajectories/completed/2026-01/traj_yvfkwnkdiso2.json +0 -49
- package/.trajectories/completed/2026-01/traj_yvfkwnkdiso2.md +0 -31
- package/.trajectories/completed/2026-01/traj_z0vcw1wrzide.json +0 -53
- package/.trajectories/completed/2026-01/traj_z0vcw1wrzide.md +0 -32
- package/.trajectories/consolidate-settings-panel.md +0 -24
- package/.trajectories/gh-cli-user-token.md +0 -26
- package/.trajectories/index.json +0 -607
package/dist/cloud/server.js
CHANGED
|
@@ -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
|
-
|
|
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('[
|
|
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
|
|
371
|
-
clientWs.send(JSON.stringify({ type: 'error', message: 'Workspace not found
|
|
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
|
|
376
|
-
//
|
|
377
|
-
|
|
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) {
|