@loompad/cli 0.1.0 → 1.1.2

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 (688) hide show
  1. package/ARCHITECTURE.md +96 -11
  2. package/CHANGELOG.md +1054 -0
  3. package/README.md +1473 -44
  4. package/THIRD_PARTY_NOTICES.md +275 -0
  5. package/dist/adapters/antigravity-cli.d.ts +62 -0
  6. package/dist/adapters/antigravity-cli.js +258 -0
  7. package/dist/adapters/antigravity-cli.js.map +1 -0
  8. package/dist/adapters/base.d.ts +61 -9
  9. package/dist/adapters/base.js +266 -15
  10. package/dist/adapters/base.js.map +1 -1
  11. package/dist/adapters/bridges/antigravity.d.ts +9 -12
  12. package/dist/adapters/bridges/antigravity.js +9 -12
  13. package/dist/adapters/bridges/antigravity.js.map +1 -1
  14. package/dist/adapters/echo.d.ts +1 -0
  15. package/dist/adapters/echo.js +59 -3
  16. package/dist/adapters/echo.js.map +1 -1
  17. package/dist/adapters/grok.d.ts +7 -0
  18. package/dist/adapters/grok.js +20 -10
  19. package/dist/adapters/grok.js.map +1 -1
  20. package/dist/adapters/index.d.ts +5 -1
  21. package/dist/adapters/index.js +53 -7
  22. package/dist/adapters/index.js.map +1 -1
  23. package/dist/adapters/model.d.ts +137 -0
  24. package/dist/adapters/model.js +527 -0
  25. package/dist/adapters/model.js.map +1 -0
  26. package/dist/adapters/opencode.d.ts +101 -1
  27. package/dist/adapters/opencode.js +874 -98
  28. package/dist/adapters/opencode.js.map +1 -1
  29. package/dist/cli/doctor.d.ts +32 -0
  30. package/dist/cli/doctor.js +89 -4
  31. package/dist/cli/doctor.js.map +1 -1
  32. package/dist/cli/index.js +2651 -87
  33. package/dist/cli/index.js.map +1 -1
  34. package/dist/cli/tui-model.js +3 -1
  35. package/dist/cli/tui-model.js.map +1 -1
  36. package/dist/cli/tui.js +44 -1
  37. package/dist/cli/tui.js.map +1 -1
  38. package/dist/cli/ui.d.ts +2 -0
  39. package/dist/cli/ui.js +13 -0
  40. package/dist/cli/ui.js.map +1 -1
  41. package/dist/core/ades.d.ts +32 -16
  42. package/dist/core/ades.js +57 -20
  43. package/dist/core/ades.js.map +1 -1
  44. package/dist/core/agent-state.d.ts +14 -0
  45. package/dist/core/agent-state.js +32 -0
  46. package/dist/core/agent-state.js.map +1 -0
  47. package/dist/core/approvals.d.ts +33 -0
  48. package/dist/core/approvals.js +29 -0
  49. package/dist/core/approvals.js.map +1 -0
  50. package/dist/core/backup.d.ts +19 -0
  51. package/dist/core/backup.js +86 -0
  52. package/dist/core/backup.js.map +1 -0
  53. package/dist/core/baton.d.ts +2 -2
  54. package/dist/core/baton.js.map +1 -1
  55. package/dist/core/brain-extract.d.ts +17 -0
  56. package/dist/core/brain-extract.js +28 -2
  57. package/dist/core/brain-extract.js.map +1 -1
  58. package/dist/core/brain-index.d.ts +71 -0
  59. package/dist/core/brain-index.js +204 -5
  60. package/dist/core/brain-index.js.map +1 -1
  61. package/dist/core/brain.d.ts +58 -2
  62. package/dist/core/brain.js +79 -1
  63. package/dist/core/brain.js.map +1 -1
  64. package/dist/core/chat-import.d.ts +71 -0
  65. package/dist/core/chat-import.js +419 -0
  66. package/dist/core/chat-import.js.map +1 -0
  67. package/dist/core/checkpoint.d.ts +107 -0
  68. package/dist/core/checkpoint.js +259 -0
  69. package/dist/core/checkpoint.js.map +1 -0
  70. package/dist/core/claude-cli.js +1 -1
  71. package/dist/core/claude-cli.js.map +1 -1
  72. package/dist/core/continuity/artifacts.d.ts +14 -0
  73. package/dist/core/continuity/artifacts.js +118 -0
  74. package/dist/core/continuity/artifacts.js.map +1 -0
  75. package/dist/core/continuity/capabilities.d.ts +65 -0
  76. package/dist/core/continuity/capabilities.js +129 -0
  77. package/dist/core/continuity/capabilities.js.map +1 -0
  78. package/dist/core/continuity/contracts.d.ts +352 -0
  79. package/dist/core/continuity/contracts.js +117 -0
  80. package/dist/core/continuity/contracts.js.map +1 -0
  81. package/dist/core/continuity/engine.d.ts +55 -0
  82. package/dist/core/continuity/engine.js +504 -0
  83. package/dist/core/continuity/engine.js.map +1 -0
  84. package/dist/core/continuity/store.d.ts +73 -0
  85. package/dist/core/continuity/store.js +438 -0
  86. package/dist/core/continuity/store.js.map +1 -0
  87. package/dist/core/conversations.d.ts +19 -0
  88. package/dist/core/conversations.js +87 -0
  89. package/dist/core/conversations.js.map +1 -0
  90. package/dist/core/crew-coordination.d.ts +6 -0
  91. package/dist/core/crew-coordination.js +24 -0
  92. package/dist/core/crew-coordination.js.map +1 -0
  93. package/dist/core/crew-policy.d.ts +18 -0
  94. package/dist/core/crew-policy.js +40 -0
  95. package/dist/core/crew-policy.js.map +1 -0
  96. package/dist/core/crew-protocol.d.ts +64 -0
  97. package/dist/core/crew-protocol.js +146 -0
  98. package/dist/core/crew-protocol.js.map +1 -0
  99. package/dist/core/crew-requests.d.ts +14 -0
  100. package/dist/core/crew-requests.js +22 -0
  101. package/dist/core/crew-requests.js.map +1 -0
  102. package/dist/core/crew.d.ts +395 -0
  103. package/dist/core/crew.js +1857 -0
  104. package/dist/core/crew.js.map +1 -0
  105. package/dist/core/digest.d.ts +40 -0
  106. package/dist/core/digest.js +94 -0
  107. package/dist/core/digest.js.map +1 -0
  108. package/dist/core/distill.js +3 -5
  109. package/dist/core/distill.js.map +1 -1
  110. package/dist/core/eventlog.d.ts +27 -25
  111. package/dist/core/eventlog.js +97 -150
  112. package/dist/core/eventlog.js.map +1 -1
  113. package/dist/core/events/contracts.d.ts +51 -0
  114. package/dist/core/events/contracts.js +2 -0
  115. package/dist/core/events/contracts.js.map +1 -0
  116. package/dist/core/events/jsonl-store.d.ts +23 -0
  117. package/dist/core/events/jsonl-store.js +90 -0
  118. package/dist/core/events/jsonl-store.js.map +1 -0
  119. package/dist/core/events/sqlite-store.d.ts +37 -0
  120. package/dist/core/events/sqlite-store.js +215 -0
  121. package/dist/core/events/sqlite-store.js.map +1 -0
  122. package/dist/core/forecast.d.ts +44 -0
  123. package/dist/core/forecast.js +46 -0
  124. package/dist/core/forecast.js.map +1 -0
  125. package/dist/core/free-pool.d.ts +65 -0
  126. package/dist/core/free-pool.js +171 -0
  127. package/dist/core/free-pool.js.map +1 -0
  128. package/dist/core/git.d.ts +30 -0
  129. package/dist/core/git.js +50 -2
  130. package/dist/core/git.js.map +1 -1
  131. package/dist/core/github-events.d.ts +53 -0
  132. package/dist/core/github-events.js +227 -0
  133. package/dist/core/github-events.js.map +1 -0
  134. package/dist/core/goal-lanes.d.ts +53 -0
  135. package/dist/core/goal-lanes.js +99 -0
  136. package/dist/core/goal-lanes.js.map +1 -0
  137. package/dist/core/hosted.d.ts +27 -0
  138. package/dist/core/hosted.js +50 -0
  139. package/dist/core/hosted.js.map +1 -0
  140. package/dist/core/http-server.d.ts +5 -0
  141. package/dist/core/http-server.js +8 -0
  142. package/dist/core/http-server.js.map +1 -0
  143. package/dist/core/invite-link.d.ts +31 -0
  144. package/dist/core/invite-link.js +45 -0
  145. package/dist/core/invite-link.js.map +1 -0
  146. package/dist/core/loops.d.ts +37 -0
  147. package/dist/core/loops.js +81 -0
  148. package/dist/core/loops.js.map +1 -0
  149. package/dist/core/mcp-catalog.d.ts +182 -0
  150. package/dist/core/mcp-catalog.js +431 -0
  151. package/dist/core/mcp-catalog.js.map +1 -0
  152. package/dist/core/mcp.d.ts +87 -0
  153. package/dist/core/mcp.js +181 -0
  154. package/dist/core/mcp.js.map +1 -0
  155. package/dist/core/memory.d.ts +1 -0
  156. package/dist/core/memory.js +5 -2
  157. package/dist/core/memory.js.map +1 -1
  158. package/dist/core/model-tools.d.ts +97 -0
  159. package/dist/core/model-tools.js +388 -0
  160. package/dist/core/model-tools.js.map +1 -0
  161. package/dist/core/notify.js +19 -1
  162. package/dist/core/notify.js.map +1 -1
  163. package/dist/core/orchestra.d.ts +635 -0
  164. package/dist/core/orchestra.js +2120 -0
  165. package/dist/core/orchestra.js.map +1 -0
  166. package/dist/core/permissions.d.ts +56 -0
  167. package/dist/core/permissions.js +122 -0
  168. package/dist/core/permissions.js.map +1 -0
  169. package/dist/core/preview-proxy.d.ts +70 -0
  170. package/dist/core/preview-proxy.js +563 -0
  171. package/dist/core/preview-proxy.js.map +1 -0
  172. package/dist/core/preview-shot.d.ts +46 -0
  173. package/dist/core/preview-shot.js +109 -0
  174. package/dist/core/preview-shot.js.map +1 -0
  175. package/dist/core/process.d.ts +36 -0
  176. package/dist/core/process.js +168 -0
  177. package/dist/core/process.js.map +1 -0
  178. package/dist/core/prompt-queue.d.ts +136 -0
  179. package/dist/core/prompt-queue.js +269 -0
  180. package/dist/core/prompt-queue.js.map +1 -0
  181. package/dist/core/prompts.d.ts +53 -0
  182. package/dist/core/prompts.js +127 -0
  183. package/dist/core/prompts.js.map +1 -0
  184. package/dist/core/proof.d.ts +82 -0
  185. package/dist/core/proof.js +216 -0
  186. package/dist/core/proof.js.map +1 -0
  187. package/dist/core/provider-text.d.ts +23 -0
  188. package/dist/core/provider-text.js +66 -0
  189. package/dist/core/provider-text.js.map +1 -0
  190. package/dist/core/providers.d.ts +152 -0
  191. package/dist/core/providers.js +343 -0
  192. package/dist/core/providers.js.map +1 -0
  193. package/dist/core/quota.d.ts +33 -0
  194. package/dist/core/quota.js +74 -0
  195. package/dist/core/quota.js.map +1 -0
  196. package/dist/core/recipes.d.ts +50 -0
  197. package/dist/core/recipes.js +99 -0
  198. package/dist/core/recipes.js.map +1 -0
  199. package/dist/core/registry.d.ts +54 -0
  200. package/dist/core/registry.js +62 -5
  201. package/dist/core/registry.js.map +1 -1
  202. package/dist/core/relay-client.d.ts +67 -0
  203. package/dist/core/relay-client.js +184 -0
  204. package/dist/core/relay-client.js.map +1 -0
  205. package/dist/core/relay-protocol.d.ts +140 -0
  206. package/dist/core/relay-protocol.js +189 -0
  207. package/dist/core/relay-protocol.js.map +1 -0
  208. package/dist/core/replay.d.ts +18 -0
  209. package/dist/core/replay.js +191 -0
  210. package/dist/core/replay.js.map +1 -0
  211. package/dist/core/review-triage.d.ts +41 -0
  212. package/dist/core/review-triage.js +124 -0
  213. package/dist/core/review-triage.js.map +1 -0
  214. package/dist/core/router.js +2 -2
  215. package/dist/core/router.js.map +1 -1
  216. package/dist/core/routes.d.ts +39 -3
  217. package/dist/core/routes.js +137 -4
  218. package/dist/core/routes.js.map +1 -1
  219. package/dist/core/run-command.d.ts +14 -0
  220. package/dist/core/run-command.js +45 -0
  221. package/dist/core/run-command.js.map +1 -0
  222. package/dist/core/runner-setup.d.ts +36 -0
  223. package/dist/core/runner-setup.js +114 -0
  224. package/dist/core/runner-setup.js.map +1 -0
  225. package/dist/core/search.js +0 -0
  226. package/dist/core/search.js.map +1 -1
  227. package/dist/core/semantic.d.ts +92 -0
  228. package/dist/core/semantic.js +227 -0
  229. package/dist/core/semantic.js.map +1 -0
  230. package/dist/core/servers.d.ts +115 -0
  231. package/dist/core/servers.js +305 -0
  232. package/dist/core/servers.js.map +1 -0
  233. package/dist/core/setup.js +64 -6
  234. package/dist/core/setup.js.map +1 -1
  235. package/dist/core/shield.d.ts +17 -0
  236. package/dist/core/shield.js +81 -0
  237. package/dist/core/shield.js.map +1 -0
  238. package/dist/core/shift.d.ts +69 -0
  239. package/dist/core/shift.js +120 -0
  240. package/dist/core/shift.js.map +1 -0
  241. package/dist/core/skill-install.d.ts +125 -0
  242. package/dist/core/skill-install.js +328 -0
  243. package/dist/core/skill-install.js.map +1 -0
  244. package/dist/core/skills.d.ts +116 -0
  245. package/dist/core/skills.js +223 -0
  246. package/dist/core/skills.js.map +1 -0
  247. package/dist/core/step-conditions.d.ts +65 -0
  248. package/dist/core/step-conditions.js +102 -0
  249. package/dist/core/step-conditions.js.map +1 -0
  250. package/dist/core/subagents.d.ts +45 -0
  251. package/dist/core/subagents.js +90 -0
  252. package/dist/core/subagents.js.map +1 -0
  253. package/dist/core/team-canon.d.ts +42 -0
  254. package/dist/core/team-canon.js +121 -0
  255. package/dist/core/team-canon.js.map +1 -0
  256. package/dist/core/team-crypto.d.ts +97 -0
  257. package/dist/core/team-crypto.js +175 -0
  258. package/dist/core/team-crypto.js.map +1 -0
  259. package/dist/core/team-hub.d.ts +514 -0
  260. package/dist/core/team-hub.js +964 -0
  261. package/dist/core/team-hub.js.map +1 -0
  262. package/dist/core/team-landing.d.ts +239 -0
  263. package/dist/core/team-landing.js +504 -0
  264. package/dist/core/team-landing.js.map +1 -0
  265. package/dist/core/team-leases.d.ts +44 -0
  266. package/dist/core/team-leases.js +145 -0
  267. package/dist/core/team-leases.js.map +1 -0
  268. package/dist/core/team-memory.d.ts +42 -0
  269. package/dist/core/team-memory.js +84 -0
  270. package/dist/core/team-memory.js.map +1 -0
  271. package/dist/core/team-policy.d.ts +85 -0
  272. package/dist/core/team-policy.js +191 -0
  273. package/dist/core/team-policy.js.map +1 -0
  274. package/dist/core/thread-md.d.ts +6 -0
  275. package/dist/core/thread-md.js +42 -0
  276. package/dist/core/thread-md.js.map +1 -0
  277. package/dist/core/turn-stats.d.ts +61 -0
  278. package/dist/core/turn-stats.js +124 -0
  279. package/dist/core/turn-stats.js.map +1 -0
  280. package/dist/core/updater.d.ts +73 -0
  281. package/dist/core/updater.js +153 -0
  282. package/dist/core/updater.js.map +1 -0
  283. package/dist/core/usage.d.ts +75 -0
  284. package/dist/core/usage.js +163 -0
  285. package/dist/core/usage.js.map +1 -0
  286. package/dist/core/windows-jobs.d.ts +15 -0
  287. package/dist/core/windows-jobs.js +170 -0
  288. package/dist/core/windows-jobs.js.map +1 -0
  289. package/dist/core/worktree-merge.d.ts +57 -0
  290. package/dist/core/worktree-merge.js +123 -0
  291. package/dist/core/worktree-merge.js.map +1 -0
  292. package/dist/core/worktree.d.ts +5 -0
  293. package/dist/core/worktree.js +45 -5
  294. package/dist/core/worktree.js.map +1 -1
  295. package/dist/core/worktrees.d.ts +6 -0
  296. package/dist/core/worktrees.js +21 -0
  297. package/dist/core/worktrees.js.map +1 -0
  298. package/dist/daemon/app-page.d.ts +0 -17
  299. package/dist/daemon/app-page.js +10 -6346
  300. package/dist/daemon/app-page.js.map +1 -1
  301. package/dist/daemon/auth.d.ts +24 -3
  302. package/dist/daemon/auth.js +66 -7
  303. package/dist/daemon/auth.js.map +1 -1
  304. package/dist/daemon/board.d.ts +7 -0
  305. package/dist/daemon/board.js +12 -1
  306. package/dist/daemon/board.js.map +1 -1
  307. package/dist/daemon/brand-icons.d.ts +6 -1
  308. package/dist/daemon/brand-icons.js +11 -3
  309. package/dist/daemon/brand-icons.js.map +1 -1
  310. package/dist/daemon/client.d.ts +665 -1
  311. package/dist/daemon/client.js +381 -3
  312. package/dist/daemon/client.js.map +1 -1
  313. package/dist/daemon/delivery.d.ts +34 -0
  314. package/dist/daemon/delivery.js +63 -0
  315. package/dist/daemon/delivery.js.map +1 -0
  316. package/dist/daemon/deploys.d.ts +62 -0
  317. package/dist/daemon/deploys.js +159 -0
  318. package/dist/daemon/deploys.js.map +1 -0
  319. package/dist/daemon/landing.d.ts +192 -0
  320. package/dist/daemon/landing.js +992 -0
  321. package/dist/daemon/landing.js.map +1 -0
  322. package/dist/daemon/onboard.d.ts +150 -0
  323. package/dist/daemon/onboard.js +390 -0
  324. package/dist/daemon/onboard.js.map +1 -0
  325. package/dist/daemon/proof.d.ts +70 -0
  326. package/dist/daemon/proof.js +172 -0
  327. package/dist/daemon/proof.js.map +1 -0
  328. package/dist/daemon/push.d.ts +19 -1
  329. package/dist/daemon/push.js +90 -3
  330. package/dist/daemon/push.js.map +1 -1
  331. package/dist/daemon/relay.d.ts +82 -0
  332. package/dist/daemon/relay.js +351 -0
  333. package/dist/daemon/relay.js.map +1 -0
  334. package/dist/daemon/routes/agent-discovery.d.ts +4 -0
  335. package/dist/daemon/routes/agent-discovery.js +29 -0
  336. package/dist/daemon/routes/agent-discovery.js.map +1 -0
  337. package/dist/daemon/routes/agent-settings.d.ts +4 -0
  338. package/dist/daemon/routes/agent-settings.js +94 -0
  339. package/dist/daemon/routes/agent-settings.js.map +1 -0
  340. package/dist/daemon/routes/agents.d.ts +4 -0
  341. package/dist/daemon/routes/agents.js +105 -0
  342. package/dist/daemon/routes/agents.js.map +1 -0
  343. package/dist/daemon/routes/alerts.d.ts +4 -0
  344. package/dist/daemon/routes/alerts.js +137 -0
  345. package/dist/daemon/routes/alerts.js.map +1 -0
  346. package/dist/daemon/routes/approvals.d.ts +4 -0
  347. package/dist/daemon/routes/approvals.js +40 -0
  348. package/dist/daemon/routes/approvals.js.map +1 -0
  349. package/dist/daemon/routes/assets.d.ts +3 -0
  350. package/dist/daemon/routes/assets.js +64 -0
  351. package/dist/daemon/routes/assets.js.map +1 -0
  352. package/dist/daemon/routes/board.d.ts +4 -0
  353. package/dist/daemon/routes/board.js +147 -0
  354. package/dist/daemon/routes/board.js.map +1 -0
  355. package/dist/daemon/routes/brain.d.ts +4 -0
  356. package/dist/daemon/routes/brain.js +270 -0
  357. package/dist/daemon/routes/brain.js.map +1 -0
  358. package/dist/daemon/routes/chats.d.ts +4 -0
  359. package/dist/daemon/routes/chats.js +249 -0
  360. package/dist/daemon/routes/chats.js.map +1 -0
  361. package/dist/daemon/routes/clients.d.ts +4 -0
  362. package/dist/daemon/routes/clients.js +49 -0
  363. package/dist/daemon/routes/clients.js.map +1 -0
  364. package/dist/daemon/routes/cloud.d.ts +4 -0
  365. package/dist/daemon/routes/cloud.js +46 -0
  366. package/dist/daemon/routes/cloud.js.map +1 -0
  367. package/dist/daemon/routes/config.d.ts +4 -0
  368. package/dist/daemon/routes/config.js +35 -0
  369. package/dist/daemon/routes/config.js.map +1 -0
  370. package/dist/daemon/routes/context.d.ts +51 -0
  371. package/dist/daemon/routes/context.js +2 -0
  372. package/dist/daemon/routes/context.js.map +1 -0
  373. package/dist/daemon/routes/crews.d.ts +4 -0
  374. package/dist/daemon/routes/crews.js +149 -0
  375. package/dist/daemon/routes/crews.js.map +1 -0
  376. package/dist/daemon/routes/files.d.ts +3 -0
  377. package/dist/daemon/routes/files.js +253 -0
  378. package/dist/daemon/routes/files.js.map +1 -0
  379. package/dist/daemon/routes/git.d.ts +4 -0
  380. package/dist/daemon/routes/git.js +96 -0
  381. package/dist/daemon/routes/git.js.map +1 -0
  382. package/dist/daemon/routes/history.d.ts +4 -0
  383. package/dist/daemon/routes/history.js +101 -0
  384. package/dist/daemon/routes/history.js.map +1 -0
  385. package/dist/daemon/routes/integrations-project.d.ts +3 -0
  386. package/dist/daemon/routes/integrations-project.js +103 -0
  387. package/dist/daemon/routes/integrations-project.js.map +1 -0
  388. package/dist/daemon/routes/integrations.d.ts +3 -0
  389. package/dist/daemon/routes/integrations.js +111 -0
  390. package/dist/daemon/routes/integrations.js.map +1 -0
  391. package/dist/daemon/routes/logs.d.ts +3 -0
  392. package/dist/daemon/routes/logs.js +40 -0
  393. package/dist/daemon/routes/logs.js.map +1 -0
  394. package/dist/daemon/routes/messages.d.ts +4 -0
  395. package/dist/daemon/routes/messages.js +60 -0
  396. package/dist/daemon/routes/messages.js.map +1 -0
  397. package/dist/daemon/routes/observability.d.ts +4 -0
  398. package/dist/daemon/routes/observability.js +247 -0
  399. package/dist/daemon/routes/observability.js.map +1 -0
  400. package/dist/daemon/routes/onboard.d.ts +12 -0
  401. package/dist/daemon/routes/onboard.js +67 -0
  402. package/dist/daemon/routes/onboard.js.map +1 -0
  403. package/dist/daemon/routes/orchestra.d.ts +4 -0
  404. package/dist/daemon/routes/orchestra.js +110 -0
  405. package/dist/daemon/routes/orchestra.js.map +1 -0
  406. package/dist/daemon/routes/pairing.d.ts +4 -0
  407. package/dist/daemon/routes/pairing.js +155 -0
  408. package/dist/daemon/routes/pairing.js.map +1 -0
  409. package/dist/daemon/routes/preview.d.ts +24 -0
  410. package/dist/daemon/routes/preview.js +122 -0
  411. package/dist/daemon/routes/preview.js.map +1 -0
  412. package/dist/daemon/routes/project-actions.d.ts +4 -0
  413. package/dist/daemon/routes/project-actions.js +53 -0
  414. package/dist/daemon/routes/project-actions.js.map +1 -0
  415. package/dist/daemon/routes/project-team.d.ts +4 -0
  416. package/dist/daemon/routes/project-team.js +201 -0
  417. package/dist/daemon/routes/project-team.js.map +1 -0
  418. package/dist/daemon/routes/projects.d.ts +14 -0
  419. package/dist/daemon/routes/projects.js +138 -0
  420. package/dist/daemon/routes/projects.js.map +1 -0
  421. package/dist/daemon/routes/prompts.d.ts +4 -0
  422. package/dist/daemon/routes/prompts.js +44 -0
  423. package/dist/daemon/routes/prompts.js.map +1 -0
  424. package/dist/daemon/routes/proof.d.ts +4 -0
  425. package/dist/daemon/routes/proof.js +22 -0
  426. package/dist/daemon/routes/proof.js.map +1 -0
  427. package/dist/daemon/routes/providers.d.ts +3 -0
  428. package/dist/daemon/routes/providers.js +60 -0
  429. package/dist/daemon/routes/providers.js.map +1 -0
  430. package/dist/daemon/routes/public.d.ts +4 -0
  431. package/dist/daemon/routes/public.js +64 -0
  432. package/dist/daemon/routes/public.js.map +1 -0
  433. package/dist/daemon/routes/queue.d.ts +4 -0
  434. package/dist/daemon/routes/queue.js +140 -0
  435. package/dist/daemon/routes/queue.js.map +1 -0
  436. package/dist/daemon/routes/routing.d.ts +4 -0
  437. package/dist/daemon/routes/routing.js +61 -0
  438. package/dist/daemon/routes/routing.js.map +1 -0
  439. package/dist/daemon/routes/runtime-handler.d.ts +2 -0
  440. package/dist/daemon/routes/runtime-handler.js +63 -0
  441. package/dist/daemon/routes/runtime-handler.js.map +1 -0
  442. package/dist/daemon/routes/search.d.ts +4 -0
  443. package/dist/daemon/routes/search.js +17 -0
  444. package/dist/daemon/routes/search.js.map +1 -0
  445. package/dist/daemon/routes/servers.d.ts +4 -0
  446. package/dist/daemon/routes/servers.js +133 -0
  447. package/dist/daemon/routes/servers.js.map +1 -0
  448. package/dist/daemon/routes/setup.d.ts +3 -0
  449. package/dist/daemon/routes/setup.js +50 -0
  450. package/dist/daemon/routes/setup.js.map +1 -0
  451. package/dist/daemon/routes/shift.d.ts +4 -0
  452. package/dist/daemon/routes/shift.js +30 -0
  453. package/dist/daemon/routes/shift.js.map +1 -0
  454. package/dist/daemon/routes/specs.d.ts +4 -0
  455. package/dist/daemon/routes/specs.js +57 -0
  456. package/dist/daemon/routes/specs.js.map +1 -0
  457. package/dist/daemon/routes/task-delivery.d.ts +4 -0
  458. package/dist/daemon/routes/task-delivery.js +38 -0
  459. package/dist/daemon/routes/task-delivery.js.map +1 -0
  460. package/dist/daemon/routes/team.d.ts +4 -0
  461. package/dist/daemon/routes/team.js +193 -0
  462. package/dist/daemon/routes/team.js.map +1 -0
  463. package/dist/daemon/routes/terminals.d.ts +4 -0
  464. package/dist/daemon/routes/terminals.js +87 -0
  465. package/dist/daemon/routes/terminals.js.map +1 -0
  466. package/dist/daemon/routes/tools.d.ts +4 -0
  467. package/dist/daemon/routes/tools.js +180 -0
  468. package/dist/daemon/routes/tools.js.map +1 -0
  469. package/dist/daemon/routes/updates.d.ts +4 -0
  470. package/dist/daemon/routes/updates.js +89 -0
  471. package/dist/daemon/routes/updates.js.map +1 -0
  472. package/dist/daemon/routes/voice.d.ts +4 -0
  473. package/dist/daemon/routes/voice.js +53 -0
  474. package/dist/daemon/routes/voice.js.map +1 -0
  475. package/dist/daemon/runner.d.ts +186 -0
  476. package/dist/daemon/runner.js +612 -0
  477. package/dist/daemon/runner.js.map +1 -0
  478. package/dist/daemon/runtime/accounting.d.ts +130 -0
  479. package/dist/daemon/runtime/accounting.js +303 -0
  480. package/dist/daemon/runtime/accounting.js.map +1 -0
  481. package/dist/daemon/runtime/agents.d.ts +16 -0
  482. package/dist/daemon/runtime/agents.js +71 -0
  483. package/dist/daemon/runtime/agents.js.map +1 -0
  484. package/dist/daemon/runtime/briefings.d.ts +134 -0
  485. package/dist/daemon/runtime/briefings.js +329 -0
  486. package/dist/daemon/runtime/briefings.js.map +1 -0
  487. package/dist/daemon/runtime/native-usage.d.ts +21 -0
  488. package/dist/daemon/runtime/native-usage.js +71 -0
  489. package/dist/daemon/runtime/native-usage.js.map +1 -0
  490. package/dist/daemon/runtime/queue.d.ts +94 -0
  491. package/dist/daemon/runtime/queue.js +222 -0
  492. package/dist/daemon/runtime/queue.js.map +1 -0
  493. package/dist/daemon/runtime/relay.d.ts +93 -0
  494. package/dist/daemon/runtime/relay.js +240 -0
  495. package/dist/daemon/runtime/relay.js.map +1 -0
  496. package/dist/daemon/runtime/turns.d.ts +202 -0
  497. package/dist/daemon/runtime/turns.js +663 -0
  498. package/dist/daemon/runtime/turns.js.map +1 -0
  499. package/dist/daemon/runtime-support.d.ts +76 -0
  500. package/dist/daemon/runtime-support.js +134 -0
  501. package/dist/daemon/runtime-support.js.map +1 -0
  502. package/dist/daemon/runtime.d.ts +739 -70
  503. package/dist/daemon/runtime.js +2751 -470
  504. package/dist/daemon/runtime.js.map +1 -1
  505. package/dist/daemon/server.d.ts +82 -55
  506. package/dist/daemon/server.js +674 -1435
  507. package/dist/daemon/server.js.map +1 -1
  508. package/dist/daemon/shift.d.ts +70 -0
  509. package/dist/daemon/shift.js +170 -0
  510. package/dist/daemon/shift.js.map +1 -0
  511. package/dist/daemon/specs.d.ts +92 -0
  512. package/dist/daemon/specs.js +285 -0
  513. package/dist/daemon/specs.js.map +1 -0
  514. package/dist/daemon/system.d.ts +208 -0
  515. package/dist/daemon/system.js +557 -0
  516. package/dist/daemon/system.js.map +1 -0
  517. package/dist/daemon/tasks.js +1 -1
  518. package/dist/daemon/team-brain.d.ts +136 -0
  519. package/dist/daemon/team-brain.js +659 -0
  520. package/dist/daemon/team-brain.js.map +1 -0
  521. package/dist/daemon/team-coordinator.d.ts +78 -0
  522. package/dist/daemon/team-coordinator.js +408 -0
  523. package/dist/daemon/team-coordinator.js.map +1 -0
  524. package/dist/daemon/team.d.ts +339 -0
  525. package/dist/daemon/team.js +1433 -0
  526. package/dist/daemon/team.js.map +1 -0
  527. package/dist/daemon/terminals.d.ts +22 -3
  528. package/dist/daemon/terminals.js +236 -16
  529. package/dist/daemon/terminals.js.map +1 -1
  530. package/dist/hub/client.d.ts +120 -0
  531. package/dist/hub/client.js +228 -0
  532. package/dist/hub/client.js.map +1 -0
  533. package/dist/hub/server.d.ts +32 -0
  534. package/dist/hub/server.js +180 -0
  535. package/dist/hub/server.js.map +1 -0
  536. package/dist/hub/supabase-client.d.ts +220 -0
  537. package/dist/hub/supabase-client.js +843 -0
  538. package/dist/hub/supabase-client.js.map +1 -0
  539. package/dist/observability/ask.d.ts +91 -0
  540. package/dist/observability/ask.js +179 -0
  541. package/dist/observability/ask.js.map +1 -0
  542. package/dist/observability/clickhouse.d.ts +33 -0
  543. package/dist/observability/clickhouse.js +48 -0
  544. package/dist/observability/clickhouse.js.map +1 -0
  545. package/dist/observability/decisions.d.ts +176 -0
  546. package/dist/observability/decisions.js +377 -0
  547. package/dist/observability/decisions.js.map +1 -0
  548. package/dist/observability/index.d.ts +89 -0
  549. package/dist/observability/index.js +348 -0
  550. package/dist/observability/index.js.map +1 -0
  551. package/dist/observability/insights.d.ts +151 -0
  552. package/dist/observability/insights.js +319 -0
  553. package/dist/observability/insights.js.map +1 -0
  554. package/dist/observability/logs-query.d.ts +82 -0
  555. package/dist/observability/logs-query.js +102 -0
  556. package/dist/observability/logs-query.js.map +1 -0
  557. package/dist/observability/logs.d.ts +72 -0
  558. package/dist/observability/logs.js +295 -0
  559. package/dist/observability/logs.js.map +1 -0
  560. package/dist/observability/metrics.d.ts +86 -0
  561. package/dist/observability/metrics.js +303 -0
  562. package/dist/observability/metrics.js.map +1 -0
  563. package/dist/observability/otlp.d.ts +127 -0
  564. package/dist/observability/otlp.js +242 -0
  565. package/dist/observability/otlp.js.map +1 -0
  566. package/dist/observability/snapshots.d.ts +43 -0
  567. package/dist/observability/snapshots.js +129 -0
  568. package/dist/observability/snapshots.js.map +1 -0
  569. package/dist/observability/triage.d.ts +49 -0
  570. package/dist/observability/triage.js +284 -0
  571. package/dist/observability/triage.js.map +1 -0
  572. package/dist/providers/adapter.d.ts +50 -0
  573. package/dist/providers/adapter.js +38 -0
  574. package/dist/providers/adapter.js.map +1 -0
  575. package/dist/providers/agent.d.ts +110 -0
  576. package/dist/providers/agent.js +487 -0
  577. package/dist/providers/agent.js.map +1 -0
  578. package/dist/providers/approvals.d.ts +24 -0
  579. package/dist/providers/approvals.js +78 -0
  580. package/dist/providers/approvals.js.map +1 -0
  581. package/dist/providers/attachments.d.ts +20 -0
  582. package/dist/providers/attachments.js +51 -0
  583. package/dist/providers/attachments.js.map +1 -0
  584. package/dist/providers/claude/adapter.d.ts +99 -0
  585. package/dist/providers/claude/adapter.js +790 -0
  586. package/dist/providers/claude/adapter.js.map +1 -0
  587. package/dist/providers/codex/adapter.d.ts +113 -0
  588. package/dist/providers/codex/adapter.js +881 -0
  589. package/dist/providers/codex/adapter.js.map +1 -0
  590. package/dist/providers/codex/instructions.d.ts +11 -0
  591. package/dist/providers/codex/instructions.js +154 -0
  592. package/dist/providers/codex/instructions.js.map +1 -0
  593. package/dist/providers/codex/rpc.d.ts +36 -0
  594. package/dist/providers/codex/rpc.js +104 -0
  595. package/dist/providers/codex/rpc.js.map +1 -0
  596. package/dist/providers/contracts.d.ts +339 -0
  597. package/dist/providers/contracts.js +19 -0
  598. package/dist/providers/contracts.js.map +1 -0
  599. package/dist/providers/directory.d.ts +70 -0
  600. package/dist/providers/directory.js +95 -0
  601. package/dist/providers/directory.js.map +1 -0
  602. package/dist/providers/errors.d.ts +47 -0
  603. package/dist/providers/errors.js +24 -0
  604. package/dist/providers/errors.js.map +1 -0
  605. package/dist/providers/index.d.ts +18 -0
  606. package/dist/providers/index.js +19 -0
  607. package/dist/providers/index.js.map +1 -0
  608. package/dist/providers/ingestion.d.ts +80 -0
  609. package/dist/providers/ingestion.js +330 -0
  610. package/dist/providers/ingestion.js.map +1 -0
  611. package/dist/providers/live.d.ts +22 -0
  612. package/dist/providers/live.js +56 -0
  613. package/dist/providers/live.js.map +1 -0
  614. package/dist/providers/process.d.ts +29 -0
  615. package/dist/providers/process.js +71 -0
  616. package/dist/providers/process.js.map +1 -0
  617. package/dist/providers/reaper.d.ts +33 -0
  618. package/dist/providers/reaper.js +65 -0
  619. package/dist/providers/reaper.js.map +1 -0
  620. package/dist/providers/service.d.ts +87 -0
  621. package/dist/providers/service.js +256 -0
  622. package/dist/providers/service.js.map +1 -0
  623. package/dist/sdk.d.ts +1 -1
  624. package/dist/sdk.js +1 -1
  625. package/dist/sdk.js.map +1 -1
  626. package/dist/types.d.ts +385 -6
  627. package/dist/types.js +1 -0
  628. package/dist/types.js.map +1 -1
  629. package/dist/version.d.ts +7 -0
  630. package/dist/version.js +16 -0
  631. package/dist/version.js.map +1 -0
  632. package/dist/web/app.css +4646 -0
  633. package/dist/web/app.js +22640 -0
  634. package/dist/web/app.js.map +7 -0
  635. package/dist/web/shell.html +39 -0
  636. package/docs/README.md +21 -0
  637. package/docs/SETUP.md +10 -0
  638. package/docs/adapters.md +21 -8
  639. package/docs/brain-continuity.md +186 -0
  640. package/docs/cloud.md +108 -0
  641. package/docs/crews.md +62 -0
  642. package/docs/design-system.md +31 -2
  643. package/docs/integration-notes.md +128 -20
  644. package/docs/loompad.md +105 -0
  645. package/docs/observability.md +220 -0
  646. package/docs/play-store.md +123 -0
  647. package/docs/proposals/agent-teams.md +249 -0
  648. package/docs/proposals/brain-continuity-implementation.md +468 -0
  649. package/docs/proposals/brain-session-continuity.md +623 -0
  650. package/docs/proposals/t3code-port.md +81 -0
  651. package/docs/proposals/t3code-reference.md +95 -0
  652. package/docs/refactoring/AGENT-TEAMS-PROGRESS.md +88 -0
  653. package/docs/refactoring/BOUNDARIES.md +120 -0
  654. package/docs/refactoring/BRAIN-AUDIT.md +176 -0
  655. package/docs/refactoring/BRAIN-NOTES.md +390 -0
  656. package/docs/refactoring/BRAIN-TODO.md +214 -0
  657. package/docs/refactoring/NOTES.md +129 -0
  658. package/docs/refactoring/T3-PORT-NOTES.md +165 -0
  659. package/docs/refactoring/T3-PORT-TODO.md +123 -0
  660. package/docs/refactoring/TODO.md +46 -0
  661. package/docs/refactoring/brain-benchmark-2026-09-28.json +48 -0
  662. package/docs/refactoring/brain-benchmark-audit-2026-09-28.json +48 -0
  663. package/docs/release-notes/v0.2.0.md +35 -0
  664. package/docs/release-notes/v0.2.1.md +59 -0
  665. package/docs/release-notes/v0.2.3.md +77 -0
  666. package/docs/release-notes/v0.2.4.md +128 -0
  667. package/docs/release-notes/v0.2.5.md +153 -0
  668. package/docs/release-notes/v0.2.6.md +62 -0
  669. package/docs/release-notes/v1.0.0.md +120 -0
  670. package/docs/release-notes/v1.0.1.md +77 -0
  671. package/docs/release-notes/v1.1.0.md +71 -0
  672. package/docs/release-notes/v1.1.1.md +38 -0
  673. package/docs/release-notes/v1.1.2.md +21 -0
  674. package/docs/teams-architecture.md +697 -0
  675. package/docs/teams.md +171 -0
  676. package/docs/windows.md +85 -0
  677. package/package.json +22 -9
  678. package/dist/adapters/claude-code.d.ts +0 -26
  679. package/dist/adapters/claude-code.js +0 -209
  680. package/dist/adapters/claude-code.js.map +0 -1
  681. package/dist/adapters/codex.d.ts +0 -51
  682. package/dist/adapters/codex.js +0 -291
  683. package/dist/adapters/codex.js.map +0 -1
  684. package/docs/img/board-linear.png +0 -0
  685. package/docs/img/board-projects.png +0 -0
  686. package/docs/img/brain.png +0 -0
  687. package/docs/img/settings.png +0 -0
  688. package/docs/img/workspace.png +0 -0
@@ -0,0 +1,964 @@
1
+ /**
2
+ * Loom Teams — the hub protocol, and an in-memory hub that is its reference.
3
+ *
4
+ * The Team Hub is the thin coordination plane between members' daemons
5
+ * (docs/teams-architecture.md). Three implementations speak `HubClient`:
6
+ *
7
+ * - MemoryHub (here): in-process, authoritative for the rules. Tests and the
8
+ * local hub server both run it, so the rules are written once.
9
+ * - HttpHubClient (src/hub/client.ts): talks to `loom hub` — a self-hosted
10
+ * hub with no Supabase (D1), and the way two daemons meet in tests.
11
+ * - Supabase (supabase/migrations/0002_teams.sql): the hosted hub, the same
12
+ * rules as row-level security and SQL functions.
13
+ *
14
+ * Phase 1 ("see each other"): teams, members, devices, invites, key envelopes,
15
+ * shared repos, presence heartbeats and a durable feed. What the hub stores is
16
+ * plain metadata plus sealed content it cannot read (D2).
17
+ */
18
+ import crypto from "node:crypto";
19
+ import { githubWebhookFeed } from "./github-events.js";
20
+ import { overlap, zoneOf } from "./team-leases.js";
21
+ /** A claimed job whose runner went quiet this long is claimable again (D12, D71). */
22
+ export const JOB_TTL_MS = 10 * 60_000;
23
+ export class HubError extends Error {
24
+ status;
25
+ constructor(message, status = 400) {
26
+ super(message);
27
+ this.status = status;
28
+ this.name = "HubError";
29
+ }
30
+ }
31
+ // ---------------------------------------------------------------------------
32
+ // Rules shared by every hub
33
+ // ---------------------------------------------------------------------------
34
+ /** A heartbeat older than this is a session that stopped (laptop closed, crash). */
35
+ export const PRESENCE_TTL_MS = 45_000;
36
+ export const INVITE_TTL_MS = 24 * 60 * 60_000;
37
+ /** A short, one-way tag for an invite token: names it in the feed without revealing it. */
38
+ export function inviteTag(invite) {
39
+ return crypto.createHash("sha256").update(invite).digest("hex").slice(0, 16);
40
+ }
41
+ export const FREE_SEATS = 3; // D21 — enforced by billing, surfaced by the hub
42
+ /** A lease not renewed for this long is stale: its owner's machine is asleep or gone (D12). */
43
+ export const LEASE_TTL_MS = 10 * 60_000;
44
+ const REPO_RE = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/;
45
+ export function normalizeRepo(repo) {
46
+ const r = repo
47
+ .trim()
48
+ .replace(/^(git@github\.com:|https?:\/\/github\.com\/)/, "")
49
+ .replace(/\.git$/, "")
50
+ .replace(/\/$/, "");
51
+ if (!REPO_RE.test(r))
52
+ throw new HubError(`"${repo}" is not a GitHub repo (owner/name)`);
53
+ return r.toLowerCase();
54
+ }
55
+ export class MemoryHub {
56
+ now;
57
+ users = new Map();
58
+ tokens = new Map(); // session token → user id
59
+ teamsById = new Map();
60
+ memberships = new Map(); // team → user → membership
61
+ devices = new Map();
62
+ invites = new Map();
63
+ envelopes = new Map(); // `${team}/${device}`
64
+ reposByTeam = new Map();
65
+ presenceByTeam = new Map();
66
+ feedByTeam = new Map();
67
+ dedupe = new Set();
68
+ feedSeq = 0;
69
+ leasesByTeam = new Map();
70
+ memoriesByTeam = new Map();
71
+ runnersByDevice = new Map();
72
+ jobsByTeam = new Map();
73
+ webhookSecrets = new Map(); // team → GitHub webhook secret (D83)
74
+ listeners = new Map();
75
+ constructor(now = Date.now) {
76
+ this.now = now;
77
+ }
78
+ /**
79
+ * Dev/self-host sign-in: a claimed GitHub login gets a session token.
80
+ * The hosted hub (Supabase) replaces this with real GitHub OAuth; the local
81
+ * hub server gates it behind its own admin secret.
82
+ */
83
+ signIn(github, name) {
84
+ const login = github.trim().toLowerCase();
85
+ if (!/^[a-z0-9](?:[a-z0-9-]{0,38})$/.test(login))
86
+ throw new HubError(`"${github}" is not a GitHub login`);
87
+ let user = [...this.users.values()].find((u) => u.github === login);
88
+ if (!user) {
89
+ user = { id: id("u"), github: login, name: name?.trim() || login };
90
+ this.users.set(user.id, user);
91
+ }
92
+ const token = crypto.randomBytes(24).toString("hex");
93
+ this.tokens.set(token, user.id);
94
+ return { token, user };
95
+ }
96
+ /** A client bound to one session — the only way to act on the hub. */
97
+ client(token) {
98
+ const userId = this.tokens.get(token);
99
+ if (!userId)
100
+ throw new HubError("not signed in to this hub", 401);
101
+ return new MemoryHubClient(this, userId);
102
+ }
103
+ // ── internals used by MemoryHubClient ──
104
+ user(id) {
105
+ const u = this.users.get(id);
106
+ if (!u)
107
+ throw new HubError("no such user", 404);
108
+ return u;
109
+ }
110
+ role(teamId, userId) {
111
+ return this.memberships.get(teamId)?.get(userId)?.role ?? null;
112
+ }
113
+ requireRole(teamId, userId, atLeast) {
114
+ const r = this.role(teamId, userId);
115
+ const rank = { viewer: 0, member: 1, owner: 2 };
116
+ if (!r)
117
+ throw new HubError("not a member of this team", 403);
118
+ if (rank[r] < rank[atLeast])
119
+ throw new HubError(`needs ${atLeast} rights`, 403);
120
+ return r;
121
+ }
122
+ team(teamId) {
123
+ const t = this.teamsById.get(teamId);
124
+ if (!t)
125
+ throw new HubError("no such team", 404);
126
+ return t;
127
+ }
128
+ createTeam(userId, name) {
129
+ const clean = name.trim().slice(0, 80);
130
+ if (!clean)
131
+ throw new HubError("a team needs a name");
132
+ const team = { id: id("t"), name: clean, createdAt: this.now(), keyVersion: 1 };
133
+ this.teamsById.set(team.id, team);
134
+ this.memberships.set(team.id, new Map([[userId, { role: "owner", joinedAt: this.now() }]]));
135
+ return team;
136
+ }
137
+ teamsOf(userId) {
138
+ const out = [];
139
+ for (const [teamId, m] of this.memberships) {
140
+ const mine = m.get(userId);
141
+ if (mine)
142
+ out.push({ ...this.team(teamId), role: mine.role });
143
+ }
144
+ return out;
145
+ }
146
+ registerDevice(userId, input) {
147
+ if (!input.sealPub || !input.signPub)
148
+ throw new HubError("a device needs both public keys");
149
+ const existing = [...this.devices.values()].find((d) => d.userId === userId && d.signPub === input.signPub);
150
+ if (existing)
151
+ return existing;
152
+ const d = {
153
+ id: id("d"),
154
+ userId,
155
+ label: input.label.slice(0, 60) || "device",
156
+ sealPub: input.sealPub,
157
+ signPub: input.signPub,
158
+ createdAt: this.now(),
159
+ };
160
+ this.devices.set(d.id, d);
161
+ return d;
162
+ }
163
+ device(deviceId, userId) {
164
+ const d = this.devices.get(deviceId);
165
+ if (!d || d.userId !== userId)
166
+ throw new HubError("that device isn't yours", 403);
167
+ return d;
168
+ }
169
+ createInvite(userId, teamId, ttlMs) {
170
+ this.requireRole(teamId, userId, "member");
171
+ // null / NaN arrive from JSON callers; either means "the default", never the floor.
172
+ if (typeof ttlMs !== "number" || !Number.isFinite(ttlMs))
173
+ ttlMs = INVITE_TTL_MS;
174
+ const invite = crypto.randomBytes(18).toString("base64url");
175
+ const expiresAt = this.now() + Math.min(Math.max(ttlMs, 60_000), 7 * INVITE_TTL_MS);
176
+ this.invites.set(invite, { teamId, createdBy: userId, expiresAt });
177
+ return { invite, expiresAt };
178
+ }
179
+ redeemInvite(userId, invite) {
180
+ const inv = this.invites.get(invite);
181
+ if (!inv || inv.usedBy || inv.expiresAt < this.now())
182
+ throw new HubError("this invite is invalid, used or expired", 403);
183
+ inv.usedBy = userId;
184
+ const m = this.memberships.get(inv.teamId);
185
+ if (!m.has(userId)) {
186
+ m.set(userId, { role: "member", joinedAt: this.now() });
187
+ // `invite` names which invite was used (a hash, never the token), so the
188
+ // inviter's Loom gives repo access to the person *that* link went to
189
+ this.appendFeedRaw(inv.teamId, null, { type: "member_joined", meta: { github: this.user(userId).github, invite: inviteTag(invite) } });
190
+ }
191
+ return { team: this.team(inv.teamId), role: m.get(userId).role };
192
+ }
193
+ members(userId, teamId) {
194
+ this.requireRole(teamId, userId, "viewer");
195
+ return [...this.memberships.get(teamId).entries()].map(([uid, m]) => ({
196
+ user: this.user(uid),
197
+ role: m.role,
198
+ joinedAt: m.joinedAt,
199
+ devices: [...this.devices.values()].filter((d) => d.userId === uid),
200
+ }));
201
+ }
202
+ removeMember(userId, teamId, target) {
203
+ const self = target === userId;
204
+ if (!self)
205
+ this.requireRole(teamId, userId, "owner");
206
+ const m = this.memberships.get(teamId);
207
+ if (!m?.has(target))
208
+ throw new HubError("not a member", 404);
209
+ const owners = [...m.values()].filter((x) => x.role === "owner").length;
210
+ if (m.get(target).role === "owner" && owners === 1)
211
+ throw new HubError("the last owner can't leave — hand ownership over first");
212
+ // Announce first, while the leaver can still hear it — otherwise their
213
+ // daemon never learns it was removed and keeps showing a team it's not in.
214
+ this.appendFeedRaw(teamId, null, { type: "member_left", meta: { github: this.user(target).github, rotateKey: true } });
215
+ m.delete(target);
216
+ // Their devices lose every envelope in this team; rotation (D6) is the
217
+ // owner's next step, and the feed says so.
218
+ for (const d of this.devices.values())
219
+ if (d.userId === target)
220
+ this.envelopes.delete(`${teamId}/${d.id}`);
221
+ for (const [k, p] of this.presenceByTeam.get(teamId) ?? [])
222
+ if (p.userId === target)
223
+ this.presenceByTeam.get(teamId).delete(k);
224
+ for (const [k, l] of this.leasesByTeam.get(teamId) ?? [])
225
+ if (l.userId === target)
226
+ this.leasesByTeam.get(teamId).delete(k);
227
+ }
228
+ // ── leases (Phase 2) ──
229
+ withStale(l) {
230
+ return { ...l, stale: this.now() - l.ts > LEASE_TTL_MS };
231
+ }
232
+ /**
233
+ * Everyone's live (non-stale) leases in a repo, except this user's own run.
234
+ * Matching on the run alone let anyone dodge a rival's lease by reusing its
235
+ * run id.
236
+ */
237
+ rivals(teamId, repo, exceptRun, userId) {
238
+ return [...(this.leasesByTeam.get(teamId)?.values() ?? [])]
239
+ .map((l) => this.withStale(l))
240
+ .filter((l) => l.repo === repo && !(l.runId === exceptRun && l.userId === userId) && !l.stale);
241
+ }
242
+ judge(teamId, repo, runId, scope, hardZones, userId) {
243
+ const rivals = this.rivals(teamId, repo, runId, userId);
244
+ const overlaps = rivals
245
+ .map((l) => ({ lease: l, paths: overlap(scope, l) }))
246
+ .filter((o) => o.paths.length > 0);
247
+ // D31: in a hard zone, any rival lease inside the same zone refuses the claim.
248
+ const zone = zoneOf(scope, hardZones);
249
+ if (zone) {
250
+ const holder = rivals.find((l) => zoneOf(l, [zone]) === zone);
251
+ if (holder)
252
+ return { overlaps, blockedBy: { lease: holder, zone } };
253
+ }
254
+ return { overlaps };
255
+ }
256
+ claimLease(userId, teamId, c) {
257
+ this.requireRole(teamId, userId, "member");
258
+ this.device(c.deviceId, userId);
259
+ const repo = normalizeRepo(c.repo);
260
+ if (!this.reposByTeam.get(teamId)?.has(repo))
261
+ throw new HubError(`${repo} isn't shared with this team`, 403);
262
+ const scope = { globs: c.globs.slice(0, 50), files: c.files.slice(0, 500), prefixes: c.prefixes.slice(0, 50) };
263
+ const verdict = this.judge(teamId, repo, c.runId, scope, c.hardZones, userId);
264
+ if (verdict.blockedBy)
265
+ return { lease: null, ...verdict };
266
+ const map = this.leasesByTeam.get(teamId) ?? new Map();
267
+ // one lease per (run, task): a re-claim replaces the old one
268
+ const existing = [...map.values()].find((l) => l.userId === userId && l.runId === c.runId && l.taskId === c.taskId);
269
+ const lease = {
270
+ ...scope,
271
+ id: existing?.id ?? id("l"),
272
+ teamId,
273
+ userId,
274
+ github: this.user(userId).github,
275
+ deviceId: c.deviceId,
276
+ repo,
277
+ runId: c.runId,
278
+ taskId: c.taskId,
279
+ state: "active",
280
+ ...(c.sealed ? { sealed: c.sealed } : {}),
281
+ since: existing?.since ?? this.now(),
282
+ ts: this.now(),
283
+ };
284
+ map.set(lease.id, lease);
285
+ this.leasesByTeam.set(teamId, map);
286
+ this.emit(teamId, { type: "lease", teamId, lease: this.withStale(lease) });
287
+ return { lease: this.withStale(lease), ...verdict };
288
+ }
289
+ ownLease(userId, teamId, leaseId) {
290
+ this.requireRole(teamId, userId, "member");
291
+ const l = this.leasesByTeam.get(teamId)?.get(leaseId);
292
+ if (!l || l.userId !== userId)
293
+ throw new HubError("that lease isn't yours", 403);
294
+ return l;
295
+ }
296
+ extendLease(userId, teamId, leaseId, scope, hardZones) {
297
+ const l = this.ownLease(userId, teamId, leaseId);
298
+ const wider = {
299
+ globs: [...new Set([...l.globs, ...scope.globs])].slice(0, 50),
300
+ files: [...new Set([...l.files, ...scope.files])].slice(0, 500),
301
+ prefixes: [...new Set([...l.prefixes, ...scope.prefixes])].slice(0, 50),
302
+ };
303
+ // only the NEW part is judged — what was already held stays held
304
+ const verdict = this.judge(teamId, l.repo, l.runId, scope, hardZones, userId);
305
+ if (verdict.blockedBy)
306
+ return { lease: null, ...verdict };
307
+ Object.assign(l, wider, { ts: this.now() });
308
+ this.emit(teamId, { type: "lease", teamId, lease: this.withStale(l) });
309
+ return { lease: this.withStale(l), ...verdict };
310
+ }
311
+ renewLeases(userId, teamId, deviceId) {
312
+ this.requireRole(teamId, userId, "member");
313
+ this.device(deviceId, userId);
314
+ let n = 0;
315
+ for (const l of this.leasesByTeam.get(teamId)?.values() ?? []) {
316
+ if (l.userId === userId && l.deviceId === deviceId) {
317
+ l.ts = this.now();
318
+ n++;
319
+ }
320
+ }
321
+ return n;
322
+ }
323
+ setRunLeaseState(userId, teamId, runId, state) {
324
+ this.requireRole(teamId, userId, "member");
325
+ let n = 0;
326
+ for (const l of this.leasesByTeam.get(teamId)?.values() ?? []) {
327
+ if (l.userId === userId && l.runId === runId && l.state !== state) {
328
+ l.state = state;
329
+ l.ts = this.now();
330
+ this.emit(teamId, { type: "lease", teamId, lease: this.withStale(l) });
331
+ n++;
332
+ }
333
+ }
334
+ return n;
335
+ }
336
+ releaseLeases(userId, teamId, runId, reason) {
337
+ this.requireRole(teamId, userId, "member");
338
+ const map = this.leasesByTeam.get(teamId);
339
+ const gone = [...(map?.values() ?? [])].filter((l) => l.userId === userId && l.runId === runId);
340
+ if (!gone.length)
341
+ return 0;
342
+ for (const l of gone)
343
+ map.delete(l.id);
344
+ this.emit(teamId, { type: "lease_gone", teamId, leaseIds: gone.map((l) => l.id), runId });
345
+ this.appendFeedRaw(teamId, userId, {
346
+ type: "lease_released",
347
+ repo: gone[0].repo,
348
+ meta: { runId, leases: gone.length, reason: reason.slice(0, 120) },
349
+ });
350
+ return gone.length;
351
+ }
352
+ // ── team memories (Phase 3) ──
353
+ mem(teamId) {
354
+ let m = this.memoriesByTeam.get(teamId);
355
+ if (!m)
356
+ this.memoriesByTeam.set(teamId, (m = new Map()));
357
+ return m;
358
+ }
359
+ publishMemory(userId, teamId, m) {
360
+ this.requireRole(teamId, userId, "member");
361
+ this.device(m.deviceId, userId);
362
+ const repo = normalizeRepo(m.repo);
363
+ if (!this.reposByTeam.get(teamId)?.has(repo))
364
+ throw new HubError(`${repo} isn't shared with this team`, 403);
365
+ if (!/^[A-Za-z0-9_-]{4,64}$/.test(m.id))
366
+ throw new HubError("bad memory id");
367
+ if (!m.hmac || !m.sealed?.c)
368
+ throw new HubError("a team memory needs its hmac and sealed content");
369
+ const github = this.user(userId).github;
370
+ const map = this.mem(teamId);
371
+ const own = map.get(m.id);
372
+ if (own && own.authorId !== userId)
373
+ throw new HubError("that memory id belongs to someone else", 403);
374
+ const twin = [...map.values()].find((x) => x.repo === repo && x.hmac === m.hmac && x.state === "live" && x.id !== m.id);
375
+ if (twin && !own) {
376
+ if (!twin.confirmedBy.includes(github))
377
+ twin.confirmedBy.push(github);
378
+ twin.updatedAt = this.now();
379
+ this.emit(teamId, { type: "memory", teamId, memory: { ...twin } });
380
+ return { memory: { ...twin }, merged: true };
381
+ }
382
+ if (m.supersedes && !map.has(m.supersedes))
383
+ throw new HubError(`no team memory "${m.supersedes}" to supersede`, 404);
384
+ const tm = {
385
+ id: m.id,
386
+ teamId,
387
+ repo,
388
+ authorId: userId,
389
+ author: github,
390
+ hmac: m.hmac,
391
+ sealed: m.sealed,
392
+ state: "live",
393
+ ...(m.supersedes ? { supersedes: m.supersedes } : {}),
394
+ confirmedBy: own?.confirmedBy ?? [github],
395
+ createdAt: own?.createdAt ?? this.now(),
396
+ updatedAt: this.now(),
397
+ };
398
+ map.set(tm.id, tm);
399
+ this.emit(teamId, { type: "memory", teamId, memory: { ...tm } });
400
+ return { memory: { ...tm }, merged: false };
401
+ }
402
+ ownMemory(userId, teamId, id) {
403
+ this.requireRole(teamId, userId, "member");
404
+ const m = this.mem(teamId).get(id);
405
+ if (!m)
406
+ throw new HubError(`no team memory "${id}"`, 404);
407
+ if (m.authorId !== userId)
408
+ throw new HubError("only its author can change a memory — record a correction instead", 403);
409
+ return m;
410
+ }
411
+ updateTeamMemory(userId, teamId, id, patch) {
412
+ const m = this.ownMemory(userId, teamId, id);
413
+ m.hmac = patch.hmac;
414
+ m.sealed = patch.sealed;
415
+ m.updatedAt = this.now();
416
+ this.emit(teamId, { type: "memory", teamId, memory: { ...m } });
417
+ return { ...m };
418
+ }
419
+ forgetTeamMemory(userId, teamId, id, reason) {
420
+ const m = this.ownMemory(userId, teamId, id);
421
+ m.state = "forgotten";
422
+ m.resolvedReason = reason.slice(0, 200);
423
+ m.updatedAt = this.now();
424
+ this.emit(teamId, { type: "memory", teamId, memory: { ...m } });
425
+ }
426
+ resolveMemories(userId, teamId, winnerId, loserId, reason) {
427
+ this.requireRole(teamId, userId, "member");
428
+ const map = this.mem(teamId);
429
+ const winner = map.get(winnerId);
430
+ const loser = map.get(loserId);
431
+ if (!winner || !loser)
432
+ throw new HubError("both memories must exist", 404);
433
+ if (winnerId === loserId)
434
+ throw new HubError("a memory can't supersede itself");
435
+ loser.state = "superseded";
436
+ loser.supersededBy = winnerId;
437
+ loser.resolvedBy = this.user(userId).github;
438
+ loser.resolvedReason = reason.slice(0, 200);
439
+ loser.updatedAt = this.now();
440
+ // the winner inherits the loser's confirmations: they agreed on the topic
441
+ for (const g of loser.confirmedBy)
442
+ if (!winner.confirmedBy.includes(g))
443
+ winner.confirmedBy.push(g);
444
+ winner.updatedAt = this.now();
445
+ this.emit(teamId, { type: "memory", teamId, memory: { ...loser } });
446
+ this.emit(teamId, { type: "memory", teamId, memory: { ...winner } });
447
+ this.appendFeedRaw(teamId, userId, {
448
+ type: "memory_resolved",
449
+ repo: loser.repo,
450
+ meta: { winner: winnerId, loser: loserId, reason: reason.slice(0, 200) },
451
+ });
452
+ return { ...loser };
453
+ }
454
+ teamMemories(userId, teamId, repo, opts = {}) {
455
+ this.requireRole(teamId, userId, "viewer");
456
+ const r = normalizeRepo(repo);
457
+ return [...this.mem(teamId).values()]
458
+ .filter((m) => m.repo === r && (opts.history || m.state === "live"))
459
+ .map((m) => ({ ...m, confirmedBy: [...m.confirmedBy] }));
460
+ }
461
+ // ── runners and jobs (Phase 5) ──
462
+ registerRunner(userId, input) {
463
+ const d = this.device(input.deviceId, userId);
464
+ const r = {
465
+ deviceId: d.id,
466
+ userId,
467
+ github: this.user(userId).github,
468
+ label: d.label,
469
+ kinds: [...new Set(input.kinds.map(String))].slice(0, 20),
470
+ shared: Boolean(input.shared),
471
+ capacity: Math.max(1, Math.min(8, Math.floor(input.capacity ?? 1))),
472
+ lastSeen: this.now(),
473
+ };
474
+ this.runnersByDevice.set(d.id, r);
475
+ return { ...r };
476
+ }
477
+ runners(userId, teamId) {
478
+ this.requireRole(teamId, userId, "viewer");
479
+ const members = this.memberships.get(teamId) ?? new Map();
480
+ return [...this.runnersByDevice.values()].filter((r) => members.has(r.userId)).map((r) => ({ ...r }));
481
+ }
482
+ revokeDevice(userId, deviceId) {
483
+ this.device(deviceId, userId);
484
+ this.devices.delete(deviceId);
485
+ this.runnersByDevice.delete(deviceId);
486
+ for (const k of [...this.envelopes.keys()])
487
+ if (k.endsWith(`/${deviceId}`))
488
+ this.envelopes.delete(k);
489
+ }
490
+ jobMap(teamId) {
491
+ let m = this.jobsByTeam.get(teamId);
492
+ if (!m)
493
+ this.jobsByTeam.set(teamId, (m = new Map()));
494
+ return m;
495
+ }
496
+ /** May this runner take this job? Its owner's jobs, or anyone's when shared (D68). */
497
+ eligible(r, j) {
498
+ if (j.target)
499
+ return j.target === r.deviceId;
500
+ return j.userId === r.userId || r.shared;
501
+ }
502
+ createJob(userId, teamId, j) {
503
+ this.requireRole(teamId, userId, "member");
504
+ this.device(j.deviceId, userId);
505
+ const repo = normalizeRepo(j.repo);
506
+ if (!this.reposByTeam.get(teamId)?.has(repo))
507
+ throw new HubError(`${repo} isn't shared with this team`, 403);
508
+ if (!["start", "continue", "fix", "return", "land"].includes(j.kind))
509
+ throw new HubError("bad job kind");
510
+ if (!j.sealed?.c)
511
+ throw new HubError("a job needs its sealed payload");
512
+ if (j.target) {
513
+ const r = this.runnersByDevice.get(j.target);
514
+ const members = this.memberships.get(teamId);
515
+ if (!r || !members?.has(r.userId))
516
+ throw new HubError("no such runner on this team", 404);
517
+ if (r.userId !== userId && !r.shared)
518
+ throw new HubError("that runner isn't shared", 403);
519
+ }
520
+ const job = {
521
+ id: id("j"),
522
+ teamId,
523
+ repo,
524
+ kind: j.kind,
525
+ userId,
526
+ github: this.user(userId).github,
527
+ ...(j.target ? { target: j.target } : {}),
528
+ state: "queued",
529
+ sealed: j.sealed,
530
+ createdAt: this.now(),
531
+ updatedAt: this.now(),
532
+ };
533
+ this.jobMap(teamId).set(job.id, job);
534
+ this.emit(teamId, { type: "job", teamId, job: { ...job } });
535
+ return { ...job };
536
+ }
537
+ claimJob(userId, teamId, runnerDeviceId, instantOnly = false) {
538
+ this.requireRole(teamId, userId, "member");
539
+ this.device(runnerDeviceId, userId);
540
+ const r = this.runnersByDevice.get(runnerDeviceId);
541
+ if (!r)
542
+ throw new HubError("that device isn't a runner — register it first", 403);
543
+ r.lastSeen = this.now();
544
+ const now = this.now();
545
+ const open = [...this.jobMap(teamId).values()]
546
+ .filter((j) => j.state === "queued" || (j.state === "claimed" && now - (j.heartbeatAt ?? 0) > JOB_TTL_MS))
547
+ .filter((j) => this.eligible(r, j))
548
+ .filter((j) => !instantOnly || j.kind === "return" || j.kind === "land")
549
+ .sort((a, b) => a.createdAt - b.createdAt);
550
+ const j = open[0];
551
+ if (!j)
552
+ return null;
553
+ Object.assign(j, { state: "claimed", runnerId: r.deviceId, runnerGithub: r.github, claimedAt: now, heartbeatAt: now, updatedAt: now });
554
+ this.emit(teamId, { type: "job", teamId, job: { ...j } });
555
+ return { ...j };
556
+ }
557
+ heldJob(userId, teamId, jobId, runnerDeviceId) {
558
+ this.requireRole(teamId, userId, "member");
559
+ this.device(runnerDeviceId, userId);
560
+ const j = this.jobMap(teamId).get(jobId);
561
+ if (!j)
562
+ throw new HubError(`no job "${jobId}"`, 404);
563
+ if (j.runnerId !== runnerDeviceId || j.state !== "claimed")
564
+ throw new HubError("that runner doesn't hold this job", 409);
565
+ return j;
566
+ }
567
+ heartbeatJob(userId, teamId, jobId, runnerDeviceId, progress) {
568
+ const j = this.heldJob(userId, teamId, jobId, runnerDeviceId);
569
+ j.heartbeatAt = this.now();
570
+ j.updatedAt = this.now();
571
+ if (progress)
572
+ j.progress = progress;
573
+ const r = this.runnersByDevice.get(runnerDeviceId);
574
+ if (r)
575
+ r.lastSeen = this.now();
576
+ this.emit(teamId, { type: "job", teamId, job: { ...j } });
577
+ return { ...j };
578
+ }
579
+ finishJob(userId, teamId, jobId, runnerDeviceId, outcome) {
580
+ const j = this.heldJob(userId, teamId, jobId, runnerDeviceId);
581
+ if (outcome.state !== "done" && outcome.state !== "failed")
582
+ throw new HubError("a job finishes done or failed");
583
+ j.state = outcome.state;
584
+ if (outcome.result)
585
+ j.result = outcome.result;
586
+ if (outcome.error)
587
+ j.error = outcome.error.slice(0, 500);
588
+ j.updatedAt = this.now();
589
+ this.emit(teamId, { type: "job", teamId, job: { ...j } });
590
+ return { ...j };
591
+ }
592
+ cancelJob(userId, teamId, jobId) {
593
+ this.requireRole(teamId, userId, "member");
594
+ const j = this.jobMap(teamId).get(jobId);
595
+ if (!j)
596
+ throw new HubError(`no job "${jobId}"`, 404);
597
+ if (j.userId !== userId)
598
+ throw new HubError("only whoever asked can cancel a job", 403);
599
+ if (j.state === "done" || j.state === "failed")
600
+ return { ...j };
601
+ j.state = "cancelled";
602
+ j.updatedAt = this.now();
603
+ this.emit(teamId, { type: "job", teamId, job: { ...j } });
604
+ return { ...j };
605
+ }
606
+ jobs(userId, teamId, opts = {}) {
607
+ this.requireRole(teamId, userId, "viewer");
608
+ return [...this.jobMap(teamId).values()]
609
+ .filter((j) => !opts.active || j.state === "queued" || j.state === "claimed")
610
+ .sort((a, b) => a.createdAt - b.createdAt)
611
+ .map((j) => ({ ...j }));
612
+ }
613
+ // ── GitHub webhooks (Phase 6, D83) ──
614
+ webhookSecret(userId, teamId, rotate = false) {
615
+ this.team(teamId);
616
+ this.requireRole(teamId, userId, "owner");
617
+ let secret = this.webhookSecrets.get(teamId);
618
+ if (!secret || rotate) {
619
+ secret = crypto.randomBytes(32).toString("hex");
620
+ this.webhookSecrets.set(teamId, secret);
621
+ }
622
+ return { secret };
623
+ }
624
+ /** The receiver's side (no session): the secret to verify a delivery against; null for no such team, or none set. */
625
+ webhookSecretOf(teamId) {
626
+ return this.teamsById.has(teamId) ? (this.webhookSecrets.get(teamId) ?? null) : null;
627
+ }
628
+ /**
629
+ * A verified delivery: its events, for repos this team shares only, appended
630
+ * as system events (no member). Returns how many were new — the rest were
631
+ * already in the feed (polled first, or a redelivery).
632
+ */
633
+ receiveGithubWebhook(teamId, event, payload) {
634
+ this.team(teamId);
635
+ const repos = [...(this.reposByTeam.get(teamId) ?? [])];
636
+ let n = 0;
637
+ for (const e of githubWebhookFeed(event, payload, repos))
638
+ if (this.appendFeedRaw(teamId, null, e))
639
+ n++;
640
+ return n;
641
+ }
642
+ leases(userId, teamId, repo) {
643
+ this.requireRole(teamId, userId, "viewer");
644
+ const r = repo ? normalizeRepo(repo) : null;
645
+ return [...(this.leasesByTeam.get(teamId)?.values() ?? [])]
646
+ .filter((l) => !r || l.repo === r)
647
+ .map((l) => this.withStale(l));
648
+ }
649
+ putKeyEnvelopes(userId, teamId, version, envs) {
650
+ const team = this.team(teamId);
651
+ this.requireRole(teamId, userId, "member");
652
+ if (version !== team.keyVersion && version !== team.keyVersion + 1) {
653
+ throw new HubError(`key version ${version} is stale (current ${team.keyVersion})`, 409);
654
+ }
655
+ // Rotation is an owner act; re-sealing the current key for a new device
656
+ // (a teammate's second laptop) is any member's.
657
+ if (version === team.keyVersion + 1)
658
+ this.requireRole(teamId, userId, "owner");
659
+ const members = this.memberships.get(teamId);
660
+ for (const e of envs) {
661
+ const d = this.devices.get(e.deviceId);
662
+ if (!d || !members.has(d.userId))
663
+ throw new HubError(`device ${e.deviceId} isn't on this team`, 400);
664
+ const k = `${teamId}/${e.deviceId}`;
665
+ const list = (this.envelopes.get(k) ?? []).filter((x) => x.version !== version);
666
+ list.push({ version, box: e.box });
667
+ this.envelopes.set(k, list);
668
+ }
669
+ if (version === team.keyVersion + 1) {
670
+ team.keyVersion = version;
671
+ this.appendFeedRaw(teamId, userId, { type: "key_rotated", meta: { version } });
672
+ }
673
+ }
674
+ keyEnvelopes(userId, teamId, deviceId) {
675
+ this.requireRole(teamId, userId, "viewer");
676
+ this.device(deviceId, userId);
677
+ return this.envelopes.get(`${teamId}/${deviceId}`) ?? [];
678
+ }
679
+ shareRepo(userId, teamId, repo) {
680
+ this.requireRole(teamId, userId, "member");
681
+ const r = normalizeRepo(repo);
682
+ const set = this.reposByTeam.get(teamId) ?? new Set();
683
+ if (set.has(r))
684
+ return;
685
+ set.add(r);
686
+ this.reposByTeam.set(teamId, set);
687
+ this.appendFeedRaw(teamId, userId, { type: "repo_shared", repo: r, meta: { repo: r } });
688
+ }
689
+ repos(userId, teamId) {
690
+ this.requireRole(teamId, userId, "viewer");
691
+ return [...(this.reposByTeam.get(teamId) ?? [])].sort();
692
+ }
693
+ presenceKey(p) {
694
+ return `${p.userId}/${p.deviceId}/${p.repo}/${p.agent}`;
695
+ }
696
+ heartbeat(userId, teamId, p) {
697
+ this.requireRole(teamId, userId, "member");
698
+ this.device(p.deviceId, userId);
699
+ const repo = normalizeRepo(p.repo);
700
+ if (!this.reposByTeam.get(teamId)?.has(repo))
701
+ throw new HubError(`${repo} isn't shared with this team`, 403);
702
+ const u = this.user(userId);
703
+ const full = {
704
+ ...p,
705
+ repo,
706
+ touches: (p.touches ?? []).slice(0, 50).map((t) => String(t).slice(0, 200)),
707
+ teamId,
708
+ userId,
709
+ github: u.github,
710
+ ts: this.now(),
711
+ };
712
+ const map = this.presenceByTeam.get(teamId) ?? new Map();
713
+ map.set(this.presenceKey(full), full);
714
+ this.presenceByTeam.set(teamId, map);
715
+ this.emit(teamId, { type: "presence", teamId, presence: full });
716
+ }
717
+ clearPresence(userId, teamId, key) {
718
+ this.requireRole(teamId, userId, "member");
719
+ const repo = normalizeRepo(key.repo);
720
+ const k = this.presenceKey({ userId, deviceId: key.deviceId, agent: key.agent, repo });
721
+ if (this.presenceByTeam.get(teamId)?.delete(k)) {
722
+ this.emit(teamId, { type: "presence_gone", teamId, userId, deviceId: key.deviceId, agent: key.agent, repo });
723
+ }
724
+ }
725
+ presence(userId, teamId) {
726
+ this.requireRole(teamId, userId, "viewer");
727
+ const cutoff = this.now() - PRESENCE_TTL_MS;
728
+ return [...(this.presenceByTeam.get(teamId)?.values() ?? [])].filter((p) => p.ts >= cutoff);
729
+ }
730
+ appendFeed(userId, teamId, e) {
731
+ this.requireRole(teamId, userId, "member");
732
+ if (e.deviceId)
733
+ this.device(e.deviceId, userId);
734
+ if (e.type === "crew_request" || e.type === "crew_request_answered") {
735
+ const id = e.meta.requestId;
736
+ if (typeof id !== "string" || !/^[a-zA-Z0-9-]{1,80}$/.test(id))
737
+ throw new HubError("invalid crew request id", 400);
738
+ const request = (this.feedByTeam.get(teamId) ?? []).find(f => f.type === "crew_request" && f.meta.requestId === id);
739
+ if (e.type === "crew_request") {
740
+ if (e.dedupeKey !== `crew-request:${id}`)
741
+ throw new HubError("invalid crew request dedupe key", 400);
742
+ if (!e.repo || !this.reposByTeam.get(teamId)?.has(normalizeRepo(e.repo)))
743
+ throw new HubError("request repo is not shared with this team", 403);
744
+ const owner = this.members(userId, teamId).find(m => m.user.github.toLowerCase() === String(e.meta.owner).toLowerCase());
745
+ if (!owner || owner.role === "viewer")
746
+ throw new HubError("the requested owner is not an active team member", 403);
747
+ for (const field of ["crew", "teammate"])
748
+ if (typeof e.meta[field] !== "string" || !/^[a-z0-9][a-z0-9-]{0,40}$/.test(String(e.meta[field])))
749
+ throw new HubError(`invalid ${field}`, 400);
750
+ const expiry = Number(e.meta.expiresAt);
751
+ if (!Number.isFinite(expiry) || expiry <= this.now() || expiry > this.now() + 7 * 86400000)
752
+ throw new HubError("crew request expiry must be within seven days", 400);
753
+ if (!e.sealed || JSON.stringify(e.sealed).length > 16000)
754
+ throw new HubError("crew request text must be sealed and bounded", 400);
755
+ if (request && (request.userId !== userId || JSON.stringify(request.meta) !== JSON.stringify(e.meta) || JSON.stringify(request.sealed) !== JSON.stringify(e.sealed)))
756
+ throw new HubError("crew request id already belongs to another request", 409);
757
+ }
758
+ else {
759
+ if (!request || request.repo !== e.repo)
760
+ throw new HubError("crew request not found", 404);
761
+ if (String(request.meta.owner).toLowerCase() !== this.user(userId).github.toLowerCase())
762
+ throw new HubError("only the requested crew's owner may answer", 403);
763
+ if (e.meta.state !== "accepted" && e.meta.state !== "declined")
764
+ throw new HubError("answer must accept or decline", 400);
765
+ if (e.dedupeKey !== `crew-answer:${id}`)
766
+ throw new HubError("invalid crew answer dedupe key", 400);
767
+ const previous = (this.feedByTeam.get(teamId) ?? []).find(f => f.type === "crew_request_answered" && f.meta.requestId === id);
768
+ if (previous && previous.meta.state !== e.meta.state)
769
+ throw new HubError("this request has already been answered", 409);
770
+ if (!previous && Number(request.meta.expiresAt) <= this.now())
771
+ throw new HubError("crew request expired", 409);
772
+ }
773
+ }
774
+ return this.appendFeedRaw(teamId, userId, e);
775
+ }
776
+ appendFeedRaw(teamId, userId, e) {
777
+ if (e.dedupeKey) {
778
+ const k = `${teamId}/${e.dedupeKey}`;
779
+ if (this.dedupe.has(k))
780
+ return null;
781
+ this.dedupe.add(k);
782
+ }
783
+ const ev = {
784
+ ...e,
785
+ ...(e.repo ? { repo: normalizeRepo(e.repo) } : {}),
786
+ id: ++this.feedSeq,
787
+ teamId,
788
+ userId,
789
+ github: userId ? this.user(userId).github : null,
790
+ ts: this.now(),
791
+ };
792
+ const list = this.feedByTeam.get(teamId) ?? [];
793
+ list.push(ev);
794
+ if (list.length > 5000)
795
+ list.splice(0, list.length - 5000);
796
+ this.feedByTeam.set(teamId, list);
797
+ this.emit(teamId, { type: "feed", teamId, event: ev });
798
+ return ev;
799
+ }
800
+ feed(userId, teamId, opts = {}) {
801
+ this.requireRole(teamId, userId, "viewer");
802
+ const list = (this.feedByTeam.get(teamId) ?? []).filter((e) => e.id > (opts.since ?? 0));
803
+ return list.slice(-Math.min(opts.limit ?? 200, 1000));
804
+ }
805
+ subscribe(userId, teamId, cb) {
806
+ this.requireRole(teamId, userId, "viewer");
807
+ const set = this.listeners.get(teamId) ?? new Set();
808
+ // A removed member stops hearing the team the moment they're removed.
809
+ const guarded = (e) => {
810
+ if (this.role(teamId, userId))
811
+ cb(e);
812
+ };
813
+ set.add(guarded);
814
+ this.listeners.set(teamId, set);
815
+ return () => set.delete(guarded);
816
+ }
817
+ emit(teamId, e) {
818
+ for (const l of this.listeners.get(teamId) ?? []) {
819
+ try {
820
+ l(e);
821
+ }
822
+ catch {
823
+ /* one bad listener never breaks the hub */
824
+ }
825
+ }
826
+ }
827
+ }
828
+ class MemoryHubClient {
829
+ hub;
830
+ userId;
831
+ constructor(hub, userId) {
832
+ this.hub = hub;
833
+ this.userId = userId;
834
+ }
835
+ run(fn) {
836
+ try {
837
+ return Promise.resolve(fn());
838
+ }
839
+ catch (err) {
840
+ return Promise.reject(err);
841
+ }
842
+ }
843
+ me() {
844
+ return this.run(() => this.hub.user(this.userId));
845
+ }
846
+ createTeam(name) {
847
+ return this.run(() => this.hub.createTeam(this.userId, name));
848
+ }
849
+ teams() {
850
+ return this.run(() => this.hub.teamsOf(this.userId));
851
+ }
852
+ registerDevice(input) {
853
+ return this.run(() => this.hub.registerDevice(this.userId, input));
854
+ }
855
+ createInvite(teamId, ttlMs) {
856
+ return this.run(() => this.hub.createInvite(this.userId, teamId, ttlMs));
857
+ }
858
+ redeemInvite(invite) {
859
+ return this.run(() => this.hub.redeemInvite(this.userId, invite));
860
+ }
861
+ members(teamId) {
862
+ return this.run(() => this.hub.members(this.userId, teamId));
863
+ }
864
+ removeMember(teamId, userId) {
865
+ return this.run(() => this.hub.removeMember(this.userId, teamId, userId));
866
+ }
867
+ putKeyEnvelopes(teamId, version, envelopes) {
868
+ return this.run(() => this.hub.putKeyEnvelopes(this.userId, teamId, version, envelopes));
869
+ }
870
+ keyEnvelopes(teamId, deviceId) {
871
+ return this.run(() => this.hub.keyEnvelopes(this.userId, teamId, deviceId));
872
+ }
873
+ shareRepo(teamId, repo) {
874
+ return this.run(() => this.hub.shareRepo(this.userId, teamId, repo));
875
+ }
876
+ repos(teamId) {
877
+ return this.run(() => this.hub.repos(this.userId, teamId));
878
+ }
879
+ heartbeat(teamId, p) {
880
+ return this.run(() => this.hub.heartbeat(this.userId, teamId, p));
881
+ }
882
+ clearPresence(teamId, key) {
883
+ return this.run(() => this.hub.clearPresence(this.userId, teamId, key));
884
+ }
885
+ presence(teamId) {
886
+ return this.run(() => this.hub.presence(this.userId, teamId));
887
+ }
888
+ appendFeed(teamId, e) {
889
+ return this.run(() => this.hub.appendFeed(this.userId, teamId, e));
890
+ }
891
+ feed(teamId, opts) {
892
+ return this.run(() => this.hub.feed(this.userId, teamId, opts));
893
+ }
894
+ subscribe(teamId, cb) {
895
+ return this.run(() => this.hub.subscribe(this.userId, teamId, cb));
896
+ }
897
+ claimLease(teamId, c) {
898
+ return this.run(() => this.hub.claimLease(this.userId, teamId, c));
899
+ }
900
+ extendLease(teamId, leaseId, scope, hardZones) {
901
+ return this.run(() => this.hub.extendLease(this.userId, teamId, leaseId, scope, hardZones));
902
+ }
903
+ renewLeases(teamId, deviceId) {
904
+ return this.run(() => this.hub.renewLeases(this.userId, teamId, deviceId));
905
+ }
906
+ setRunLeaseState(teamId, runId, state) {
907
+ return this.run(() => this.hub.setRunLeaseState(this.userId, teamId, runId, state));
908
+ }
909
+ releaseLeases(teamId, runId, reason) {
910
+ return this.run(() => this.hub.releaseLeases(this.userId, teamId, runId, reason));
911
+ }
912
+ leases(teamId, repo) {
913
+ return this.run(() => this.hub.leases(this.userId, teamId, repo));
914
+ }
915
+ publishMemory(teamId, m) {
916
+ return this.run(() => this.hub.publishMemory(this.userId, teamId, m));
917
+ }
918
+ updateTeamMemory(teamId, id, patch) {
919
+ return this.run(() => this.hub.updateTeamMemory(this.userId, teamId, id, patch));
920
+ }
921
+ forgetTeamMemory(teamId, id, reason) {
922
+ return this.run(() => this.hub.forgetTeamMemory(this.userId, teamId, id, reason));
923
+ }
924
+ resolveMemories(teamId, winnerId, loserId, reason) {
925
+ return this.run(() => this.hub.resolveMemories(this.userId, teamId, winnerId, loserId, reason));
926
+ }
927
+ teamMemories(teamId, repo, opts) {
928
+ return this.run(() => this.hub.teamMemories(this.userId, teamId, repo, opts));
929
+ }
930
+ registerRunner(input) {
931
+ return this.run(() => this.hub.registerRunner(this.userId, input));
932
+ }
933
+ runners(teamId) {
934
+ return this.run(() => this.hub.runners(this.userId, teamId));
935
+ }
936
+ revokeDevice(deviceId) {
937
+ return this.run(() => this.hub.revokeDevice(this.userId, deviceId));
938
+ }
939
+ createJob(teamId, j) {
940
+ return this.run(() => this.hub.createJob(this.userId, teamId, j));
941
+ }
942
+ claimJob(teamId, runnerDeviceId, instantOnly = false) {
943
+ return this.run(() => this.hub.claimJob(this.userId, teamId, runnerDeviceId, instantOnly));
944
+ }
945
+ heartbeatJob(teamId, jobId, runnerDeviceId, progress) {
946
+ return this.run(() => this.hub.heartbeatJob(this.userId, teamId, jobId, runnerDeviceId, progress));
947
+ }
948
+ finishJob(teamId, jobId, runnerDeviceId, outcome) {
949
+ return this.run(() => this.hub.finishJob(this.userId, teamId, jobId, runnerDeviceId, outcome));
950
+ }
951
+ cancelJob(teamId, jobId) {
952
+ return this.run(() => this.hub.cancelJob(this.userId, teamId, jobId));
953
+ }
954
+ jobs(teamId, opts) {
955
+ return this.run(() => this.hub.jobs(this.userId, teamId, opts));
956
+ }
957
+ webhookSecret(teamId, rotate) {
958
+ return this.run(() => this.hub.webhookSecret(this.userId, teamId, rotate));
959
+ }
960
+ }
961
+ function id(prefix) {
962
+ return `${prefix}_${crypto.randomBytes(8).toString("hex")}`;
963
+ }
964
+ //# sourceMappingURL=team-hub.js.map