agent-relay 1.1.0 → 1.2.3

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 (567) hide show
  1. package/.gitattributes +3 -0
  2. package/.nvmrc +1 -0
  3. package/.trajectories/completed/2026-01/traj_1dviorhnkcb5.json +65 -0
  4. package/.trajectories/completed/2026-01/traj_1dviorhnkcb5.md +37 -0
  5. package/.trajectories/completed/2026-01/traj_1k5if5snst2e.json +65 -0
  6. package/.trajectories/completed/2026-01/traj_1k5if5snst2e.md +37 -0
  7. package/.trajectories/completed/2026-01/traj_1rp3rges5811.json +49 -0
  8. package/.trajectories/completed/2026-01/traj_1rp3rges5811.md +31 -0
  9. package/.trajectories/completed/2026-01/traj_22bhyulruouw.json +113 -0
  10. package/.trajectories/completed/2026-01/traj_22bhyulruouw.md +57 -0
  11. package/.trajectories/completed/2026-01/traj_2dao7ddgnta0.json +53 -0
  12. package/.trajectories/completed/2026-01/traj_2dao7ddgnta0.md +32 -0
  13. package/.trajectories/completed/2026-01/traj_3t0440mjeunc.json +26 -0
  14. package/.trajectories/completed/2026-01/traj_3t0440mjeunc.md +6 -0
  15. package/.trajectories/completed/2026-01/traj_45x9494d9xnr.json +47 -0
  16. package/.trajectories/completed/2026-01/traj_45x9494d9xnr.md +32 -0
  17. package/.trajectories/completed/2026-01/traj_4aa0bb77s4nh.json +53 -0
  18. package/.trajectories/completed/2026-01/traj_4aa0bb77s4nh.md +32 -0
  19. package/.trajectories/completed/2026-01/traj_5lhmzq8rxpqv.json +59 -0
  20. package/.trajectories/completed/2026-01/traj_5lhmzq8rxpqv.md +33 -0
  21. package/.trajectories/completed/2026-01/traj_5vr4e9erb1fs.json +53 -0
  22. package/.trajectories/completed/2026-01/traj_5vr4e9erb1fs.md +32 -0
  23. package/.trajectories/completed/2026-01/traj_6fgiwdoklvym.json +48 -0
  24. package/.trajectories/completed/2026-01/traj_6fgiwdoklvym.md +24 -0
  25. package/.trajectories/completed/2026-01/traj_7ludwvz45veh.json +209 -0
  26. package/.trajectories/completed/2026-01/traj_7ludwvz45veh.md +97 -0
  27. package/.trajectories/completed/2026-01/traj_9921cuhel0pj.json +48 -0
  28. package/.trajectories/completed/2026-01/traj_9921cuhel0pj.md +24 -0
  29. package/.trajectories/completed/2026-01/traj_ajs7zqfux4wc.json +49 -0
  30. package/.trajectories/completed/2026-01/traj_ajs7zqfux4wc.md +23 -0
  31. package/.trajectories/completed/2026-01/traj_cvtqhlwcq9s0.json +53 -0
  32. package/.trajectories/completed/2026-01/traj_cvtqhlwcq9s0.md +32 -0
  33. package/.trajectories/completed/2026-01/traj_cxofprm2m2en.json +49 -0
  34. package/.trajectories/completed/2026-01/traj_cxofprm2m2en.md +31 -0
  35. package/.trajectories/completed/2026-01/traj_d2hhz3k0vrhn.json +26 -0
  36. package/.trajectories/completed/2026-01/traj_d2hhz3k0vrhn.md +6 -0
  37. package/.trajectories/completed/2026-01/traj_dfuvww9pege5.json +59 -0
  38. package/.trajectories/completed/2026-01/traj_dfuvww9pege5.md +37 -0
  39. package/.trajectories/completed/2026-01/traj_g0fisy9h51mf.json +77 -0
  40. package/.trajectories/completed/2026-01/traj_g0fisy9h51mf.md +42 -0
  41. package/.trajectories/completed/2026-01/traj_gjdre5voouod.json +53 -0
  42. package/.trajectories/completed/2026-01/traj_gjdre5voouod.md +32 -0
  43. package/.trajectories/completed/2026-01/traj_gtlyqtta3x8l.json +25 -0
  44. package/.trajectories/completed/2026-01/traj_gtlyqtta3x8l.md +15 -0
  45. package/.trajectories/completed/2026-01/traj_h4xijiuip3w4.json +101 -0
  46. package/.trajectories/completed/2026-01/traj_h4xijiuip3w4.md +44 -0
  47. package/.trajectories/completed/2026-01/traj_hhxte7w4gjjx.json +22 -0
  48. package/.trajectories/completed/2026-01/traj_hhxte7w4gjjx.md +5 -0
  49. package/.trajectories/completed/2026-01/traj_hpungyhoj6v5.json +53 -0
  50. package/.trajectories/completed/2026-01/traj_hpungyhoj6v5.md +32 -0
  51. package/.trajectories/completed/2026-01/traj_m2xkjv0w2sq7.json +25 -0
  52. package/.trajectories/completed/2026-01/traj_m2xkjv0w2sq7.md +15 -0
  53. package/.trajectories/completed/2026-01/traj_noq5zbvnrdvz.json +53 -0
  54. package/.trajectories/completed/2026-01/traj_noq5zbvnrdvz.md +32 -0
  55. package/.trajectories/completed/2026-01/traj_ntbs6ppopf46.json +53 -0
  56. package/.trajectories/completed/2026-01/traj_ntbs6ppopf46.md +32 -0
  57. package/.trajectories/completed/2026-01/traj_ozd98si6a7ns.json +48 -0
  58. package/.trajectories/completed/2026-01/traj_ozd98si6a7ns.md +24 -0
  59. package/.trajectories/completed/2026-01/traj_prdza7a5cxp5.json +53 -0
  60. package/.trajectories/completed/2026-01/traj_prdza7a5cxp5.md +32 -0
  61. package/.trajectories/completed/2026-01/traj_qb3twvvywfwi.json +77 -0
  62. package/.trajectories/completed/2026-01/traj_qb3twvvywfwi.md +42 -0
  63. package/.trajectories/completed/2026-01/traj_qft54mi7nfor.json +53 -0
  64. package/.trajectories/completed/2026-01/traj_qft54mi7nfor.md +32 -0
  65. package/.trajectories/completed/2026-01/traj_qx9uhf8whhxo.json +83 -0
  66. package/.trajectories/completed/2026-01/traj_qx9uhf8whhxo.md +47 -0
  67. package/.trajectories/completed/2026-01/traj_rd9toccj18a0.json +59 -0
  68. package/.trajectories/completed/2026-01/traj_rd9toccj18a0.md +37 -0
  69. package/.trajectories/completed/2026-01/traj_rt4fiw3ecp50.json +48 -0
  70. package/.trajectories/completed/2026-01/traj_rt4fiw3ecp50.md +16 -0
  71. package/.trajectories/completed/2026-01/traj_st8j35b0hrlc.json +59 -0
  72. package/.trajectories/completed/2026-01/traj_st8j35b0hrlc.md +37 -0
  73. package/.trajectories/completed/2026-01/traj_t1yy8m7hbuxp.json +53 -0
  74. package/.trajectories/completed/2026-01/traj_t1yy8m7hbuxp.md +32 -0
  75. package/.trajectories/completed/2026-01/traj_tmux_orchestrator_analysis.json +84 -0
  76. package/.trajectories/completed/2026-01/traj_tmux_orchestrator_analysis.md +109 -0
  77. package/.trajectories/completed/2026-01/traj_u9n9eqasw16k.json +53 -0
  78. package/.trajectories/completed/2026-01/traj_u9n9eqasw16k.md +32 -0
  79. package/.trajectories/completed/2026-01/traj_v87hypnongqx.json +71 -0
  80. package/.trajectories/completed/2026-01/traj_v87hypnongqx.md +42 -0
  81. package/.trajectories/completed/2026-01/traj_wkp2fgzdyinb.json +53 -0
  82. package/.trajectories/completed/2026-01/traj_wkp2fgzdyinb.md +32 -0
  83. package/.trajectories/completed/2026-01/traj_x14t8w8rn7xg.json +20 -0
  84. package/.trajectories/completed/2026-01/traj_x14t8w8rn7xg.md +6 -0
  85. package/.trajectories/completed/2026-01/traj_xnwbznkvv8ua.json +175 -0
  86. package/.trajectories/completed/2026-01/traj_xnwbznkvv8ua.md +82 -0
  87. package/.trajectories/completed/2026-01/traj_ysjc8zaeqtd3.json +47 -0
  88. package/.trajectories/completed/2026-01/traj_ysjc8zaeqtd3.md +32 -0
  89. package/.trajectories/completed/2026-01/traj_yvdadtvdgnz3.json +59 -0
  90. package/.trajectories/completed/2026-01/traj_yvdadtvdgnz3.md +37 -0
  91. package/.trajectories/completed/2026-01/traj_z0vcw1wrzide.json +53 -0
  92. package/.trajectories/completed/2026-01/traj_z0vcw1wrzide.md +32 -0
  93. package/.trajectories/index.json +314 -0
  94. package/ARCHITECTURE.md +1245 -0
  95. package/README.md +1 -1
  96. package/TESTING.md +278 -0
  97. package/deploy/init-db.sql +5 -0
  98. package/deploy/scripts/setup-fly-workspaces.sh +69 -0
  99. package/deploy/scripts/setup-railway.sh +75 -0
  100. package/deploy/workspace/entrypoint-browser.sh +118 -0
  101. package/deploy/workspace/entrypoint.sh +348 -0
  102. package/deploy/workspace/git-credential-relay +111 -0
  103. package/dist/bridge/spawner.d.ts +53 -0
  104. package/dist/bridge/spawner.js +203 -19
  105. package/dist/bridge/types.d.ts +12 -0
  106. package/dist/cli/index.js +618 -5
  107. package/dist/cloud/api/auth.d.ts +3 -2
  108. package/dist/cloud/api/auth.js +10 -98
  109. package/dist/cloud/api/billing.js +30 -9
  110. package/dist/cloud/api/cli-pty-runner.d.ts +54 -0
  111. package/dist/cloud/api/cli-pty-runner.js +119 -0
  112. package/dist/cloud/api/codex-auth-helper.d.ts +15 -0
  113. package/dist/cloud/api/codex-auth-helper.js +100 -0
  114. package/dist/cloud/api/generic-webhooks.d.ts +8 -0
  115. package/dist/cloud/api/generic-webhooks.js +129 -0
  116. package/dist/cloud/api/git.d.ts +8 -0
  117. package/dist/cloud/api/git.js +152 -0
  118. package/dist/cloud/api/github-app.d.ts +11 -0
  119. package/dist/cloud/api/github-app.js +189 -0
  120. package/dist/cloud/api/middleware/planLimits.d.ts +7 -0
  121. package/dist/cloud/api/middleware/planLimits.js +39 -1
  122. package/dist/cloud/api/monitoring.d.ts +11 -0
  123. package/dist/cloud/api/monitoring.js +578 -0
  124. package/dist/cloud/api/nango-auth.d.ts +9 -0
  125. package/dist/cloud/api/nango-auth.js +377 -0
  126. package/dist/cloud/api/onboarding.d.ts +8 -1
  127. package/dist/cloud/api/onboarding.js +313 -119
  128. package/dist/cloud/api/policy.d.ts +8 -0
  129. package/dist/cloud/api/policy.js +229 -0
  130. package/dist/cloud/api/providers.js +114 -42
  131. package/dist/cloud/api/repos.d.ts +1 -0
  132. package/dist/cloud/api/repos.js +186 -0
  133. package/dist/cloud/api/test-helpers.d.ts +10 -0
  134. package/dist/cloud/api/test-helpers.js +575 -0
  135. package/dist/cloud/api/webhooks.d.ts +8 -0
  136. package/dist/cloud/api/webhooks.js +645 -0
  137. package/dist/cloud/api/workspaces.js +320 -12
  138. package/dist/cloud/billing/plans.js +32 -19
  139. package/dist/cloud/billing/types.d.ts +9 -3
  140. package/dist/cloud/config.d.ts +9 -2
  141. package/dist/cloud/config.js +13 -4
  142. package/dist/cloud/db/drizzle.d.ts +84 -1
  143. package/dist/cloud/db/drizzle.js +470 -0
  144. package/dist/cloud/db/index.d.ts +9 -4
  145. package/dist/cloud/db/index.js +11 -3
  146. package/dist/cloud/db/schema.d.ts +3283 -556
  147. package/dist/cloud/db/schema.js +314 -1
  148. package/dist/cloud/index.d.ts +1 -0
  149. package/dist/cloud/index.js +2 -0
  150. package/dist/cloud/provisioner/index.d.ts +56 -0
  151. package/dist/cloud/provisioner/index.js +676 -34
  152. package/dist/cloud/server.d.ts +1 -0
  153. package/dist/cloud/server.js +362 -13
  154. package/dist/cloud/services/auto-scaler.d.ts +152 -0
  155. package/dist/cloud/services/auto-scaler.js +439 -0
  156. package/dist/cloud/services/capacity-manager.d.ts +148 -0
  157. package/dist/cloud/services/capacity-manager.js +449 -0
  158. package/dist/cloud/services/ci-agent-spawner.d.ts +49 -0
  159. package/dist/cloud/services/ci-agent-spawner.js +373 -0
  160. package/dist/cloud/services/index.d.ts +12 -0
  161. package/dist/cloud/services/index.js +15 -0
  162. package/dist/cloud/services/mention-handler.d.ts +65 -0
  163. package/dist/cloud/services/mention-handler.js +405 -0
  164. package/dist/cloud/services/nango.d.ts +186 -0
  165. package/dist/cloud/services/nango.js +344 -0
  166. package/dist/cloud/services/persistence.d.ts +131 -0
  167. package/dist/cloud/services/persistence.js +200 -0
  168. package/dist/cloud/services/planLimits.d.ts +37 -0
  169. package/dist/cloud/services/planLimits.js +86 -5
  170. package/dist/cloud/services/scaling-orchestrator.d.ts +159 -0
  171. package/dist/cloud/services/scaling-orchestrator.js +502 -0
  172. package/dist/cloud/services/scaling-policy.d.ts +121 -0
  173. package/dist/cloud/services/scaling-policy.js +415 -0
  174. package/dist/cloud/vault/index.js +1 -1
  175. package/dist/cloud/webhooks/index.d.ts +24 -0
  176. package/dist/cloud/webhooks/index.js +29 -0
  177. package/dist/cloud/webhooks/parsers/github.d.ts +8 -0
  178. package/dist/cloud/webhooks/parsers/github.js +234 -0
  179. package/dist/cloud/webhooks/parsers/index.d.ts +23 -0
  180. package/dist/cloud/webhooks/parsers/index.js +30 -0
  181. package/dist/cloud/webhooks/parsers/linear.d.ts +9 -0
  182. package/dist/cloud/webhooks/parsers/linear.js +258 -0
  183. package/dist/cloud/webhooks/parsers/slack.d.ts +9 -0
  184. package/dist/cloud/webhooks/parsers/slack.js +214 -0
  185. package/dist/cloud/webhooks/responders/github.d.ts +8 -0
  186. package/dist/cloud/webhooks/responders/github.js +73 -0
  187. package/dist/cloud/webhooks/responders/index.d.ts +23 -0
  188. package/dist/cloud/webhooks/responders/index.js +30 -0
  189. package/dist/cloud/webhooks/responders/linear.d.ts +9 -0
  190. package/dist/cloud/webhooks/responders/linear.js +149 -0
  191. package/dist/cloud/webhooks/responders/slack.d.ts +20 -0
  192. package/dist/cloud/webhooks/responders/slack.js +178 -0
  193. package/dist/cloud/webhooks/router.d.ts +25 -0
  194. package/dist/cloud/webhooks/router.js +504 -0
  195. package/dist/cloud/webhooks/rules-engine.d.ts +24 -0
  196. package/dist/cloud/webhooks/rules-engine.js +287 -0
  197. package/dist/cloud/webhooks/types.d.ts +186 -0
  198. package/dist/cloud/webhooks/types.js +8 -0
  199. package/dist/continuity/formatter.d.ts +51 -0
  200. package/dist/continuity/formatter.js +313 -0
  201. package/dist/continuity/handoff-store.d.ts +67 -0
  202. package/dist/continuity/handoff-store.js +472 -0
  203. package/dist/continuity/index.d.ts +45 -0
  204. package/dist/continuity/index.js +48 -0
  205. package/dist/continuity/ledger-store.d.ts +110 -0
  206. package/dist/continuity/ledger-store.js +500 -0
  207. package/dist/continuity/manager.d.ts +178 -0
  208. package/dist/continuity/manager.js +562 -0
  209. package/dist/continuity/parser.d.ts +76 -0
  210. package/dist/continuity/parser.js +579 -0
  211. package/dist/continuity/types.d.ts +180 -0
  212. package/dist/continuity/types.js +9 -0
  213. package/dist/daemon/agent-manager.d.ts +27 -0
  214. package/dist/daemon/agent-manager.js +107 -6
  215. package/dist/daemon/agent-registry.d.ts +32 -0
  216. package/dist/daemon/agent-registry.js +42 -2
  217. package/dist/daemon/api.d.ts +12 -0
  218. package/dist/daemon/api.js +131 -2
  219. package/dist/daemon/cli-auth.d.ts +67 -0
  220. package/dist/daemon/cli-auth.js +537 -0
  221. package/dist/daemon/cloud-sync.js +9 -7
  222. package/dist/daemon/orchestrator.js +30 -0
  223. package/dist/daemon/router.d.ts +5 -0
  224. package/dist/daemon/router.js +78 -26
  225. package/dist/daemon/server.d.ts +5 -0
  226. package/dist/daemon/server.js +9 -1
  227. package/dist/daemon/services/browser-testing.d.ts +88 -0
  228. package/dist/daemon/services/browser-testing.js +244 -0
  229. package/dist/daemon/services/container-spawner.d.ts +135 -0
  230. package/dist/daemon/services/container-spawner.js +313 -0
  231. package/dist/daemon/types.d.ts +5 -1
  232. package/dist/dashboard/out/404.html +1 -1
  233. package/dist/dashboard/out/_next/static/chunks/116-2502180def231162.js +1 -0
  234. package/dist/dashboard/out/_next/static/chunks/282-980c2eb8fff20123.js +1 -0
  235. package/dist/dashboard/out/_next/static/chunks/699-3b1cd6618a45d259.js +1 -0
  236. package/dist/dashboard/out/_next/static/chunks/724-2dae7627550ab88f.js +9 -0
  237. package/dist/dashboard/out/_next/static/chunks/766-1f2dd8cb7f766b0b.js +1 -0
  238. package/dist/dashboard/out/_next/static/chunks/app/app/onboarding/page-3fdfa60e53f2810d.js +1 -0
  239. package/dist/dashboard/out/_next/static/chunks/app/app/page-e6381e5a6e1fbcfd.js +1 -0
  240. package/dist/dashboard/out/_next/static/chunks/app/connect-repos/page-3538dfe0ffe984b8.js +1 -0
  241. package/dist/dashboard/out/_next/static/chunks/app/history/{page-b6edd4dde8d08194.js → page-abb9ab2d329f56e9.js} +1 -1
  242. package/dist/dashboard/out/_next/static/chunks/app/layout-c0d118c0f92d969c.js +1 -0
  243. package/dist/dashboard/out/_next/static/chunks/app/login/page-c22d080201cbd9fb.js +1 -0
  244. package/dist/dashboard/out/_next/static/chunks/app/metrics/page-67a3e98d9a43a6ed.js +1 -0
  245. package/dist/dashboard/out/_next/static/chunks/app/page-77e9c65420a06cfb.js +1 -0
  246. package/dist/dashboard/out/_next/static/chunks/app/pricing/page-b08ed1c34d14434a.js +1 -0
  247. package/dist/dashboard/out/_next/static/chunks/app/providers/page-e88bc117ef7671c3.js +1 -0
  248. package/dist/dashboard/out/_next/static/chunks/app/signup/page-68d34f50baa8ab6b.js +1 -0
  249. package/dist/dashboard/out/_next/static/chunks/e868780c-48e5f147c90a3a41.js +18 -0
  250. package/dist/dashboard/out/_next/static/chunks/{main-app-5d692157a8eb1fd9.js → main-app-6e8e8d3ef4e0192a.js} +1 -1
  251. package/dist/dashboard/out/_next/static/chunks/{main-c2f423b9c9f4591b.js → main-ed4e1fb6f29c34cf.js} +1 -1
  252. package/dist/dashboard/out/_next/static/chunks/webpack-1cdd8ed57114d5e1.js +1 -0
  253. package/dist/dashboard/out/_next/static/css/29852f26181969a0.css +1 -0
  254. package/dist/dashboard/out/_next/static/css/7c3ae9e8617d42a5.css +1 -0
  255. package/dist/dashboard/out/app/onboarding.html +1 -0
  256. package/dist/dashboard/out/app/onboarding.txt +7 -0
  257. package/dist/dashboard/out/app.html +1 -14
  258. package/dist/dashboard/out/app.txt +2 -2
  259. package/dist/dashboard/out/connect-repos.html +1 -0
  260. package/dist/dashboard/out/connect-repos.txt +7 -0
  261. package/dist/dashboard/out/history.html +1 -1
  262. package/dist/dashboard/out/history.txt +2 -2
  263. package/dist/dashboard/out/index.html +1 -1
  264. package/dist/dashboard/out/index.txt +2 -2
  265. package/dist/dashboard/out/login.html +6 -0
  266. package/dist/dashboard/out/login.txt +7 -0
  267. package/dist/dashboard/out/metrics.html +1 -1
  268. package/dist/dashboard/out/metrics.txt +2 -2
  269. package/dist/dashboard/out/pricing.html +3 -3
  270. package/dist/dashboard/out/pricing.txt +2 -2
  271. package/dist/dashboard/out/providers.html +1 -0
  272. package/dist/dashboard/out/providers.txt +7 -0
  273. package/dist/dashboard/out/signup.html +6 -0
  274. package/dist/dashboard/out/signup.txt +7 -0
  275. package/dist/dashboard-server/server.js +1308 -8
  276. package/dist/hooks/emitter.d.ts +40 -0
  277. package/dist/hooks/emitter.js +63 -0
  278. package/dist/hooks/index.d.ts +3 -0
  279. package/dist/hooks/index.js +3 -0
  280. package/dist/hooks/registry.d.ts +173 -0
  281. package/dist/hooks/registry.js +476 -0
  282. package/dist/hooks/trajectory-hooks.d.ts +52 -0
  283. package/dist/hooks/trajectory-hooks.js +183 -0
  284. package/dist/hooks/types.d.ts +141 -0
  285. package/dist/index.d.ts +2 -0
  286. package/dist/index.js +3 -0
  287. package/dist/memory/adapters/index.d.ts +8 -0
  288. package/dist/memory/adapters/index.js +8 -0
  289. package/dist/memory/adapters/inmemory.d.ts +59 -0
  290. package/dist/memory/adapters/inmemory.js +195 -0
  291. package/dist/memory/adapters/supermemory.d.ts +71 -0
  292. package/dist/memory/adapters/supermemory.js +338 -0
  293. package/dist/memory/factory.d.ts +48 -0
  294. package/dist/memory/factory.js +143 -0
  295. package/dist/memory/index.d.ts +32 -0
  296. package/dist/memory/index.js +32 -0
  297. package/dist/memory/memory-hooks.d.ts +60 -0
  298. package/dist/memory/memory-hooks.js +313 -0
  299. package/dist/memory/service.d.ts +49 -0
  300. package/dist/memory/service.js +146 -0
  301. package/dist/memory/types.d.ts +195 -0
  302. package/dist/memory/types.js +8 -0
  303. package/dist/policy/agent-policy.d.ts +225 -0
  304. package/dist/policy/agent-policy.js +665 -0
  305. package/dist/policy/cloud-policy-fetcher.d.ts +12 -0
  306. package/dist/policy/cloud-policy-fetcher.js +64 -0
  307. package/dist/resiliency/crash-insights.d.ts +156 -0
  308. package/dist/resiliency/crash-insights.js +492 -0
  309. package/dist/resiliency/gossip-health.d.ts +137 -0
  310. package/dist/resiliency/gossip-health.js +241 -0
  311. package/dist/resiliency/index.d.ts +5 -0
  312. package/dist/resiliency/index.js +5 -0
  313. package/dist/resiliency/leader-watchdog.d.ts +109 -0
  314. package/dist/resiliency/leader-watchdog.js +189 -0
  315. package/dist/resiliency/memory-monitor.d.ts +172 -0
  316. package/dist/resiliency/memory-monitor.js +593 -0
  317. package/dist/resiliency/stateless-lead.d.ts +149 -0
  318. package/dist/resiliency/stateless-lead.js +308 -0
  319. package/dist/resiliency/supervisor.d.ts +38 -0
  320. package/dist/resiliency/supervisor.js +122 -0
  321. package/dist/shared/cli-auth-config.d.ts +91 -0
  322. package/dist/shared/cli-auth-config.js +264 -0
  323. package/dist/storage/adapter.d.ts +1 -1
  324. package/dist/trajectory/config.d.ts +84 -0
  325. package/dist/trajectory/config.js +163 -0
  326. package/dist/trajectory/index.d.ts +8 -0
  327. package/dist/trajectory/index.js +8 -0
  328. package/dist/trajectory/integration.d.ts +292 -0
  329. package/dist/trajectory/integration.js +834 -0
  330. package/dist/utils/logger.js +1 -1
  331. package/dist/utils/project-namespace.d.ts +24 -0
  332. package/dist/utils/project-namespace.js +84 -0
  333. package/dist/wrapper/parser.d.ts +10 -0
  334. package/dist/wrapper/parser.js +100 -33
  335. package/dist/wrapper/pty-wrapper.d.ts +197 -16
  336. package/dist/wrapper/pty-wrapper.js +943 -106
  337. package/dist/wrapper/shared.d.ts +165 -0
  338. package/dist/wrapper/shared.js +270 -0
  339. package/dist/wrapper/tmux-wrapper.d.ts +73 -11
  340. package/dist/wrapper/tmux-wrapper.js +541 -120
  341. package/package.json +16 -16
  342. package/scripts/postinstall.js +60 -0
  343. package/test-push.txt +1 -0
  344. package/bin/tmux +0 -0
  345. package/dist/bridge/config.d.ts.map +0 -1
  346. package/dist/bridge/config.js.map +0 -1
  347. package/dist/bridge/index.d.ts.map +0 -1
  348. package/dist/bridge/index.js.map +0 -1
  349. package/dist/bridge/multi-project-client.d.ts.map +0 -1
  350. package/dist/bridge/multi-project-client.js.map +0 -1
  351. package/dist/bridge/shadow-cli.d.ts.map +0 -1
  352. package/dist/bridge/shadow-cli.js.map +0 -1
  353. package/dist/bridge/shadow-config.d.ts.map +0 -1
  354. package/dist/bridge/shadow-config.js.map +0 -1
  355. package/dist/bridge/spawner.d.ts.map +0 -1
  356. package/dist/bridge/spawner.js.map +0 -1
  357. package/dist/bridge/teams-config.d.ts.map +0 -1
  358. package/dist/bridge/teams-config.js.map +0 -1
  359. package/dist/bridge/types.d.ts.map +0 -1
  360. package/dist/bridge/types.js.map +0 -1
  361. package/dist/bridge/utils.d.ts.map +0 -1
  362. package/dist/bridge/utils.js.map +0 -1
  363. package/dist/cli/index.d.ts.map +0 -1
  364. package/dist/cli/index.js.map +0 -1
  365. package/dist/cloud/api/auth.d.ts.map +0 -1
  366. package/dist/cloud/api/auth.js.map +0 -1
  367. package/dist/cloud/api/billing.d.ts.map +0 -1
  368. package/dist/cloud/api/billing.js.map +0 -1
  369. package/dist/cloud/api/coordinators.d.ts.map +0 -1
  370. package/dist/cloud/api/coordinators.js.map +0 -1
  371. package/dist/cloud/api/daemons.d.ts.map +0 -1
  372. package/dist/cloud/api/daemons.js.map +0 -1
  373. package/dist/cloud/api/middleware/planLimits.d.ts.map +0 -1
  374. package/dist/cloud/api/middleware/planLimits.js.map +0 -1
  375. package/dist/cloud/api/onboarding.d.ts.map +0 -1
  376. package/dist/cloud/api/onboarding.js.map +0 -1
  377. package/dist/cloud/api/providers.d.ts.map +0 -1
  378. package/dist/cloud/api/providers.js.map +0 -1
  379. package/dist/cloud/api/repos.d.ts.map +0 -1
  380. package/dist/cloud/api/repos.js.map +0 -1
  381. package/dist/cloud/api/teams.d.ts.map +0 -1
  382. package/dist/cloud/api/teams.js.map +0 -1
  383. package/dist/cloud/api/usage.d.ts.map +0 -1
  384. package/dist/cloud/api/usage.js.map +0 -1
  385. package/dist/cloud/api/workspaces.d.ts.map +0 -1
  386. package/dist/cloud/api/workspaces.js.map +0 -1
  387. package/dist/cloud/billing/index.d.ts.map +0 -1
  388. package/dist/cloud/billing/index.js.map +0 -1
  389. package/dist/cloud/billing/plans.d.ts.map +0 -1
  390. package/dist/cloud/billing/plans.js.map +0 -1
  391. package/dist/cloud/billing/service.d.ts.map +0 -1
  392. package/dist/cloud/billing/service.js.map +0 -1
  393. package/dist/cloud/billing/types.d.ts.map +0 -1
  394. package/dist/cloud/billing/types.js.map +0 -1
  395. package/dist/cloud/config.d.ts.map +0 -1
  396. package/dist/cloud/config.js.map +0 -1
  397. package/dist/cloud/db/drizzle.d.ts.map +0 -1
  398. package/dist/cloud/db/drizzle.js.map +0 -1
  399. package/dist/cloud/db/index.d.ts.map +0 -1
  400. package/dist/cloud/db/index.js.map +0 -1
  401. package/dist/cloud/db/schema.d.ts.map +0 -1
  402. package/dist/cloud/db/schema.js.map +0 -1
  403. package/dist/cloud/index.d.ts.map +0 -1
  404. package/dist/cloud/index.js.map +0 -1
  405. package/dist/cloud/provisioner/index.d.ts.map +0 -1
  406. package/dist/cloud/provisioner/index.js.map +0 -1
  407. package/dist/cloud/server.d.ts.map +0 -1
  408. package/dist/cloud/server.js.map +0 -1
  409. package/dist/cloud/services/coordinator.d.ts.map +0 -1
  410. package/dist/cloud/services/coordinator.js.map +0 -1
  411. package/dist/cloud/services/planLimits.d.ts.map +0 -1
  412. package/dist/cloud/services/planLimits.js.map +0 -1
  413. package/dist/cloud/vault/index.d.ts.map +0 -1
  414. package/dist/cloud/vault/index.js.map +0 -1
  415. package/dist/daemon/agent-manager.d.ts.map +0 -1
  416. package/dist/daemon/agent-manager.js.map +0 -1
  417. package/dist/daemon/agent-registry.d.ts.map +0 -1
  418. package/dist/daemon/agent-registry.js.map +0 -1
  419. package/dist/daemon/api.d.ts.map +0 -1
  420. package/dist/daemon/api.js.map +0 -1
  421. package/dist/daemon/auth.d.ts.map +0 -1
  422. package/dist/daemon/auth.js.map +0 -1
  423. package/dist/daemon/cloud-sync.d.ts.map +0 -1
  424. package/dist/daemon/cloud-sync.js.map +0 -1
  425. package/dist/daemon/connection.d.ts.map +0 -1
  426. package/dist/daemon/connection.js.map +0 -1
  427. package/dist/daemon/index.d.ts.map +0 -1
  428. package/dist/daemon/index.js.map +0 -1
  429. package/dist/daemon/orchestrator.d.ts.map +0 -1
  430. package/dist/daemon/orchestrator.js.map +0 -1
  431. package/dist/daemon/registry.d.ts.map +0 -1
  432. package/dist/daemon/registry.js.map +0 -1
  433. package/dist/daemon/router.d.ts.map +0 -1
  434. package/dist/daemon/router.js.map +0 -1
  435. package/dist/daemon/server.d.ts.map +0 -1
  436. package/dist/daemon/server.js.map +0 -1
  437. package/dist/daemon/types.d.ts.map +0 -1
  438. package/dist/daemon/types.js.map +0 -1
  439. package/dist/daemon/workspace-manager.d.ts.map +0 -1
  440. package/dist/daemon/workspace-manager.js.map +0 -1
  441. package/dist/dashboard/out/_next/static/chunks/693-7b3301d8f6bc5014.js +0 -1
  442. package/dist/dashboard/out/_next/static/chunks/713-f78477eb185f1f4d.js +0 -1
  443. package/dist/dashboard/out/_next/static/chunks/766-e53e1cfe39b0b5b5.js +0 -1
  444. package/dist/dashboard/out/_next/static/chunks/900-037c64bfd797fb2a.js +0 -1
  445. package/dist/dashboard/out/_next/static/chunks/app/app/page-e3d9e1f4466b9bae.js +0 -1
  446. package/dist/dashboard/out/_next/static/chunks/app/layout-2433bb48965f4333.js +0 -1
  447. package/dist/dashboard/out/_next/static/chunks/app/metrics/page-e68825a81db67ba1.js +0 -1
  448. package/dist/dashboard/out/_next/static/chunks/app/page-cc108bf68c8a657f.js +0 -1
  449. package/dist/dashboard/out/_next/static/chunks/app/pricing/page-d80e03a5297f95b6.js +0 -1
  450. package/dist/dashboard/out/_next/static/chunks/webpack-a5acc2831d094776.js +0 -1
  451. package/dist/dashboard/out/_next/static/css/79b80143647a07d7.css +0 -1
  452. package/dist/dashboard/out/_next/static/css/8cf277370ad48cfe.css +0 -1
  453. package/dist/dashboard-server/metrics.d.ts.map +0 -1
  454. package/dist/dashboard-server/metrics.js.map +0 -1
  455. package/dist/dashboard-server/needs-attention.d.ts.map +0 -1
  456. package/dist/dashboard-server/needs-attention.js.map +0 -1
  457. package/dist/dashboard-server/server.d.ts.map +0 -1
  458. package/dist/dashboard-server/server.js.map +0 -1
  459. package/dist/dashboard-server/start.d.ts.map +0 -1
  460. package/dist/dashboard-server/start.js.map +0 -1
  461. package/dist/hooks/inbox-check/hook.d.ts.map +0 -1
  462. package/dist/hooks/inbox-check/hook.js.map +0 -1
  463. package/dist/hooks/inbox-check/index.d.ts.map +0 -1
  464. package/dist/hooks/inbox-check/index.js.map +0 -1
  465. package/dist/hooks/inbox-check/types.d.ts.map +0 -1
  466. package/dist/hooks/inbox-check/types.js.map +0 -1
  467. package/dist/hooks/inbox-check/utils.d.ts.map +0 -1
  468. package/dist/hooks/inbox-check/utils.js.map +0 -1
  469. package/dist/hooks/index.d.ts.map +0 -1
  470. package/dist/hooks/index.js.map +0 -1
  471. package/dist/hooks/types.d.ts.map +0 -1
  472. package/dist/hooks/types.js.map +0 -1
  473. package/dist/index.d.ts.map +0 -1
  474. package/dist/index.js.map +0 -1
  475. package/dist/protocol/framing.d.ts.map +0 -1
  476. package/dist/protocol/framing.js.map +0 -1
  477. package/dist/protocol/index.d.ts.map +0 -1
  478. package/dist/protocol/index.js.map +0 -1
  479. package/dist/protocol/types.d.ts.map +0 -1
  480. package/dist/protocol/types.js.map +0 -1
  481. package/dist/resiliency/context-persistence.d.ts.map +0 -1
  482. package/dist/resiliency/context-persistence.js.map +0 -1
  483. package/dist/resiliency/health-monitor.d.ts.map +0 -1
  484. package/dist/resiliency/health-monitor.js.map +0 -1
  485. package/dist/resiliency/index.d.ts.map +0 -1
  486. package/dist/resiliency/index.js.map +0 -1
  487. package/dist/resiliency/logger.d.ts.map +0 -1
  488. package/dist/resiliency/logger.js.map +0 -1
  489. package/dist/resiliency/metrics.d.ts.map +0 -1
  490. package/dist/resiliency/metrics.js.map +0 -1
  491. package/dist/resiliency/provider-context.d.ts.map +0 -1
  492. package/dist/resiliency/provider-context.js.map +0 -1
  493. package/dist/resiliency/supervisor.d.ts.map +0 -1
  494. package/dist/resiliency/supervisor.js.map +0 -1
  495. package/dist/state/agent-state.d.ts.map +0 -1
  496. package/dist/state/agent-state.js.map +0 -1
  497. package/dist/storage/adapter.d.ts.map +0 -1
  498. package/dist/storage/adapter.js.map +0 -1
  499. package/dist/storage/sqlite-adapter.d.ts.map +0 -1
  500. package/dist/storage/sqlite-adapter.js.map +0 -1
  501. package/dist/utils/agent-config.d.ts.map +0 -1
  502. package/dist/utils/agent-config.js.map +0 -1
  503. package/dist/utils/command-resolver.d.ts.map +0 -1
  504. package/dist/utils/command-resolver.js.map +0 -1
  505. package/dist/utils/index.d.ts.map +0 -1
  506. package/dist/utils/index.js.map +0 -1
  507. package/dist/utils/logger.d.ts.map +0 -1
  508. package/dist/utils/logger.js.map +0 -1
  509. package/dist/utils/name-generator.d.ts.map +0 -1
  510. package/dist/utils/name-generator.js.map +0 -1
  511. package/dist/utils/project-namespace.d.ts.map +0 -1
  512. package/dist/utils/project-namespace.js.map +0 -1
  513. package/dist/utils/tmux-resolver.d.ts.map +0 -1
  514. package/dist/utils/tmux-resolver.js.map +0 -1
  515. package/dist/utils/update-checker.d.ts.map +0 -1
  516. package/dist/utils/update-checker.js.map +0 -1
  517. package/dist/wrapper/client.d.ts.map +0 -1
  518. package/dist/wrapper/client.js.map +0 -1
  519. package/dist/wrapper/inbox.d.ts.map +0 -1
  520. package/dist/wrapper/inbox.js.map +0 -1
  521. package/dist/wrapper/index.d.ts.map +0 -1
  522. package/dist/wrapper/index.js.map +0 -1
  523. package/dist/wrapper/parser.d.ts.map +0 -1
  524. package/dist/wrapper/parser.js.map +0 -1
  525. package/dist/wrapper/pty-wrapper.d.ts.map +0 -1
  526. package/dist/wrapper/pty-wrapper.js.map +0 -1
  527. package/dist/wrapper/tmux-wrapper.d.ts.map +0 -1
  528. package/dist/wrapper/tmux-wrapper.js.map +0 -1
  529. package/docs/AGENTS.md +0 -513
  530. package/docs/ARCHITECTURE_DECISIONS.md +0 -175
  531. package/docs/CHANGELOG.md +0 -11
  532. package/docs/CLI-SIMPLIFICATION-COMPLETE.md +0 -48
  533. package/docs/CLOUD-ARCHITECTURE.md +0 -652
  534. package/docs/CLOUD-ONBOARDING-DESIGN.md +0 -1983
  535. package/docs/COMPETITIVE_ANALYSIS.md +0 -897
  536. package/docs/CONTRIBUTING.md +0 -151
  537. package/docs/DESIGN_BRIDGE_STAFFING.md +0 -878
  538. package/docs/DESIGN_V2.md +0 -1079
  539. package/docs/INTEGRATION-GUIDE.md +0 -926
  540. package/docs/MONETIZATION.md +0 -1679
  541. package/docs/PROPOSAL-trajectories.md +0 -1582
  542. package/docs/PROTOCOL.md +0 -325
  543. package/docs/SCALING_ANALYSIS.md +0 -280
  544. package/docs/TESTING_PRESENCE_FEATURES.md +0 -327
  545. package/docs/TMUX_IMPLEMENTATION_NOTES.md +0 -364
  546. package/docs/TMUX_IMPROVEMENTS.md +0 -968
  547. package/docs/agent-relay-snippet.md +0 -168
  548. package/docs/competitive-analysis-mcp-agent-mail.md +0 -389
  549. package/docs/dashboard-v2-plan.md +0 -179
  550. package/docs/guides/CLOUD.md +0 -236
  551. package/docs/guides/LOCAL.md +0 -535
  552. package/docs/guides/SELF-HOSTED.md +0 -494
  553. package/docs/proposals/shadow-as-subagent.md +0 -765
  554. package/docs/proposals/slack-bot-integration.md +0 -1457
  555. package/docs/removable-code-analysis.md +0 -24
  556. package/scripts/dev/PUBLIC_RELEASE_PLAN.md +0 -88
  557. package/scripts/dev/dev-team-setup.sh +0 -431
  558. package/scripts/e2e-test.sh +0 -119
  559. package/scripts/games/game-protocol.md +0 -79
  560. package/scripts/games/hearts-setup.sh +0 -264
  561. package/scripts/tictactoe-setup.sh +0 -181
  562. /package/dist/dashboard/out/_next/static/chunks/{117-b2cd8d6485aacf2b.js → 117-f7b8ab0809342e77.js} +0 -0
  563. /package/dist/dashboard/out/_next/static/chunks/{648-8f3f26864ce515e5.js → 648-5cc6e1921389a58a.js} +0 -0
  564. /package/dist/dashboard/out/_next/static/chunks/app/_not-found/{page-0b990dbb71d72a98.js → page-53b8a69f76db17d0.js} +0 -0
  565. /package/dist/dashboard/out/_next/static/chunks/{fd9d1056-bf46c09eb57e019c.js → fd9d1056-609918ca7b6280bb.js} +0 -0
  566. /package/dist/dashboard/out/_next/static/{6HHWb2ZmnJ4OSm0zUP7h4 → wPgKJtcOmTFLpUncDg16A}/_buildManifest.js +0 -0
  567. /package/dist/dashboard/out/_next/static/{6HHWb2ZmnJ4OSm0zUP7h4 → wPgKJtcOmTFLpUncDg16A}/_ssgManifest.js +0 -0
@@ -1,1457 +0,0 @@
1
- # Slack Bot Integration Proposal
2
-
3
- **Author:** Claude
4
- **Date:** 2025-12-30
5
- **Status:** Draft
6
- **Estimated Effort:** 5-7 days
7
-
8
- ---
9
-
10
- ## Executive Summary
11
-
12
- This proposal outlines a plan to integrate agent-relay messaging with Slack, following the **cloud-first architecture** established in PR #35. The integration enables:
13
-
14
- - AI agents to communicate in Slack channels alongside humans
15
- - Humans to interact with agents via @mentions and slash commands
16
- - Real-time bidirectional sync between relay daemon and Slack
17
- - Thread preservation across both systems
18
- - **Cloud-managed OAuth and credentials** via Nango integration platform
19
- - **Multi-workspace support** through the daemon orchestrator
20
- - **Plan-based access** (Pro/Team/Enterprise tiers)
21
-
22
- ---
23
-
24
- ## Table of Contents
25
-
26
- 1. [Goals & Non-Goals](#1-goals--non-goals)
27
- 2. [Architecture Overview](#2-architecture-overview)
28
- 3. [Cloud Components](#3-cloud-components)
29
- 4. [Daemon Components](#4-daemon-components)
30
- 5. [Database Schema](#5-database-schema)
31
- 6. [Dashboard UI](#6-dashboard-ui)
32
- 7. [Implementation Phases](#7-implementation-phases)
33
- 8. [API Specifications](#8-api-specifications)
34
- 9. [Security & Plan Limits](#9-security--plan-limits)
35
- 10. [Testing Strategy](#10-testing-strategy)
36
- 11. [Open Questions](#11-open-questions)
37
-
38
- ---
39
-
40
- ## 1. Goals & Non-Goals
41
-
42
- ### Goals
43
-
44
- | Goal | Description |
45
- |------|-------------|
46
- | **Cloud-managed credentials** | Slack OAuth tokens stored in encrypted vault, not local files |
47
- | **Multi-workspace support** | Each workspace can have its own Slack integration |
48
- | **Bidirectional messaging** | Messages flow Slack ↔ Relay in real-time |
49
- | **Thread preservation** | Relay threads map to Slack threads and vice versa |
50
- | **Plan-gated access** | Slack integration available on Pro+ plans |
51
- | **Dashboard configuration** | Connect/disconnect Slack via UI |
52
- | **Self-hosted parity** | Works in cloud-hosted, self-hosted, and hybrid modes |
53
-
54
- ### Non-Goals (v1)
55
-
56
- | Non-Goal | Rationale |
57
- |----------|-----------|
58
- | Multi-Slack-workspace per relay workspace | Complexity; one Slack workspace per relay workspace |
59
- | Slack-only agents | Agents should exist in relay first |
60
- | Rich Block Kit formatting | Plain text first; enhance later |
61
- | Slack Enterprise Grid | Requires org-level OAuth; v2 |
62
-
63
- ---
64
-
65
- ## 2. Architecture Overview
66
-
67
- ### High-Level Design (Cloud Paradigm)
68
-
69
- ```
70
- ┌─────────────────────────────────────────────────────────────────────────────┐
71
- │ SLACK WORKSPACE │
72
- │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
73
- │ │ #agents │ │ #alerts │ │ @agent-bot │ │
74
- │ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │
75
- │ └────────────────┴────────────────┘ │
76
- │ │ │
77
- │ Slack Events API (Socket Mode) │
78
- └──────────────────────────┼──────────────────────────────────────────────────┘
79
-
80
-
81
- ┌──────────────────────────────────────────────────────────────────────────────┐
82
- │ AGENT RELAY CLOUD │
83
- │ │
84
- │ ┌────────────────────────────────────────────────────────────────────────┐ │
85
- │ │ src/cloud/ │ │
86
- │ │ ┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐ │ │
87
- │ │ │ api/integrations │ │ services/slack │ │ Nango │ │ │
88
- │ │ │ /slack.ts │ │ SlackService │ │ (external) │ │ │
89
- │ │ │ │ │ │ │ │ │ │
90
- │ │ │ • OAuth trigger │ │ • Token fetch │ │ • OAuth flow │ │ │
91
- │ │ │ • Disconnect │ │ • Workspace sync │ │ • Token storage │ │ │
92
- │ │ │ • Status │ │ • Health check │ │ • Auto refresh │ │ │
93
- │ │ └──────────────────┘ └──────────────────┘ └──────────────────┘ │ │
94
- │ │ │ │
95
- │ │ ┌──────────────────┐ ┌──────────────────┐ │ │
96
- │ │ │ db/schema.ts │ │ api/middleware/ │ │ │
97
- │ │ │ │ │ planLimits.ts │ │ │
98
- │ │ │ • slack_integrations│ • requirePro() │ │ │
99
- │ │ │ • slack_channels │ │ • checkSlackLimit│ │ │
100
- │ │ └──────────────────┘ └──────────────────┘ │ │
101
- │ └────────────────────────────────────────────────────────────────────────┘ │
102
- │ │ │
103
- │ Cloud Sync API │
104
- │ │ │
105
- └────────────────────────────────────┼────────────────────────────────────────┘
106
-
107
-
108
- ┌──────────────────────────────────────────────────────────────────────────────┐
109
- │ LOCAL DAEMON │
110
- │ │
111
- │ ┌────────────────────────────────────────────────────────────────────────┐ │
112
- │ │ src/daemon/ │ │
113
- │ │ ┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐ │ │
114
- │ │ │ orchestrator.ts │ │ slack-bridge.ts │ │ cloud-sync.ts │ │ │
115
- │ │ │ │ │ (NEW) │ │ │ │ │
116
- │ │ │ • Manages │ │ │ │ • Pulls Slack │ │ │
117
- │ │ │ workspaces │ │ • Slack ↔ Relay │ │ credentials │ │ │
118
- │ │ │ • Starts bridge │ │ • Thread mapping │ │ • Syncs config │ │ │
119
- │ │ │ per workspace │ │ • Event handling │ │ • Token refresh │ │ │
120
- │ │ └──────────────────┘ └──────────────────┘ └──────────────────┘ │ │
121
- │ │ │ │
122
- │ │ ┌──────────────────┐ ┌──────────────────┐ │ │
123
- │ │ │ router.ts │ │ agent-manager.ts │ │ │
124
- │ │ │ │ │ │ │ │
125
- │ │ │ • Routes msgs │ │ • Agent lifecycle│ │ │
126
- │ │ │ • SlackBridge │ │ • Health monitor │ │ │
127
- │ │ │ as agent │ │ │ │ │
128
- │ │ └──────────────────┘ └──────────────────┘ │ │
129
- │ └────────────────────────────────────────────────────────────────────────┘ │
130
- │ │ │
131
- │ Unix Domain Socket │
132
- │ │ │
133
- │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
134
- │ │ Alice │ │ Bob │ │ SlackBridge │ │ Dashboard │ │
135
- │ │ (Claude) │ │ (Claude) │ │ (daemon) │ │ (observer) │ │
136
- │ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ │
137
- └──────────────────────────────────────────────────────────────────────────────┘
138
- ```
139
-
140
- ### Deployment Model Alignment
141
-
142
- | Mode | How Slack Integration Works |
143
- |------|----------------------------|
144
- | **Cloud Hosted** | OAuth via cloud, credentials in vault, daemon bridge runs in cloud workspace |
145
- | **Self-Hosted** | OAuth via cloud servers, credentials synced to local daemon |
146
- | **Hybrid/Local** | OAuth via cloud, daemon runs locally with synced credentials |
147
-
148
- This follows the same pattern as provider credentials (Claude API keys, etc.) established in PR #35.
149
-
150
- ---
151
-
152
- ## 3. Cloud Components
153
-
154
- ### 3.1 Nango Integration
155
-
156
- [Nango](https://www.nango.dev/) handles all OAuth complexity:
157
- - OAuth flow (authorization URL, token exchange)
158
- - Secure token storage (encrypted at rest)
159
- - Automatic token refresh before expiry
160
- - Connection status monitoring
161
-
162
- **Nango Setup:**
163
- ```yaml
164
- # nango.yaml - Integration configuration
165
- integrations:
166
- slack:
167
- provider: slack
168
- syncs: []
169
- actions:
170
- - name: post-message
171
- - name: list-channels
172
- scopes:
173
- - app_mentions:read
174
- - channels:history
175
- - channels:read
176
- - chat:write
177
- - groups:history
178
- - groups:read
179
- - im:history
180
- - im:read
181
- - im:write
182
- - users:read
183
- ```
184
-
185
- ### 3.2 API Routes (`src/cloud/api/integrations/slack.ts`)
186
-
187
- ```typescript
188
- // src/cloud/api/integrations/slack.ts
189
-
190
- import { Router } from 'express';
191
- import { Nango } from '@nangohq/node';
192
- import { requireAuth } from '../middleware/auth';
193
- import { requirePlan } from '../middleware/planLimits';
194
- import { SlackService } from '../../services/slack';
195
- import { db, eq } from '../../db';
196
- import { slackIntegrations } from '../../db/schema';
197
-
198
- const router = Router();
199
- const nango = new Nango({ secretKey: process.env.NANGO_SECRET_KEY });
200
-
201
- // All Slack routes require Pro+ plan
202
- router.use(requireAuth, requirePlan('pro'));
203
-
204
- /**
205
- * GET /api/integrations/slack/status
206
- * Get Slack integration status for current workspace
207
- */
208
- router.get('/status', async (req, res) => {
209
- const { workspaceId } = req.query;
210
-
211
- const integration = await db.query.slackIntegrations.findFirst({
212
- where: eq(slackIntegrations.workspaceId, workspaceId),
213
- });
214
-
215
- if (!integration) {
216
- return res.json({ connected: false });
217
- }
218
-
219
- // Check Nango connection status
220
- try {
221
- const connection = await nango.getConnection('slack', integration.nangoConnectionId);
222
-
223
- res.json({
224
- connected: true,
225
- valid: connection.credentials?.access_token != null,
226
- slackWorkspace: integration.slackWorkspaceName,
227
- slackTeamId: integration.slackTeamId,
228
- channels: {
229
- broadcast: integration.broadcastChannel,
230
- alerts: integration.alertsChannel,
231
- },
232
- connectedAt: integration.createdAt,
233
- connectedBy: integration.connectedByUserId,
234
- });
235
- } catch (error) {
236
- res.json({ connected: true, valid: false, error: 'Token expired' });
237
- }
238
- });
239
-
240
- /**
241
- * GET /api/integrations/slack/connect
242
- * Get Nango connect URL for Slack OAuth
243
- */
244
- router.get('/connect', async (req, res) => {
245
- const { workspaceId } = req.query;
246
-
247
- // Generate unique connection ID for this workspace
248
- const connectionId = `workspace-${workspaceId}-slack`;
249
-
250
- // Nango handles the entire OAuth flow
251
- const connectUrl = await nango.auth('slack', connectionId, {
252
- detectClosedAuthWindow: true,
253
- });
254
-
255
- // Store pending connection
256
- await db.insert(slackIntegrations).values({
257
- id: generateId(),
258
- workspaceId,
259
- nangoConnectionId: connectionId,
260
- status: 'pending',
261
- connectedByUserId: req.user.id,
262
- }).onConflictDoUpdate({
263
- target: slackIntegrations.workspaceId,
264
- set: { status: 'pending', updatedAt: new Date() },
265
- });
266
-
267
- res.json({ connectUrl });
268
- });
269
-
270
- /**
271
- * POST /api/integrations/slack/webhook
272
- * Nango webhook for connection events
273
- */
274
- router.post('/webhook', async (req, res) => {
275
- const { type, connectionId, provider } = req.body;
276
-
277
- if (provider !== 'slack') {
278
- return res.json({ ok: true });
279
- }
280
-
281
- // Extract workspaceId from connectionId
282
- const match = connectionId.match(/^workspace-(.+)-slack$/);
283
- if (!match) {
284
- return res.status(400).json({ error: 'Invalid connection ID' });
285
- }
286
- const workspaceId = match[1];
287
-
288
- if (type === 'auth') {
289
- // OAuth completed - fetch team info
290
- const connection = await nango.getConnection('slack', connectionId);
291
- const teamInfo = connection.connection_config?.team;
292
-
293
- await db.update(slackIntegrations)
294
- .set({
295
- status: 'connected',
296
- slackTeamId: teamInfo?.id,
297
- slackWorkspaceName: teamInfo?.name,
298
- updatedAt: new Date(),
299
- })
300
- .where(eq(slackIntegrations.workspaceId, workspaceId));
301
-
302
- // Notify daemon to connect
303
- await SlackService.notifyDaemonConnect(workspaceId);
304
- }
305
-
306
- if (type === 'token_refreshed') {
307
- // Token was refreshed - notify daemon to reconnect
308
- await SlackService.notifyDaemonReconnect(workspaceId);
309
- }
310
-
311
- res.json({ ok: true });
312
- });
313
-
314
- /**
315
- * POST /api/integrations/slack/disconnect
316
- * Disconnect Slack integration
317
- */
318
- router.post('/disconnect', async (req, res) => {
319
- const { workspaceId } = req.body;
320
-
321
- const integration = await db.query.slackIntegrations.findFirst({
322
- where: eq(slackIntegrations.workspaceId, workspaceId),
323
- });
324
-
325
- if (!integration) {
326
- return res.status(404).json({ error: 'No Slack integration found' });
327
- }
328
-
329
- // Delete connection from Nango (revokes token)
330
- await nango.deleteConnection('slack', integration.nangoConnectionId);
331
-
332
- // Delete integration record
333
- await db.delete(slackIntegrations)
334
- .where(eq(slackIntegrations.id, integration.id));
335
-
336
- // Notify daemon to disconnect
337
- await SlackService.notifyDaemonDisconnect(workspaceId);
338
-
339
- res.json({ success: true });
340
- });
341
-
342
- /**
343
- * PUT /api/integrations/slack/config
344
- * Update Slack integration configuration
345
- */
346
- router.put('/config', async (req, res) => {
347
- const { workspaceId, broadcastChannel, alertsChannel, showAgentToAgent, showThinking } = req.body;
348
-
349
- await db.update(slackIntegrations)
350
- .set({
351
- broadcastChannel,
352
- alertsChannel,
353
- config: { showAgentToAgent, showThinking },
354
- updatedAt: new Date(),
355
- })
356
- .where(eq(slackIntegrations.workspaceId, workspaceId));
357
-
358
- // Notify daemon to reload config
359
- await SlackService.notifyDaemonConfigUpdate(workspaceId);
360
-
361
- res.json({ success: true });
362
- });
363
-
364
- /**
365
- * GET /api/integrations/slack/channels
366
- * List available Slack channels for configuration
367
- */
368
- router.get('/channels', async (req, res) => {
369
- const { workspaceId } = req.query;
370
-
371
- const channels = await SlackService.listChannels(workspaceId);
372
- res.json({ channels });
373
- });
374
-
375
- export default router;
376
- ```
377
-
378
- ### 3.3 Slack Service (`src/cloud/services/slack.ts`)
379
-
380
- ```typescript
381
- // src/cloud/services/slack.ts
382
-
383
- import { Nango } from '@nangohq/node';
384
- import { WebClient } from '@slack/web-api';
385
- import { db, eq } from '../db';
386
- import { slackIntegrations } from '../db/schema';
387
-
388
- const nango = new Nango({ secretKey: process.env.NANGO_SECRET_KEY });
389
-
390
- export class SlackService {
391
- /**
392
- * Get Slack access token from Nango
393
- * Nango automatically refreshes expired tokens
394
- */
395
- static async getAccessToken(workspaceId: string): Promise<string | null> {
396
- const integration = await db.query.slackIntegrations.findFirst({
397
- where: eq(slackIntegrations.workspaceId, workspaceId),
398
- });
399
-
400
- if (!integration?.nangoConnectionId) return null;
401
-
402
- try {
403
- const connection = await nango.getConnection('slack', integration.nangoConnectionId);
404
- return connection.credentials?.access_token || null;
405
- } catch {
406
- return null;
407
- }
408
- }
409
-
410
- /**
411
- * List channels the bot can access
412
- */
413
- static async listChannels(workspaceId: string): Promise<SlackChannel[]> {
414
- const token = await this.getAccessToken(workspaceId);
415
- if (!token) throw new Error('No Slack connection found');
416
-
417
- const client = new WebClient(token);
418
- const result = await client.conversations.list({
419
- types: 'public_channel,private_channel',
420
- exclude_archived: true,
421
- });
422
-
423
- return result.channels?.map(ch => ({
424
- id: ch.id!,
425
- name: ch.name!,
426
- isPrivate: ch.is_private || false,
427
- isMember: ch.is_member || false,
428
- })) || [];
429
- }
430
-
431
- /**
432
- * Get credentials for daemon sync
433
- * Called by daemon cloud-sync to retrieve Slack config
434
- */
435
- static async getCredentialsForDaemon(workspaceId: string): Promise<SlackDaemonConfig | null> {
436
- const integration = await db.query.slackIntegrations.findFirst({
437
- where: eq(slackIntegrations.workspaceId, workspaceId),
438
- });
439
-
440
- if (!integration?.nangoConnectionId) return null;
441
-
442
- try {
443
- const connection = await nango.getConnection('slack', integration.nangoConnectionId);
444
-
445
- if (!connection.credentials?.access_token) return null;
446
-
447
- return {
448
- botToken: connection.credentials.access_token,
449
- // App token for Socket Mode - stored in connection metadata or env
450
- appToken: process.env.SLACK_APP_TOKEN,
451
- teamId: integration.slackTeamId!,
452
- teamName: integration.slackWorkspaceName!,
453
- broadcastChannel: integration.broadcastChannel,
454
- alertsChannel: integration.alertsChannel,
455
- config: integration.config || {},
456
- };
457
- } catch {
458
- return null;
459
- }
460
- }
461
-
462
- /**
463
- * Notify daemon of Slack connection changes
464
- */
465
- static async notifyDaemonConnect(workspaceId: string): Promise<void> {
466
- await this.sendDaemonNotification(workspaceId, 'slack:connect');
467
- }
468
-
469
- static async notifyDaemonDisconnect(workspaceId: string): Promise<void> {
470
- await this.sendDaemonNotification(workspaceId, 'slack:disconnect');
471
- }
472
-
473
- static async notifyDaemonConfigUpdate(workspaceId: string): Promise<void> {
474
- await this.sendDaemonNotification(workspaceId, 'slack:config-update');
475
- }
476
-
477
- static async notifyDaemonReconnect(workspaceId: string): Promise<void> {
478
- await this.sendDaemonNotification(workspaceId, 'slack:reconnect');
479
- }
480
-
481
- private static async sendDaemonNotification(workspaceId: string, event: string): Promise<void> {
482
- // Implementation depends on daemon connection method
483
- // Could be WebSocket push, Redis pub/sub, or polling
484
- }
485
- }
486
-
487
- interface SlackChannel {
488
- id: string;
489
- name: string;
490
- isPrivate: boolean;
491
- isMember: boolean;
492
- }
493
-
494
- interface SlackDaemonConfig {
495
- botToken: string;
496
- appToken: string;
497
- teamId: string;
498
- teamName: string;
499
- broadcastChannel: string | null;
500
- alertsChannel: string | null;
501
- config: {
502
- showAgentToAgent?: boolean;
503
- showThinking?: boolean;
504
- };
505
- }
506
- ```
507
-
508
- ### 3.4 Why Nango?
509
-
510
- | Concern | Without Nango | With Nango |
511
- |---------|---------------|------------|
512
- | OAuth flow | Custom implementation | Hosted UI, handles edge cases |
513
- | Token storage | Custom vault, encryption | Encrypted at rest, compliant |
514
- | Token refresh | Manual refresh logic | Automatic, before expiry |
515
- | Multiple providers | Per-provider code | Unified API |
516
- | Maintenance | Security updates needed | Managed service |
517
-
518
- **Dependencies:**
519
- ```json
520
- {
521
- "dependencies": {
522
- "@nangohq/node": "^0.40.0"
523
- }
524
- }
525
- ```
526
-
527
- ---
528
-
529
- ## 4. Daemon Components
530
-
531
- ### 4.1 Slack Bridge (`src/daemon/slack-bridge.ts`)
532
-
533
- ```typescript
534
- // src/daemon/slack-bridge.ts
535
-
536
- import { App, LogLevel } from '@slack/bolt';
537
- import { RelayClient } from '../wrapper/client';
538
- import { SlackThreadStore } from './slack-thread-store';
539
- import { SlackMessageFormatter } from './slack-formatter';
540
- import { logger } from '../resiliency/logger';
541
-
542
- export interface SlackBridgeConfig {
543
- botToken: string;
544
- appToken: string;
545
- teamId: string;
546
- teamName: string;
547
- broadcastChannel: string | null;
548
- alertsChannel: string | null;
549
- config: {
550
- showAgentToAgent?: boolean;
551
- showThinking?: boolean;
552
- };
553
- // Relay connection
554
- socketPath: string;
555
- workspaceId: string;
556
- }
557
-
558
- export class SlackBridge {
559
- private slackApp: App | null = null;
560
- private relayClient: RelayClient | null = null;
561
- private threadStore: SlackThreadStore;
562
- private formatter: SlackMessageFormatter;
563
- private config: SlackBridgeConfig;
564
- private running = false;
565
-
566
- constructor(config: SlackBridgeConfig) {
567
- this.config = config;
568
- this.threadStore = new SlackThreadStore();
569
- this.formatter = new SlackMessageFormatter();
570
- }
571
-
572
- async start(): Promise<void> {
573
- if (this.running) return;
574
-
575
- logger.info('Starting Slack bridge', {
576
- workspaceId: this.config.workspaceId,
577
- slackTeam: this.config.teamName,
578
- });
579
-
580
- // 1. Initialize Slack App (Socket Mode)
581
- this.slackApp = new App({
582
- token: this.config.botToken,
583
- appToken: this.config.appToken,
584
- socketMode: true,
585
- logLevel: LogLevel.WARN,
586
- });
587
-
588
- // 2. Register event handlers
589
- this.registerSlackHandlers();
590
-
591
- // 3. Connect to local relay daemon
592
- this.relayClient = new RelayClient({
593
- socketPath: this.config.socketPath,
594
- agentName: 'SlackBridge',
595
- cli: 'slack',
596
- reconnect: true,
597
- });
598
-
599
- this.relayClient.onMessage = this.onRelayMessage.bind(this);
600
- this.relayClient.onStateChange = this.onRelayStateChange.bind(this);
601
-
602
- await this.relayClient.connect();
603
-
604
- // 4. Subscribe to all messages
605
- this.relayClient.subscribe('*');
606
-
607
- // 5. Start Slack app
608
- await this.slackApp.start();
609
-
610
- this.running = true;
611
- logger.info('Slack bridge started successfully');
612
- }
613
-
614
- async stop(): Promise<void> {
615
- if (!this.running) return;
616
-
617
- logger.info('Stopping Slack bridge');
618
-
619
- if (this.slackApp) {
620
- await this.slackApp.stop();
621
- this.slackApp = null;
622
- }
623
-
624
- if (this.relayClient) {
625
- await this.relayClient.disconnect();
626
- this.relayClient = null;
627
- }
628
-
629
- this.running = false;
630
- }
631
-
632
- async updateConfig(newConfig: Partial<SlackBridgeConfig>): Promise<void> {
633
- // Update config without full restart for channel changes
634
- this.config = { ...this.config, ...newConfig };
635
- logger.info('Slack bridge config updated', { workspaceId: this.config.workspaceId });
636
- }
637
-
638
- isRunning(): boolean {
639
- return this.running;
640
- }
641
-
642
- // ─────────────────────────────────────────────────────────────
643
- // Slack → Relay handlers
644
- // ─────────────────────────────────────────────────────────────
645
-
646
- private registerSlackHandlers(): void {
647
- if (!this.slackApp) return;
648
-
649
- // Handle @mentions
650
- this.slackApp.event('app_mention', async ({ event, say }) => {
651
- await this.handleMention(event, say);
652
- });
653
-
654
- // Handle DMs
655
- this.slackApp.event('message', async ({ event, say }) => {
656
- if (event.channel_type === 'im' && !event.bot_id) {
657
- await this.handleDirectMessage(event, say);
658
- }
659
- });
660
-
661
- // Handle thread replies
662
- this.slackApp.event('message', async ({ event }) => {
663
- if (event.thread_ts && event.thread_ts !== event.ts && !event.bot_id) {
664
- await this.handleThreadReply(event);
665
- }
666
- });
667
-
668
- // Slash command
669
- this.slackApp.command('/relay', async ({ command, ack, respond }) => {
670
- await ack();
671
- await this.handleSlashCommand(command, respond);
672
- });
673
- }
674
-
675
- private async handleMention(event: any, say: Function): Promise<void> {
676
- // Extract agent name from message (e.g., "@relay Alice please help")
677
- const agentMatch = event.text.match(/<@[A-Z0-9]+>\s*@?(\w+)\s*(.*)/s);
678
-
679
- if (!agentMatch) {
680
- await say({
681
- text: 'Usage: @AgentRelay AgentName your message',
682
- thread_ts: event.ts,
683
- });
684
- return;
685
- }
686
-
687
- const [, agentName, messageBody] = agentMatch;
688
- const slackUser = await this.resolveUser(event.user);
689
- const thread = this.threadStore.getOrCreate(event.thread_ts || event.ts, event.channel);
690
-
691
- this.relayClient?.sendMessage(
692
- agentName,
693
- messageBody.trim(),
694
- 'message',
695
- {
696
- slack_user: slackUser,
697
- slack_channel: event.channel,
698
- slack_ts: event.ts,
699
- slack_thread_ts: event.thread_ts,
700
- },
701
- thread
702
- );
703
-
704
- logger.debug('Forwarded Slack mention to relay', { agentName, slackUser });
705
- }
706
-
707
- private async handleDirectMessage(event: any, say: Function): Promise<void> {
708
- // Parse agent target from DM: "Alice: help me" or just broadcast
709
- const match = event.text.match(/^@?(\w+):\s*(.+)$/s);
710
-
711
- const agentName = match ? match[1] : '*';
712
- const messageBody = match ? match[2] : event.text;
713
- const slackUser = await this.resolveUser(event.user);
714
-
715
- this.relayClient?.sendMessage(
716
- agentName,
717
- messageBody.trim(),
718
- 'message',
719
- {
720
- slack_user: slackUser,
721
- slack_channel: event.channel,
722
- slack_ts: event.ts,
723
- slack_dm: true,
724
- }
725
- );
726
- }
727
-
728
- private async handleThreadReply(event: any): Promise<void> {
729
- const relayThread = this.threadStore.getRelayThread(event.thread_ts, event.channel);
730
- if (!relayThread) return; // Not a relay thread
731
-
732
- const slackUser = await this.resolveUser(event.user);
733
- const threadMeta = this.threadStore.getMeta(event.thread_ts, event.channel);
734
-
735
- this.relayClient?.sendMessage(
736
- threadMeta?.targetAgent || '*',
737
- event.text,
738
- 'message',
739
- {
740
- slack_user: slackUser,
741
- slack_thread_ts: event.thread_ts,
742
- },
743
- relayThread
744
- );
745
- }
746
-
747
- private async handleSlashCommand(command: any, respond: Function): Promise<void> {
748
- const match = command.text.match(/^@?(\w+)\s+(.+)$/s);
749
-
750
- if (!match) {
751
- await respond('Usage: /relay @AgentName your message');
752
- return;
753
- }
754
-
755
- const [, agentName, messageBody] = match;
756
- const slackUser = await this.resolveUser(command.user_id);
757
-
758
- this.relayClient?.sendMessage(
759
- agentName,
760
- messageBody.trim(),
761
- 'message',
762
- {
763
- slack_user: slackUser,
764
- slack_channel: command.channel_id,
765
- slack_command: true,
766
- }
767
- );
768
-
769
- await respond(`Message sent to ${agentName}`);
770
- }
771
-
772
- // ─────────────────────────────────────────────────────────────
773
- // Relay → Slack handlers
774
- // ─────────────────────────────────────────────────────────────
775
-
776
- private async onRelayMessage(
777
- from: string,
778
- payload: { kind: string; body: string; data?: Record<string, unknown>; thread?: string },
779
- messageId: string,
780
- meta?: { importance?: number }
781
- ): Promise<void> {
782
- // Skip self-messages
783
- if (from === 'SlackBridge') return;
784
-
785
- // Skip messages originating from Slack (prevent loop)
786
- if (payload.data?.slack_ts) return;
787
-
788
- // Skip thinking unless configured
789
- if (payload.kind === 'thinking' && !this.config.config.showThinking) return;
790
-
791
- // Determine channel
792
- let channel = this.config.broadcastChannel;
793
- let threadTs: string | undefined;
794
-
795
- // Reply to Slack conversation
796
- if (payload.data?.slack_channel) {
797
- channel = payload.data.slack_channel as string;
798
- threadTs = payload.data.slack_thread_ts as string;
799
- }
800
- // High importance → alerts channel
801
- else if (meta?.importance && meta.importance >= 80 && this.config.alertsChannel) {
802
- channel = this.config.alertsChannel;
803
- }
804
- // Map relay thread to Slack thread
805
- else if (payload.thread) {
806
- const slackThread = this.threadStore.getSlackThread(payload.thread);
807
- if (slackThread) {
808
- channel = slackThread.channel;
809
- threadTs = slackThread.ts;
810
- }
811
- }
812
-
813
- if (!channel) {
814
- logger.warn('No channel configured for Slack message');
815
- return;
816
- }
817
-
818
- const formatted = this.formatter.format(from, payload);
819
-
820
- try {
821
- const result = await this.slackApp?.client.chat.postMessage({
822
- channel,
823
- text: formatted,
824
- thread_ts: threadTs,
825
- unfurl_links: false,
826
- unfurl_media: false,
827
- });
828
-
829
- // Track thread mapping
830
- if (result?.ts && payload.thread) {
831
- this.threadStore.map(payload.thread, {
832
- ts: result.ts,
833
- channel,
834
- targetAgent: from,
835
- });
836
- }
837
- } catch (error) {
838
- logger.error('Failed to post to Slack', { error, channel });
839
- }
840
- }
841
-
842
- private onRelayStateChange(state: string): void {
843
- logger.info('Slack bridge relay connection state', { state });
844
- }
845
-
846
- private async resolveUser(userId: string): Promise<string> {
847
- try {
848
- const result = await this.slackApp?.client.users.info({ user: userId });
849
- return result?.user?.real_name || result?.user?.name || userId;
850
- } catch {
851
- return userId;
852
- }
853
- }
854
- }
855
- ```
856
-
857
- ### 4.2 Integration with Orchestrator (`src/daemon/orchestrator.ts`)
858
-
859
- ```typescript
860
- // Addition to src/daemon/orchestrator.ts
861
-
862
- import { SlackBridge, SlackBridgeConfig } from './slack-bridge';
863
-
864
- export class DaemonOrchestrator {
865
- private slackBridges: Map<string, SlackBridge> = new Map();
866
-
867
- // Called during workspace initialization or when Slack is connected
868
- async initializeSlackBridge(workspaceId: string): Promise<void> {
869
- // Check if already running
870
- if (this.slackBridges.has(workspaceId)) {
871
- return;
872
- }
873
-
874
- // Get Slack config from cloud sync
875
- const slackConfig = await this.cloudSync.getSlackConfig(workspaceId);
876
- if (!slackConfig) {
877
- logger.debug('No Slack integration for workspace', { workspaceId });
878
- return;
879
- }
880
-
881
- const bridge = new SlackBridge({
882
- ...slackConfig,
883
- socketPath: this.getSocketPath(workspaceId),
884
- workspaceId,
885
- });
886
-
887
- try {
888
- await bridge.start();
889
- this.slackBridges.set(workspaceId, bridge);
890
- logger.info('Slack bridge initialized', { workspaceId });
891
- } catch (error) {
892
- logger.error('Failed to initialize Slack bridge', { workspaceId, error });
893
- }
894
- }
895
-
896
- async stopSlackBridge(workspaceId: string): Promise<void> {
897
- const bridge = this.slackBridges.get(workspaceId);
898
- if (bridge) {
899
- await bridge.stop();
900
- this.slackBridges.delete(workspaceId);
901
- logger.info('Slack bridge stopped', { workspaceId });
902
- }
903
- }
904
-
905
- // Called when cloud sync receives Slack notification
906
- async handleSlackNotification(workspaceId: string, event: string): Promise<void> {
907
- switch (event) {
908
- case 'slack:connect':
909
- await this.initializeSlackBridge(workspaceId);
910
- break;
911
- case 'slack:disconnect':
912
- await this.stopSlackBridge(workspaceId);
913
- break;
914
- case 'slack:config-update':
915
- const bridge = this.slackBridges.get(workspaceId);
916
- if (bridge) {
917
- const newConfig = await this.cloudSync.getSlackConfig(workspaceId);
918
- if (newConfig) {
919
- await bridge.updateConfig(newConfig);
920
- }
921
- }
922
- break;
923
- }
924
- }
925
-
926
- // Health check includes Slack bridges
927
- getHealth(): DaemonHealth {
928
- return {
929
- ...this.baseHealth(),
930
- slackBridges: Array.from(this.slackBridges.entries()).map(([id, bridge]) => ({
931
- workspaceId: id,
932
- running: bridge.isRunning(),
933
- })),
934
- };
935
- }
936
- }
937
- ```
938
-
939
- ### 4.3 Cloud Sync Extension (`src/daemon/cloud-sync.ts`)
940
-
941
- ```typescript
942
- // Addition to src/daemon/cloud-sync.ts
943
-
944
- export class CloudSync {
945
- /**
946
- * Get Slack configuration for a workspace
947
- * Called by orchestrator when initializing Slack bridge
948
- */
949
- async getSlackConfig(workspaceId: string): Promise<SlackBridgeConfig | null> {
950
- try {
951
- const response = await this.apiClient.get(
952
- `/api/integrations/slack/daemon-config?workspaceId=${workspaceId}`
953
- );
954
-
955
- if (!response.data.connected) {
956
- return null;
957
- }
958
-
959
- return response.data.config;
960
- } catch (error) {
961
- logger.error('Failed to fetch Slack config', { workspaceId, error });
962
- return null;
963
- }
964
- }
965
-
966
- /**
967
- * Subscribe to Slack notifications
968
- */
969
- subscribeToSlackNotifications(callback: (workspaceId: string, event: string) => void): void {
970
- // WebSocket subscription or polling
971
- this.on('slack:notification', callback);
972
- }
973
- }
974
- ```
975
-
976
- ---
977
-
978
- ## 5. Database Schema
979
-
980
- ### 5.1 Drizzle Schema (`src/cloud/db/schema.ts`)
981
-
982
- ```typescript
983
- // Addition to src/cloud/db/schema.ts
984
-
985
- import { pgTable, text, timestamp, jsonb, boolean } from 'drizzle-orm/pg-core';
986
-
987
- export const slackIntegrations = pgTable('slack_integrations', {
988
- id: text('id').primaryKey(),
989
- workspaceId: text('workspace_id').notNull().references(() => workspaces.id, { onDelete: 'cascade' }),
990
-
991
- // Nango connection (handles OAuth, token storage, refresh)
992
- nangoConnectionId: text('nango_connection_id').notNull(),
993
- status: text('status').notNull().default('pending'), // pending, connected, error
994
-
995
- // Slack workspace info (populated after OAuth)
996
- slackTeamId: text('slack_team_id'),
997
- slackWorkspaceName: text('slack_workspace_name'),
998
-
999
- // Channel configuration
1000
- broadcastChannel: text('broadcast_channel'),
1001
- alertsChannel: text('alerts_channel'),
1002
-
1003
- // Behavior configuration
1004
- config: jsonb('config').$type<{
1005
- showAgentToAgent?: boolean;
1006
- showThinking?: boolean;
1007
- threadTTLHours?: number;
1008
- }>().default({}),
1009
-
1010
- // Metadata
1011
- connectedByUserId: text('connected_by_user_id').references(() => users.id),
1012
- createdAt: timestamp('created_at').defaultNow().notNull(),
1013
- updatedAt: timestamp('updated_at').defaultNow().notNull(),
1014
- });
1015
-
1016
- export const slackChannelMappings = pgTable('slack_channel_mappings', {
1017
- id: text('id').primaryKey(),
1018
- integrationId: text('integration_id').notNull().references(() => slackIntegrations.id, { onDelete: 'cascade' }),
1019
-
1020
- // Relay topic → Slack channel mapping
1021
- relayTopic: text('relay_topic').notNull(),
1022
- slackChannelId: text('slack_channel_id').notNull(),
1023
- slackChannelName: text('slack_channel_name'),
1024
-
1025
- createdAt: timestamp('created_at').defaultNow().notNull(),
1026
- });
1027
-
1028
- // Index for fast lookup
1029
- export const slackIntegrationsWorkspaceIdx = index('slack_integrations_workspace_idx')
1030
- .on(slackIntegrations.workspaceId);
1031
- ```
1032
-
1033
- ### 5.2 SQL Migration
1034
-
1035
- ```sql
1036
- -- deploy/migrations/004_slack_integrations.sql
1037
-
1038
- CREATE TABLE slack_integrations (
1039
- id TEXT PRIMARY KEY,
1040
- workspace_id TEXT NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
1041
- nango_connection_id TEXT NOT NULL,
1042
- status TEXT NOT NULL DEFAULT 'pending',
1043
- slack_team_id TEXT,
1044
- slack_workspace_name TEXT,
1045
- broadcast_channel TEXT,
1046
- alerts_channel TEXT,
1047
- config JSONB DEFAULT '{}',
1048
- connected_by_user_id TEXT REFERENCES users(id),
1049
- created_at TIMESTAMP DEFAULT NOW() NOT NULL,
1050
- updated_at TIMESTAMP DEFAULT NOW() NOT NULL
1051
- );
1052
-
1053
- CREATE INDEX slack_integrations_workspace_idx ON slack_integrations(workspace_id);
1054
-
1055
- CREATE TABLE slack_channel_mappings (
1056
- id TEXT PRIMARY KEY,
1057
- integration_id TEXT NOT NULL REFERENCES slack_integrations(id) ON DELETE CASCADE,
1058
- relay_topic TEXT NOT NULL,
1059
- slack_channel_id TEXT NOT NULL,
1060
- slack_channel_name TEXT,
1061
- created_at TIMESTAMP DEFAULT NOW() NOT NULL
1062
- );
1063
-
1064
- CREATE INDEX slack_channel_mappings_integration_idx ON slack_channel_mappings(integration_id);
1065
- ```
1066
-
1067
- ---
1068
-
1069
- ## 6. Dashboard UI
1070
-
1071
- ### 6.1 Slack Integration Panel (`src/dashboard/react-components/SlackIntegrationPanel.tsx`)
1072
-
1073
- ```typescript
1074
- // src/dashboard/react-components/SlackIntegrationPanel.tsx
1075
-
1076
- import React, { useState, useEffect } from 'react';
1077
- import { useSession } from './hooks/useSession';
1078
- import { api } from '../lib/api';
1079
-
1080
- interface SlackStatus {
1081
- connected: boolean;
1082
- valid?: boolean;
1083
- slackWorkspace?: string;
1084
- channels?: {
1085
- broadcast: string | null;
1086
- alerts: string | null;
1087
- };
1088
- connectedAt?: string;
1089
- }
1090
-
1091
- export function SlackIntegrationPanel({ workspaceId }: { workspaceId: string }) {
1092
- const { plan } = useSession();
1093
- const [status, setStatus] = useState<SlackStatus | null>(null);
1094
- const [channels, setChannels] = useState<{ id: string; name: string }[]>([]);
1095
- const [loading, setLoading] = useState(true);
1096
- const [configuring, setConfiguring] = useState(false);
1097
-
1098
- // Config form state
1099
- const [broadcastChannel, setBroadcastChannel] = useState('');
1100
- const [alertsChannel, setAlertsChannel] = useState('');
1101
- const [showAgentToAgent, setShowAgentToAgent] = useState(true);
1102
-
1103
- useEffect(() => {
1104
- loadStatus();
1105
- }, [workspaceId]);
1106
-
1107
- async function loadStatus() {
1108
- setLoading(true);
1109
- try {
1110
- const res = await api.get(`/api/integrations/slack/status?workspaceId=${workspaceId}`);
1111
- setStatus(res.data);
1112
- if (res.data.connected) {
1113
- setBroadcastChannel(res.data.channels?.broadcast || '');
1114
- setAlertsChannel(res.data.channels?.alerts || '');
1115
- loadChannels();
1116
- }
1117
- } catch (error) {
1118
- console.error('Failed to load Slack status', error);
1119
- }
1120
- setLoading(false);
1121
- }
1122
-
1123
- async function loadChannels() {
1124
- try {
1125
- const res = await api.get(`/api/integrations/slack/channels?workspaceId=${workspaceId}`);
1126
- setChannels(res.data.channels);
1127
- } catch (error) {
1128
- console.error('Failed to load channels', error);
1129
- }
1130
- }
1131
-
1132
- async function handleConnect() {
1133
- try {
1134
- const res = await api.get(`/api/integrations/slack/oauth/start?workspaceId=${workspaceId}`);
1135
- window.location.href = res.data.authUrl;
1136
- } catch (error) {
1137
- console.error('Failed to start OAuth', error);
1138
- }
1139
- }
1140
-
1141
- async function handleDisconnect() {
1142
- if (!confirm('Disconnect Slack integration?')) return;
1143
-
1144
- try {
1145
- await api.post('/api/integrations/slack/disconnect', { workspaceId });
1146
- setStatus({ connected: false });
1147
- } catch (error) {
1148
- console.error('Failed to disconnect', error);
1149
- }
1150
- }
1151
-
1152
- async function handleSaveConfig() {
1153
- setConfiguring(true);
1154
- try {
1155
- await api.put('/api/integrations/slack/config', {
1156
- workspaceId,
1157
- broadcastChannel,
1158
- alertsChannel,
1159
- showAgentToAgent,
1160
- });
1161
- await loadStatus();
1162
- } catch (error) {
1163
- console.error('Failed to save config', error);
1164
- }
1165
- setConfiguring(false);
1166
- }
1167
-
1168
- // Plan check
1169
- if (plan === 'free') {
1170
- return (
1171
- <div className="slack-panel disabled">
1172
- <h3>Slack Integration</h3>
1173
- <p>Slack integration is available on Pro plans and above.</p>
1174
- <a href="/pricing" className="upgrade-btn">Upgrade to Pro</a>
1175
- </div>
1176
- );
1177
- }
1178
-
1179
- if (loading) {
1180
- return <div className="slack-panel loading">Loading...</div>;
1181
- }
1182
-
1183
- return (
1184
- <div className="slack-panel">
1185
- <h3>Slack Integration</h3>
1186
-
1187
- {!status?.connected ? (
1188
- <div className="slack-connect">
1189
- <p>Connect Slack to see agent messages in your workspace.</p>
1190
- <button onClick={handleConnect} className="connect-btn">
1191
- <SlackLogo /> Connect to Slack
1192
- </button>
1193
- </div>
1194
- ) : (
1195
- <div className="slack-connected">
1196
- <div className="status-row">
1197
- <span className={`status-dot ${status.valid ? 'green' : 'red'}`} />
1198
- <span>Connected to <strong>{status.slackWorkspace}</strong></span>
1199
- <button onClick={handleDisconnect} className="disconnect-btn">Disconnect</button>
1200
- </div>
1201
-
1202
- <div className="config-section">
1203
- <h4>Channel Configuration</h4>
1204
-
1205
- <label>
1206
- Broadcast Channel
1207
- <select value={broadcastChannel} onChange={e => setBroadcastChannel(e.target.value)}>
1208
- <option value="">Select channel...</option>
1209
- {channels.map(ch => (
1210
- <option key={ch.id} value={ch.id}>#{ch.name}</option>
1211
- ))}
1212
- </select>
1213
- </label>
1214
-
1215
- <label>
1216
- Alerts Channel (optional)
1217
- <select value={alertsChannel} onChange={e => setAlertsChannel(e.target.value)}>
1218
- <option value="">None</option>
1219
- {channels.map(ch => (
1220
- <option key={ch.id} value={ch.id}>#{ch.name}</option>
1221
- ))}
1222
- </select>
1223
- </label>
1224
-
1225
- <label className="checkbox">
1226
- <input
1227
- type="checkbox"
1228
- checked={showAgentToAgent}
1229
- onChange={e => setShowAgentToAgent(e.target.checked)}
1230
- />
1231
- Show agent-to-agent messages
1232
- </label>
1233
-
1234
- <button onClick={handleSaveConfig} disabled={configuring}>
1235
- {configuring ? 'Saving...' : 'Save Configuration'}
1236
- </button>
1237
- </div>
1238
- </div>
1239
- )}
1240
- </div>
1241
- );
1242
- }
1243
- ```
1244
-
1245
- ### 6.2 Integration into Settings Page
1246
-
1247
- ```typescript
1248
- // In src/dashboard/app/workspace/[id]/settings/page.tsx
1249
-
1250
- import { SlackIntegrationPanel } from '@/react-components/SlackIntegrationPanel';
1251
-
1252
- export default function WorkspaceSettings({ params }) {
1253
- return (
1254
- <div className="settings-page">
1255
- <h2>Workspace Settings</h2>
1256
-
1257
- {/* Other settings... */}
1258
-
1259
- <section className="integrations-section">
1260
- <h3>Integrations</h3>
1261
- <SlackIntegrationPanel workspaceId={params.id} />
1262
- {/* Future: Discord, Teams, etc. */}
1263
- </section>
1264
- </div>
1265
- );
1266
- }
1267
- ```
1268
-
1269
- ---
1270
-
1271
- ## 7. Implementation Phases
1272
-
1273
- ### Phase 1: Cloud Infrastructure (Days 1-2)
1274
-
1275
- - [ ] Database schema and migration
1276
- - [ ] Slack OAuth API routes
1277
- - [ ] SlackService for token management
1278
- - [ ] Vault integration for credentials
1279
- - [ ] Plan limit middleware
1280
-
1281
- ### Phase 2: Daemon Bridge (Days 2-4)
1282
-
1283
- - [ ] SlackBridge class (Slack ↔ Relay)
1284
- - [ ] Thread store for mapping
1285
- - [ ] Message formatter
1286
- - [ ] Orchestrator integration
1287
- - [ ] Cloud sync for credentials
1288
-
1289
- ### Phase 3: Dashboard UI (Days 4-5)
1290
-
1291
- - [ ] SlackIntegrationPanel component
1292
- - [ ] OAuth flow UI
1293
- - [ ] Channel configuration
1294
- - [ ] Status display
1295
-
1296
- ### Phase 4: Testing & Polish (Days 5-7)
1297
-
1298
- - [ ] Unit tests for services
1299
- - [ ] Integration tests
1300
- - [ ] E2E flow testing
1301
- - [ ] Documentation
1302
- - [ ] Error handling & edge cases
1303
-
1304
- ---
1305
-
1306
- ## 8. API Specifications
1307
-
1308
- ### 8.1 Cloud API Endpoints
1309
-
1310
- | Method | Path | Description | Auth |
1311
- |--------|------|-------------|------|
1312
- | GET | `/api/integrations/slack/status` | Get integration status | Pro+ |
1313
- | GET | `/api/integrations/slack/oauth/start` | Start OAuth flow | Pro+ |
1314
- | GET | `/api/integrations/slack/oauth/callback` | OAuth callback | Pro+ |
1315
- | POST | `/api/integrations/slack/disconnect` | Remove integration | Pro+ |
1316
- | PUT | `/api/integrations/slack/config` | Update config | Pro+ |
1317
- | GET | `/api/integrations/slack/channels` | List Slack channels | Pro+ |
1318
- | GET | `/api/integrations/slack/daemon-config` | Get config for daemon | Internal |
1319
-
1320
- ### 8.2 Daemon Sync API
1321
-
1322
- ```typescript
1323
- // Called by daemon cloud-sync
1324
- GET /api/integrations/slack/daemon-config?workspaceId=xxx
1325
- Authorization: Bearer <daemon-token>
1326
-
1327
- Response:
1328
- {
1329
- "connected": true,
1330
- "config": {
1331
- "botToken": "xoxb-...",
1332
- "appToken": "xapp-...",
1333
- "teamId": "T123",
1334
- "teamName": "My Workspace",
1335
- "broadcastChannel": "C456",
1336
- "alertsChannel": null,
1337
- "config": { "showAgentToAgent": true }
1338
- }
1339
- }
1340
- ```
1341
-
1342
- ---
1343
-
1344
- ## 9. Security & Plan Limits
1345
-
1346
- ### 9.1 Plan-Based Access
1347
-
1348
- | Plan | Slack Integration |
1349
- |------|-------------------|
1350
- | Free | ❌ Not available |
1351
- | Pro | ✅ 1 Slack workspace |
1352
- | Team | ✅ 1 Slack workspace per relay workspace |
1353
- | Enterprise | ✅ Multiple + Enterprise Grid |
1354
-
1355
- ### 9.2 Credential Security (via Nango)
1356
-
1357
- - **Encrypted storage**: Nango encrypts all tokens at rest (SOC 2 compliant)
1358
- - **Token refresh**: Automatic refresh before expiry, no manual logic needed
1359
- - **Revocation**: `nango.deleteConnection()` revokes and removes tokens
1360
- - **No local storage**: Credentials fetched on-demand, never written to disk
1361
- - **Audit logging**: Nango provides connection activity logs
1362
-
1363
- ### 9.3 Plan Limit Middleware
1364
-
1365
- ```typescript
1366
- // src/cloud/api/middleware/planLimits.ts
1367
-
1368
- export function requireSlackAccess(req, res, next) {
1369
- const plan = req.user.plan;
1370
-
1371
- if (plan === 'free') {
1372
- return res.status(403).json({
1373
- error: 'Slack integration requires Pro plan or above',
1374
- upgrade: '/pricing',
1375
- });
1376
- }
1377
-
1378
- next();
1379
- }
1380
- ```
1381
-
1382
- ---
1383
-
1384
- ## 10. Testing Strategy
1385
-
1386
- ### 10.1 Unit Tests
1387
-
1388
- ```typescript
1389
- // src/cloud/services/__tests__/slack.test.ts
1390
- describe('SlackService', () => {
1391
- it('builds correct OAuth URL', () => { ... });
1392
- it('validates tokens correctly', () => { ... });
1393
- it('handles token revocation', () => { ... });
1394
- });
1395
-
1396
- // src/daemon/__tests__/slack-bridge.test.ts
1397
- describe('SlackBridge', () => {
1398
- it('forwards relay broadcasts to Slack', () => { ... });
1399
- it('forwards Slack mentions to relay', () => { ... });
1400
- it('maps threads bidirectionally', () => { ... });
1401
- it('prevents message loops', () => { ... });
1402
- });
1403
- ```
1404
-
1405
- ### 10.2 Integration Tests
1406
-
1407
- ```typescript
1408
- describe('Slack Integration E2E', () => {
1409
- it('completes OAuth flow and stores credentials', async () => { ... });
1410
- it('daemon receives credentials via cloud sync', async () => { ... });
1411
- it('messages flow relay → slack → relay', async () => { ... });
1412
- });
1413
- ```
1414
-
1415
- ---
1416
-
1417
- ## 11. Open Questions
1418
-
1419
- ### Q1: Slack App Distribution?
1420
-
1421
- **Options:**
1422
- 1. **Single Anthropic-owned app**: Users install our app
1423
- 2. **Per-customer apps**: Customers create their own Slack apps
1424
- 3. **Both**: Managed app for cloud, bring-your-own for self-hosted
1425
-
1426
- **Recommendation:** Single managed app for cloud, instructions for self-hosted.
1427
-
1428
- ### Q2: Enterprise Grid Support?
1429
-
1430
- Enterprise Grid requires org-level OAuth and cross-workspace routing.
1431
-
1432
- **Recommendation:** Defer to v2, design schema to support it.
1433
-
1434
- ### Q3: Rate Limit Handling?
1435
-
1436
- Slack has strict rate limits (1 msg/sec/channel).
1437
-
1438
- **Recommendation:** Implement queue with backoff in SlackBridge.
1439
-
1440
- ---
1441
-
1442
- ## Summary
1443
-
1444
- This revised proposal aligns Slack integration with the **cloud-first architecture** from PR #35:
1445
-
1446
- | Component | Location | Purpose |
1447
- |-----------|----------|---------|
1448
- | OAuth & API | `src/cloud/api/integrations/slack.ts` | Cloud endpoints |
1449
- | Slack Service | `src/cloud/services/slack.ts` | Token retrieval via Nango |
1450
- | Credentials | Nango (external) | OAuth, storage, refresh |
1451
- | Database | `src/cloud/db/schema.ts` | Integration config (channels, settings) |
1452
- | Daemon Bridge | `src/daemon/slack-bridge.ts` | Message routing |
1453
- | Orchestrator | `src/daemon/orchestrator.ts` | Lifecycle management |
1454
- | Cloud Sync | `src/daemon/cloud-sync.ts` | Credential retrieval |
1455
- | Dashboard | `src/dashboard/react-components/` | Configuration UI |
1456
-
1457
- The integration uses Nango for OAuth/credentials (same pattern planned for other providers like Discord, Teams), keeping our code focused on the Slack ↔ Relay bridge logic.