@mrpatronz/nexusflow 2.8.1 → 2.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (732) hide show
  1. package/README.md +172 -113
  2. package/dist/agent/AcpCliAdapter.d.ts +2 -0
  3. package/dist/agent/AcpCliAdapter.d.ts.map +1 -1
  4. package/dist/agent/AcpCliAdapter.js +50 -4
  5. package/dist/agent/AcpCliAdapter.js.map +1 -1
  6. package/dist/agent/AntigravityCliAdapter.d.ts +44 -2
  7. package/dist/agent/AntigravityCliAdapter.d.ts.map +1 -1
  8. package/dist/agent/AntigravityCliAdapter.js +363 -16
  9. package/dist/agent/AntigravityCliAdapter.js.map +1 -1
  10. package/dist/agent/ClaudeCliAdapter.d.ts +3 -0
  11. package/dist/agent/ClaudeCliAdapter.d.ts.map +1 -1
  12. package/dist/agent/ClaudeCliAdapter.js +37 -10
  13. package/dist/agent/ClaudeCliAdapter.js.map +1 -1
  14. package/dist/agent/ClaudeSdkAdapter.d.ts +1 -0
  15. package/dist/agent/ClaudeSdkAdapter.d.ts.map +1 -1
  16. package/dist/agent/ClaudeSdkAdapter.js +38 -11
  17. package/dist/agent/ClaudeSdkAdapter.js.map +1 -1
  18. package/dist/agent/CliAdapterBase.d.ts +2 -0
  19. package/dist/agent/CliAdapterBase.d.ts.map +1 -1
  20. package/dist/agent/CliAdapterBase.js +5 -1
  21. package/dist/agent/CliAdapterBase.js.map +1 -1
  22. package/dist/agent/CodexCliAdapter.d.ts +1 -1
  23. package/dist/agent/CodexCliAdapter.d.ts.map +1 -1
  24. package/dist/agent/CodexCliAdapter.js +5 -4
  25. package/dist/agent/CodexCliAdapter.js.map +1 -1
  26. package/dist/agent/CodexSdkAdapter.d.ts.map +1 -1
  27. package/dist/agent/CodexSdkAdapter.js +4 -1
  28. package/dist/agent/CodexSdkAdapter.js.map +1 -1
  29. package/dist/agent/CopilotAcpAdapter.d.ts.map +1 -1
  30. package/dist/agent/CopilotAcpAdapter.js +2 -2
  31. package/dist/agent/CopilotAcpAdapter.js.map +1 -1
  32. package/dist/agent/ProviderRegistry.d.ts +8 -1
  33. package/dist/agent/ProviderRegistry.d.ts.map +1 -1
  34. package/dist/agent/ProviderRegistry.js.map +1 -1
  35. package/dist/agent/TurnSessionManager.d.ts +65 -0
  36. package/dist/agent/TurnSessionManager.d.ts.map +1 -0
  37. package/dist/agent/TurnSessionManager.js +330 -0
  38. package/dist/agent/TurnSessionManager.js.map +1 -0
  39. package/dist/agent/adapters.d.ts.map +1 -1
  40. package/dist/agent/adapters.js +23 -6
  41. package/dist/agent/adapters.js.map +1 -1
  42. package/dist/agent/cliAvailability.d.ts.map +1 -1
  43. package/dist/agent/cliAvailability.js +12 -0
  44. package/dist/agent/cliAvailability.js.map +1 -1
  45. package/dist/agent/mcp-config.d.ts +0 -5
  46. package/dist/agent/mcp-config.d.ts.map +1 -1
  47. package/dist/agent/mcp-config.js +2 -1
  48. package/dist/agent/mcp-config.js.map +1 -1
  49. package/dist/agent/models.d.ts +13 -0
  50. package/dist/agent/models.d.ts.map +1 -1
  51. package/dist/agent/models.js +262 -8
  52. package/dist/agent/models.js.map +1 -1
  53. package/dist/agent/nativeTools.d.ts.map +1 -1
  54. package/dist/agent/nativeTools.js +1 -0
  55. package/dist/agent/nativeTools.js.map +1 -1
  56. package/dist/agent/orchestrator.d.ts.map +1 -1
  57. package/dist/agent/orchestrator.js +3 -2
  58. package/dist/agent/orchestrator.js.map +1 -1
  59. package/dist/agent/session.d.ts +1 -0
  60. package/dist/agent/session.d.ts.map +1 -1
  61. package/dist/agent/session.js.map +1 -1
  62. package/dist/analyzers/detect-deps.js +16 -1
  63. package/dist/analyzers/detect-deps.js.map +1 -1
  64. package/dist/analyzers/run-analyzer.d.ts.map +1 -1
  65. package/dist/analyzers/run-analyzer.js.map +1 -1
  66. package/dist/commands/adapter.d.ts.map +1 -1
  67. package/dist/commands/adapter.js +15 -14
  68. package/dist/commands/adapter.js.map +1 -1
  69. package/dist/commands/add-repo.d.ts.map +1 -1
  70. package/dist/commands/add-repo.js +6 -5
  71. package/dist/commands/add-repo.js.map +1 -1
  72. package/dist/commands/commit.js +6 -8
  73. package/dist/commands/commit.js.map +1 -1
  74. package/dist/commands/config.d.ts.map +1 -1
  75. package/dist/commands/config.js +2 -1
  76. package/dist/commands/config.js.map +1 -1
  77. package/dist/commands/create.d.ts +4 -4
  78. package/dist/commands/create.d.ts.map +1 -1
  79. package/dist/commands/create.js +8 -7
  80. package/dist/commands/create.js.map +1 -1
  81. package/dist/commands/desktop.d.ts +44 -3
  82. package/dist/commands/desktop.d.ts.map +1 -1
  83. package/dist/commands/desktop.js +246 -7
  84. package/dist/commands/desktop.js.map +1 -1
  85. package/dist/commands/diff.d.ts.map +1 -1
  86. package/dist/commands/diff.js +3 -2
  87. package/dist/commands/diff.js.map +1 -1
  88. package/dist/commands/doctor.d.ts.map +1 -1
  89. package/dist/commands/doctor.js +3 -2
  90. package/dist/commands/doctor.js.map +1 -1
  91. package/dist/commands/finish.js +2 -1
  92. package/dist/commands/finish.js.map +1 -1
  93. package/dist/commands/handoff.d.ts.map +1 -1
  94. package/dist/commands/handoff.js +12 -12
  95. package/dist/commands/handoff.js.map +1 -1
  96. package/dist/commands/init.d.ts +2 -2
  97. package/dist/commands/init.d.ts.map +1 -1
  98. package/dist/commands/init.js +8 -7
  99. package/dist/commands/init.js.map +1 -1
  100. package/dist/commands/isolate.d.ts.map +1 -1
  101. package/dist/commands/isolate.js +2 -1
  102. package/dist/commands/isolate.js.map +1 -1
  103. package/dist/commands/knowledge.d.ts.map +1 -1
  104. package/dist/commands/knowledge.js +3 -2
  105. package/dist/commands/knowledge.js.map +1 -1
  106. package/dist/commands/list.d.ts.map +1 -1
  107. package/dist/commands/list.js +3 -2
  108. package/dist/commands/list.js.map +1 -1
  109. package/dist/commands/logs.js +3 -3
  110. package/dist/commands/logs.js.map +1 -1
  111. package/dist/commands/mcp.d.ts +1 -2
  112. package/dist/commands/mcp.d.ts.map +1 -1
  113. package/dist/commands/mcp.js +19 -12
  114. package/dist/commands/mcp.js.map +1 -1
  115. package/dist/commands/migrate.d.ts +12 -0
  116. package/dist/commands/migrate.d.ts.map +1 -0
  117. package/dist/commands/migrate.js +128 -0
  118. package/dist/commands/migrate.js.map +1 -0
  119. package/dist/commands/open.d.ts.map +1 -1
  120. package/dist/commands/open.js +2 -1
  121. package/dist/commands/open.js.map +1 -1
  122. package/dist/commands/progress.d.ts.map +1 -1
  123. package/dist/commands/progress.js +2 -1
  124. package/dist/commands/progress.js.map +1 -1
  125. package/dist/commands/project.d.ts.map +1 -1
  126. package/dist/commands/project.js +7 -6
  127. package/dist/commands/project.js.map +1 -1
  128. package/dist/commands/refresh.d.ts.map +1 -1
  129. package/dist/commands/refresh.js +8 -11
  130. package/dist/commands/refresh.js.map +1 -1
  131. package/dist/commands/remove.d.ts.map +1 -1
  132. package/dist/commands/remove.js +2 -1
  133. package/dist/commands/remove.js.map +1 -1
  134. package/dist/commands/schedule.js +16 -18
  135. package/dist/commands/schedule.js.map +1 -1
  136. package/dist/commands/start.d.ts.map +1 -1
  137. package/dist/commands/start.js +9 -8
  138. package/dist/commands/start.js.map +1 -1
  139. package/dist/commands/status.d.ts.map +1 -1
  140. package/dist/commands/status.js +2 -1
  141. package/dist/commands/status.js.map +1 -1
  142. package/dist/commands/stop.d.ts.map +1 -1
  143. package/dist/commands/stop.js +2 -1
  144. package/dist/commands/stop.js.map +1 -1
  145. package/dist/commands/strategy.d.ts.map +1 -1
  146. package/dist/commands/strategy.js +6 -5
  147. package/dist/commands/strategy.js.map +1 -1
  148. package/dist/commands/sync.js +8 -10
  149. package/dist/commands/sync.js.map +1 -1
  150. package/dist/commands/tui.d.ts.map +1 -1
  151. package/dist/commands/tui.js +11 -10
  152. package/dist/commands/tui.js.map +1 -1
  153. package/dist/commands/ui.d.ts.map +1 -1
  154. package/dist/commands/ui.js +4 -3
  155. package/dist/commands/ui.js.map +1 -1
  156. package/dist/core/adapters/local-storage.d.ts +3 -1
  157. package/dist/core/adapters/local-storage.d.ts.map +1 -1
  158. package/dist/core/adapters/local-storage.js +62 -4
  159. package/dist/core/adapters/local-storage.js.map +1 -1
  160. package/dist/core/analysis-cache.d.ts.map +1 -1
  161. package/dist/core/analysis-cache.js +8 -7
  162. package/dist/core/analysis-cache.js.map +1 -1
  163. package/dist/core/brand-config.d.ts +261 -0
  164. package/dist/core/brand-config.d.ts.map +1 -0
  165. package/dist/core/brand-config.js +514 -0
  166. package/dist/core/brand-config.js.map +1 -0
  167. package/dist/core/config.d.ts +1 -1
  168. package/dist/core/config.d.ts.map +1 -1
  169. package/dist/core/config.js +18 -8
  170. package/dist/core/config.js.map +1 -1
  171. package/dist/core/constants.d.ts +88 -0
  172. package/dist/core/constants.d.ts.map +1 -0
  173. package/dist/core/constants.js +103 -0
  174. package/dist/core/constants.js.map +1 -0
  175. package/dist/core/doctor.d.ts.map +1 -1
  176. package/dist/core/doctor.js +11 -3
  177. package/dist/core/doctor.js.map +1 -1
  178. package/dist/core/generation-lock.d.ts +2 -2
  179. package/dist/core/generation-lock.d.ts.map +1 -1
  180. package/dist/core/generation-lock.js +49 -22
  181. package/dist/core/generation-lock.js.map +1 -1
  182. package/dist/core/knowledge.d.ts +19 -0
  183. package/dist/core/knowledge.d.ts.map +1 -1
  184. package/dist/core/knowledge.js +150 -24
  185. package/dist/core/knowledge.js.map +1 -1
  186. package/dist/core/migrate.d.ts +53 -0
  187. package/dist/core/migrate.d.ts.map +1 -0
  188. package/dist/core/migrate.js +607 -0
  189. package/dist/core/migrate.js.map +1 -0
  190. package/dist/core/new-repo.js +1 -1
  191. package/dist/core/new-repo.js.map +1 -1
  192. package/dist/core/projects.d.ts.map +1 -1
  193. package/dist/core/projects.js +13 -4
  194. package/dist/core/projects.js.map +1 -1
  195. package/dist/core/refresh.d.ts.map +1 -1
  196. package/dist/core/refresh.js +6 -4
  197. package/dist/core/refresh.js.map +1 -1
  198. package/dist/core/scheduler.d.ts +1 -1
  199. package/dist/core/scheduler.d.ts.map +1 -1
  200. package/dist/core/scheduler.js +11 -9
  201. package/dist/core/scheduler.js.map +1 -1
  202. package/dist/core/sync.js +1 -1
  203. package/dist/core/sync.js.map +1 -1
  204. package/dist/core/workspace-git.d.ts.map +1 -1
  205. package/dist/core/workspace-git.js +34 -10
  206. package/dist/core/workspace-git.js.map +1 -1
  207. package/dist/core/workspace-state.d.ts +4 -0
  208. package/dist/core/workspace-state.d.ts.map +1 -1
  209. package/dist/core/workspace-state.js +8 -8
  210. package/dist/core/workspace-state.js.map +1 -1
  211. package/dist/core/workspace.d.ts +3 -4
  212. package/dist/core/workspace.d.ts.map +1 -1
  213. package/dist/core/workspace.js +62 -30
  214. package/dist/core/workspace.js.map +1 -1
  215. package/dist/desktop-server.js +6 -3
  216. package/dist/desktop-server.js.map +1 -1
  217. package/dist/generators/base.d.ts.map +1 -1
  218. package/dist/generators/base.js +7 -5
  219. package/dist/generators/base.js.map +1 -1
  220. package/dist/generators/claude.d.ts.map +1 -1
  221. package/dist/generators/claude.js +2 -1
  222. package/dist/generators/claude.js.map +1 -1
  223. package/dist/generators/cursor.d.ts +1 -1
  224. package/dist/generators/cursor.d.ts.map +1 -1
  225. package/dist/generators/cursor.js +9 -4
  226. package/dist/generators/cursor.js.map +1 -1
  227. package/dist/generators/index.d.ts +4 -8
  228. package/dist/generators/index.d.ts.map +1 -1
  229. package/dist/generators/index.js +34 -69
  230. package/dist/generators/index.js.map +1 -1
  231. package/dist/generators/plan-generator.d.ts.map +1 -1
  232. package/dist/generators/plan-generator.js +20 -11
  233. package/dist/generators/plan-generator.js.map +1 -1
  234. package/dist/generators/skills-generator.d.ts.map +1 -1
  235. package/dist/generators/skills-generator.js +26 -11
  236. package/dist/generators/skills-generator.js.map +1 -1
  237. package/dist/gui/app_logo.png +0 -0
  238. package/dist/gui/assets/AgentsPage-CnXzZFo1.js +3 -0
  239. package/dist/gui/assets/DashboardPage-rm3hKgsw.js +1 -0
  240. package/dist/gui/assets/GettingStartedPage-BKeuSHZQ.js +1 -0
  241. package/dist/gui/assets/NullStore-DuSDhGOe.js +1 -0
  242. package/dist/gui/assets/ProjectsPage-CioaA4uf.js +1 -0
  243. package/dist/gui/assets/ScaffoldRepoInline-DKZ1rPmE.js +1 -0
  244. package/dist/gui/assets/Separator-BgWyHDtg.js +1 -0
  245. package/dist/gui/assets/SettingsPage-BevO8oge.js +1 -0
  246. package/dist/gui/assets/SkillsPage-D2stJRWy.js +6 -0
  247. package/dist/gui/assets/StartWorkPage-C8a6eN90.js +1 -0
  248. package/dist/gui/assets/{StrategiesPage-C36TZgDX.js → StrategiesPage-BRQMly_d.js} +2 -2
  249. package/dist/gui/assets/WorkroomsPage-KqBkRD-x.js +4 -0
  250. package/dist/gui/assets/WorkspacesPage-DcA2i5HN.js +2 -0
  251. package/dist/gui/assets/alert-QnMXzwpU.js +1 -0
  252. package/dist/gui/assets/alert-dialog-BHEUQVyS.js +1 -0
  253. package/dist/gui/assets/badge-DTBzXOWn.js +1 -0
  254. package/dist/gui/assets/brand-hHByyyzq.js +1 -0
  255. package/dist/gui/assets/button-CREn1mup.js +1 -0
  256. package/dist/gui/assets/card-yrg6ov8B.js +1 -0
  257. package/dist/gui/assets/chevron-down-B_Mwbim8.js +1 -0
  258. package/dist/gui/assets/{circle-plus-lmU1yNJd.js → circle-plus--zM3-3r3.js} +1 -1
  259. package/dist/gui/assets/{code-xml-jFAa3-TT.js → code-xml-DH-38Tt7.js} +1 -1
  260. package/dist/gui/assets/{git-branch-Cfj9D8yW.js → git-branch-Ce0I2663.js} +1 -1
  261. package/dist/gui/assets/index-BB4BuHBQ.css +2 -0
  262. package/dist/gui/assets/index-BTe-_1Mu.js +57 -0
  263. package/dist/gui/assets/isElementDisabled-BrwmCB1d.js +1 -0
  264. package/dist/gui/assets/label-DC730X8s.js +1 -0
  265. package/dist/gui/assets/layers-j_brOuoa.js +1 -0
  266. package/dist/gui/assets/menu-d_G9km1-.js +1 -0
  267. package/dist/gui/assets/shield-check-Dd1Yv3l2.js +1 -0
  268. package/dist/gui/assets/square-check-big-BSIiYXPI.js +1 -0
  269. package/dist/gui/assets/tabs-BHwTt4t1.js +1 -0
  270. package/dist/gui/assets/{trash-2-CtR35_eF.js → trash-2-CMdv3eD3.js} +1 -1
  271. package/dist/gui/assets/useAriaLabelledBy-BmJErI5a.js +1 -0
  272. package/dist/gui/assets/useValueChanged-WS99hTmm.js +1 -0
  273. package/dist/gui/assets/{vsc-DFh0-53h.js → vsc-eMRaEwN3.js} +1 -1
  274. package/dist/gui/favicon-aurora.svg +8 -0
  275. package/dist/gui/favicon.ico +0 -0
  276. package/dist/gui/favicon.svg +8 -1
  277. package/dist/gui/index.html +21 -12
  278. package/dist/harness/claude.d.ts.map +1 -1
  279. package/dist/harness/claude.js +97 -7
  280. package/dist/harness/claude.js.map +1 -1
  281. package/dist/harness/codex.d.ts +1 -0
  282. package/dist/harness/codex.d.ts.map +1 -1
  283. package/dist/harness/codex.js +51 -34
  284. package/dist/harness/codex.js.map +1 -1
  285. package/dist/index.js +57 -27
  286. package/dist/index.js.map +1 -1
  287. package/dist/mcp/server.d.ts +12 -1
  288. package/dist/mcp/server.d.ts.map +1 -1
  289. package/dist/mcp/server.js +24 -6
  290. package/dist/mcp/server.js.map +1 -1
  291. package/dist/mcp/tools.d.ts +4 -2
  292. package/dist/mcp/tools.d.ts.map +1 -1
  293. package/dist/mcp/tools.js +372 -17
  294. package/dist/mcp/tools.js.map +1 -1
  295. package/dist/orchestration/detect.d.ts.map +1 -1
  296. package/dist/orchestration/detect.js +12 -5
  297. package/dist/orchestration/detect.js.map +1 -1
  298. package/dist/orchestration/log-tail.js +17 -17
  299. package/dist/orchestration/log-tail.js.map +1 -1
  300. package/dist/orchestration/runner.d.ts +1 -1
  301. package/dist/orchestration/runner.d.ts.map +1 -1
  302. package/dist/orchestration/runner.js +13 -8
  303. package/dist/orchestration/runner.js.map +1 -1
  304. package/dist/resources/agents-catalog.d.ts +3 -1
  305. package/dist/resources/agents-catalog.d.ts.map +1 -1
  306. package/dist/resources/agents-catalog.js +12 -6
  307. package/dist/resources/agents-catalog.js.map +1 -1
  308. package/dist/resources/contracts.d.ts +7 -0
  309. package/dist/resources/contracts.d.ts.map +1 -1
  310. package/dist/resources/contracts.js +8 -1
  311. package/dist/resources/contracts.js.map +1 -1
  312. package/dist/resources/fs-safety.d.ts +7 -1
  313. package/dist/resources/fs-safety.d.ts.map +1 -1
  314. package/dist/resources/fs-safety.js +44 -1
  315. package/dist/resources/fs-safety.js.map +1 -1
  316. package/dist/resources/materializer.d.ts +1 -0
  317. package/dist/resources/materializer.d.ts.map +1 -1
  318. package/dist/resources/materializer.js +48 -17
  319. package/dist/resources/materializer.js.map +1 -1
  320. package/dist/resources/service.d.ts.map +1 -1
  321. package/dist/resources/service.js +3 -2
  322. package/dist/resources/service.js.map +1 -1
  323. package/dist/server.d.ts +2 -19
  324. package/dist/server.d.ts.map +1 -1
  325. package/dist/server.js +1158 -318
  326. package/dist/server.js.map +1 -1
  327. package/dist/static-resources/workflows/cost-aware-sol-luna.md +95 -0
  328. package/dist/storage/db.d.ts +94 -0
  329. package/dist/storage/db.d.ts.map +1 -0
  330. package/dist/storage/db.js +609 -0
  331. package/dist/storage/db.js.map +1 -0
  332. package/dist/types.d.ts +19 -0
  333. package/dist/types.d.ts.map +1 -1
  334. package/dist/utils/detect-ai.d.ts.map +1 -1
  335. package/dist/utils/detect-ai.js +1 -3
  336. package/dist/utils/detect-ai.js.map +1 -1
  337. package/dist/utils/multi-git.d.ts +0 -8
  338. package/dist/utils/multi-git.d.ts.map +1 -1
  339. package/dist/utils/multi-git.js +12 -4
  340. package/dist/utils/multi-git.js.map +1 -1
  341. package/dist/utils/pr.d.ts.map +1 -1
  342. package/dist/utils/pr.js +10 -5
  343. package/dist/utils/pr.js.map +1 -1
  344. package/dist/utils/resolve-workspace.d.ts +1 -1
  345. package/dist/utils/resolve-workspace.d.ts.map +1 -1
  346. package/dist/utils/resolve-workspace.js +10 -3
  347. package/dist/utils/resolve-workspace.js.map +1 -1
  348. package/dist/utils/session-finder.d.ts +12 -1
  349. package/dist/utils/session-finder.d.ts.map +1 -1
  350. package/dist/utils/session-finder.js +183 -0
  351. package/dist/utils/session-finder.js.map +1 -1
  352. package/dist/utils/skills-catalog.d.ts +5 -1
  353. package/dist/utils/skills-catalog.d.ts.map +1 -1
  354. package/dist/utils/skills-catalog.js +131 -75
  355. package/dist/utils/skills-catalog.js.map +1 -1
  356. package/dist/utils/terminal-launch.d.ts +4 -0
  357. package/dist/utils/terminal-launch.d.ts.map +1 -1
  358. package/dist/utils/terminal-launch.js +71 -12
  359. package/dist/utils/terminal-launch.js.map +1 -1
  360. package/dist/utils/update-check.d.ts +4 -1
  361. package/dist/utils/update-check.d.ts.map +1 -1
  362. package/dist/utils/update-check.js +66 -19
  363. package/dist/utils/update-check.js.map +1 -1
  364. package/dist/utils/workflows.d.ts +4 -1
  365. package/dist/utils/workflows.d.ts.map +1 -1
  366. package/dist/utils/workflows.js +51 -21
  367. package/dist/utils/workflows.js.map +1 -1
  368. package/dist/workrooms/client.d.ts +54 -0
  369. package/dist/workrooms/client.d.ts.map +1 -0
  370. package/dist/workrooms/client.js +191 -0
  371. package/dist/workrooms/client.js.map +1 -0
  372. package/dist/workrooms/contracts.d.ts +579 -0
  373. package/dist/workrooms/contracts.d.ts.map +1 -0
  374. package/dist/workrooms/contracts.js +231 -0
  375. package/dist/workrooms/contracts.js.map +1 -0
  376. package/dist/workrooms/crypto.d.ts +30 -0
  377. package/dist/workrooms/crypto.d.ts.map +1 -0
  378. package/dist/workrooms/crypto.js +72 -0
  379. package/dist/workrooms/crypto.js.map +1 -0
  380. package/dist/workrooms/host.d.ts +61 -0
  381. package/dist/workrooms/host.d.ts.map +1 -0
  382. package/dist/workrooms/host.js +747 -0
  383. package/dist/workrooms/host.js.map +1 -0
  384. package/dist/workrooms/local-resources.d.ts +26 -0
  385. package/dist/workrooms/local-resources.d.ts.map +1 -0
  386. package/dist/workrooms/local-resources.js +367 -0
  387. package/dist/workrooms/local-resources.js.map +1 -0
  388. package/dist/workrooms/manager.d.ts +228 -0
  389. package/dist/workrooms/manager.d.ts.map +1 -0
  390. package/dist/workrooms/manager.js +768 -0
  391. package/dist/workrooms/manager.js.map +1 -0
  392. package/dist/workrooms/milestones.d.ts +42 -0
  393. package/dist/workrooms/milestones.d.ts.map +1 -0
  394. package/dist/workrooms/milestones.js +94 -0
  395. package/dist/workrooms/milestones.js.map +1 -0
  396. package/dist/workrooms/portable.d.ts +21 -0
  397. package/dist/workrooms/portable.d.ts.map +1 -0
  398. package/dist/workrooms/portable.js +201 -0
  399. package/dist/workrooms/portable.js.map +1 -0
  400. package/dist/workrooms/resource-package.d.ts +7 -0
  401. package/dist/workrooms/resource-package.d.ts.map +1 -0
  402. package/dist/workrooms/resource-package.js +260 -0
  403. package/dist/workrooms/resource-package.js.map +1 -0
  404. package/dist/workrooms/service.d.ts +92 -0
  405. package/dist/workrooms/service.d.ts.map +1 -0
  406. package/dist/workrooms/service.js +788 -0
  407. package/dist/workrooms/service.js.map +1 -0
  408. package/dist/workrooms/sqlite-store.d.ts +31 -0
  409. package/dist/workrooms/sqlite-store.d.ts.map +1 -0
  410. package/dist/workrooms/sqlite-store.js +511 -0
  411. package/dist/workrooms/sqlite-store.js.map +1 -0
  412. package/dist/workrooms/store.d.ts +542 -0
  413. package/dist/workrooms/store.d.ts.map +1 -0
  414. package/dist/workrooms/store.js +71 -0
  415. package/dist/workrooms/store.js.map +1 -0
  416. package/package.json +16 -10
  417. package/resources/workflows/cost-aware-sol-luna.md +95 -0
  418. package/dist/agent/AcpCliAdapter.test.d.ts +0 -2
  419. package/dist/agent/AcpCliAdapter.test.d.ts.map +0 -1
  420. package/dist/agent/AcpCliAdapter.test.js +0 -235
  421. package/dist/agent/AcpCliAdapter.test.js.map +0 -1
  422. package/dist/agent/AntigravityCliAdapter.test.d.ts +0 -2
  423. package/dist/agent/AntigravityCliAdapter.test.d.ts.map +0 -1
  424. package/dist/agent/AntigravityCliAdapter.test.js +0 -222
  425. package/dist/agent/AntigravityCliAdapter.test.js.map +0 -1
  426. package/dist/agent/ClaudeCliAdapter.test.d.ts +0 -2
  427. package/dist/agent/ClaudeCliAdapter.test.d.ts.map +0 -1
  428. package/dist/agent/ClaudeCliAdapter.test.js +0 -320
  429. package/dist/agent/ClaudeCliAdapter.test.js.map +0 -1
  430. package/dist/agent/ClaudeSdkAdapter.test.d.ts +0 -2
  431. package/dist/agent/ClaudeSdkAdapter.test.d.ts.map +0 -1
  432. package/dist/agent/ClaudeSdkAdapter.test.js +0 -155
  433. package/dist/agent/ClaudeSdkAdapter.test.js.map +0 -1
  434. package/dist/agent/CliAdapterBase.test.d.ts +0 -2
  435. package/dist/agent/CliAdapterBase.test.d.ts.map +0 -1
  436. package/dist/agent/CliAdapterBase.test.js +0 -240
  437. package/dist/agent/CliAdapterBase.test.js.map +0 -1
  438. package/dist/agent/CodexCliAdapter.test.d.ts +0 -2
  439. package/dist/agent/CodexCliAdapter.test.d.ts.map +0 -1
  440. package/dist/agent/CodexCliAdapter.test.js +0 -331
  441. package/dist/agent/CodexCliAdapter.test.js.map +0 -1
  442. package/dist/agent/CodexSdkAdapter.test.d.ts +0 -2
  443. package/dist/agent/CodexSdkAdapter.test.d.ts.map +0 -1
  444. package/dist/agent/CodexSdkAdapter.test.js +0 -76
  445. package/dist/agent/CodexSdkAdapter.test.js.map +0 -1
  446. package/dist/agent/ProviderRegistry.test.d.ts +0 -2
  447. package/dist/agent/ProviderRegistry.test.d.ts.map +0 -1
  448. package/dist/agent/ProviderRegistry.test.js +0 -129
  449. package/dist/agent/ProviderRegistry.test.js.map +0 -1
  450. package/dist/agent/adapters.test.d.ts +0 -2
  451. package/dist/agent/adapters.test.d.ts.map +0 -1
  452. package/dist/agent/adapters.test.js +0 -71
  453. package/dist/agent/adapters.test.js.map +0 -1
  454. package/dist/agent/cliAvailability.test.d.ts +0 -2
  455. package/dist/agent/cliAvailability.test.d.ts.map +0 -1
  456. package/dist/agent/cliAvailability.test.js +0 -217
  457. package/dist/agent/cliAvailability.test.js.map +0 -1
  458. package/dist/agent/mcp-config.test.d.ts +0 -2
  459. package/dist/agent/mcp-config.test.d.ts.map +0 -1
  460. package/dist/agent/mcp-config.test.js +0 -22
  461. package/dist/agent/mcp-config.test.js.map +0 -1
  462. package/dist/agent/models.test.d.ts +0 -2
  463. package/dist/agent/models.test.d.ts.map +0 -1
  464. package/dist/agent/models.test.js +0 -40
  465. package/dist/agent/models.test.js.map +0 -1
  466. package/dist/agent/nativeAgent.test.d.ts +0 -2
  467. package/dist/agent/nativeAgent.test.d.ts.map +0 -1
  468. package/dist/agent/nativeAgent.test.js +0 -35
  469. package/dist/agent/nativeAgent.test.js.map +0 -1
  470. package/dist/agent/nativeTools.test.d.ts +0 -2
  471. package/dist/agent/nativeTools.test.d.ts.map +0 -1
  472. package/dist/agent/nativeTools.test.js +0 -67
  473. package/dist/agent/nativeTools.test.js.map +0 -1
  474. package/dist/agent/orchestrator.test.d.ts +0 -2
  475. package/dist/agent/orchestrator.test.d.ts.map +0 -1
  476. package/dist/agent/orchestrator.test.js +0 -284
  477. package/dist/agent/orchestrator.test.js.map +0 -1
  478. package/dist/agent/session.test.d.ts +0 -2
  479. package/dist/agent/session.test.d.ts.map +0 -1
  480. package/dist/agent/session.test.js +0 -74
  481. package/dist/agent/session.test.js.map +0 -1
  482. package/dist/agent/smoke-chat.test.d.ts +0 -2
  483. package/dist/agent/smoke-chat.test.d.ts.map +0 -1
  484. package/dist/agent/smoke-chat.test.js +0 -166
  485. package/dist/agent/smoke-chat.test.js.map +0 -1
  486. package/dist/commands/add-repo.test.d.ts +0 -2
  487. package/dist/commands/add-repo.test.d.ts.map +0 -1
  488. package/dist/commands/add-repo.test.js +0 -30
  489. package/dist/commands/add-repo.test.js.map +0 -1
  490. package/dist/commands/commands.test.d.ts +0 -2
  491. package/dist/commands/commands.test.d.ts.map +0 -1
  492. package/dist/commands/commands.test.js +0 -156
  493. package/dist/commands/commands.test.js.map +0 -1
  494. package/dist/commands/commit.test.d.ts +0 -2
  495. package/dist/commands/commit.test.d.ts.map +0 -1
  496. package/dist/commands/commit.test.js +0 -54
  497. package/dist/commands/commit.test.js.map +0 -1
  498. package/dist/commands/isolate.test.d.ts +0 -2
  499. package/dist/commands/isolate.test.d.ts.map +0 -1
  500. package/dist/commands/isolate.test.js +0 -59
  501. package/dist/commands/isolate.test.js.map +0 -1
  502. package/dist/commands/knowledge.test.d.ts +0 -2
  503. package/dist/commands/knowledge.test.d.ts.map +0 -1
  504. package/dist/commands/knowledge.test.js +0 -90
  505. package/dist/commands/knowledge.test.js.map +0 -1
  506. package/dist/commands/status.test.d.ts +0 -2
  507. package/dist/commands/status.test.d.ts.map +0 -1
  508. package/dist/commands/status.test.js +0 -31
  509. package/dist/commands/status.test.js.map +0 -1
  510. package/dist/commands/ui.test.d.ts +0 -2
  511. package/dist/commands/ui.test.d.ts.map +0 -1
  512. package/dist/commands/ui.test.js +0 -58
  513. package/dist/commands/ui.test.js.map +0 -1
  514. package/dist/core/adapters/storage.test.d.ts +0 -2
  515. package/dist/core/adapters/storage.test.d.ts.map +0 -1
  516. package/dist/core/adapters/storage.test.js +0 -65
  517. package/dist/core/adapters/storage.test.js.map +0 -1
  518. package/dist/core/analysis-cache.test.d.ts +0 -2
  519. package/dist/core/analysis-cache.test.d.ts.map +0 -1
  520. package/dist/core/analysis-cache.test.js +0 -163
  521. package/dist/core/analysis-cache.test.js.map +0 -1
  522. package/dist/core/config.test.d.ts +0 -2
  523. package/dist/core/config.test.d.ts.map +0 -1
  524. package/dist/core/config.test.js +0 -90
  525. package/dist/core/config.test.js.map +0 -1
  526. package/dist/core/finish.test.d.ts +0 -2
  527. package/dist/core/finish.test.d.ts.map +0 -1
  528. package/dist/core/finish.test.js +0 -110
  529. package/dist/core/finish.test.js.map +0 -1
  530. package/dist/core/generation-lock.test.d.ts +0 -2
  531. package/dist/core/generation-lock.test.d.ts.map +0 -1
  532. package/dist/core/generation-lock.test.js +0 -134
  533. package/dist/core/generation-lock.test.js.map +0 -1
  534. package/dist/core/isolate.test.d.ts +0 -2
  535. package/dist/core/isolate.test.d.ts.map +0 -1
  536. package/dist/core/isolate.test.js +0 -320
  537. package/dist/core/isolate.test.js.map +0 -1
  538. package/dist/core/knowledge.test.d.ts +0 -2
  539. package/dist/core/knowledge.test.d.ts.map +0 -1
  540. package/dist/core/knowledge.test.js +0 -287
  541. package/dist/core/knowledge.test.js.map +0 -1
  542. package/dist/core/locks.test.d.ts +0 -2
  543. package/dist/core/locks.test.d.ts.map +0 -1
  544. package/dist/core/locks.test.js +0 -143
  545. package/dist/core/locks.test.js.map +0 -1
  546. package/dist/core/new-repo.test.d.ts +0 -2
  547. package/dist/core/new-repo.test.d.ts.map +0 -1
  548. package/dist/core/new-repo.test.js +0 -78
  549. package/dist/core/new-repo.test.js.map +0 -1
  550. package/dist/core/progress.test.d.ts +0 -2
  551. package/dist/core/progress.test.d.ts.map +0 -1
  552. package/dist/core/progress.test.js +0 -52
  553. package/dist/core/progress.test.js.map +0 -1
  554. package/dist/core/projects.test.d.ts +0 -2
  555. package/dist/core/projects.test.d.ts.map +0 -1
  556. package/dist/core/projects.test.js +0 -165
  557. package/dist/core/projects.test.js.map +0 -1
  558. package/dist/core/refresh.test.d.ts +0 -2
  559. package/dist/core/refresh.test.d.ts.map +0 -1
  560. package/dist/core/refresh.test.js +0 -93
  561. package/dist/core/refresh.test.js.map +0 -1
  562. package/dist/core/scanner.test.d.ts +0 -2
  563. package/dist/core/scanner.test.d.ts.map +0 -1
  564. package/dist/core/scanner.test.js +0 -53
  565. package/dist/core/scanner.test.js.map +0 -1
  566. package/dist/core/scheduler.test.d.ts +0 -2
  567. package/dist/core/scheduler.test.d.ts.map +0 -1
  568. package/dist/core/scheduler.test.js +0 -169
  569. package/dist/core/scheduler.test.js.map +0 -1
  570. package/dist/core/sync.test.d.ts +0 -2
  571. package/dist/core/sync.test.d.ts.map +0 -1
  572. package/dist/core/sync.test.js +0 -128
  573. package/dist/core/sync.test.js.map +0 -1
  574. package/dist/core/workspace-git.test.d.ts +0 -2
  575. package/dist/core/workspace-git.test.d.ts.map +0 -1
  576. package/dist/core/workspace-git.test.js +0 -52
  577. package/dist/core/workspace-git.test.js.map +0 -1
  578. package/dist/core/workspace-skills-workflow.integration.test.d.ts +0 -2
  579. package/dist/core/workspace-skills-workflow.integration.test.d.ts.map +0 -1
  580. package/dist/core/workspace-skills-workflow.integration.test.js +0 -177
  581. package/dist/core/workspace-skills-workflow.integration.test.js.map +0 -1
  582. package/dist/core/workspace-state.test.d.ts +0 -2
  583. package/dist/core/workspace-state.test.d.ts.map +0 -1
  584. package/dist/core/workspace-state.test.js +0 -84
  585. package/dist/core/workspace-state.test.js.map +0 -1
  586. package/dist/core/workspace.create.test.d.ts +0 -2
  587. package/dist/core/workspace.create.test.d.ts.map +0 -1
  588. package/dist/core/workspace.create.test.js +0 -224
  589. package/dist/core/workspace.create.test.js.map +0 -1
  590. package/dist/core/workspace.test.d.ts +0 -2
  591. package/dist/core/workspace.test.d.ts.map +0 -1
  592. package/dist/core/workspace.test.js +0 -53
  593. package/dist/core/workspace.test.js.map +0 -1
  594. package/dist/core/worktree.test.d.ts +0 -2
  595. package/dist/core/worktree.test.d.ts.map +0 -1
  596. package/dist/core/worktree.test.js +0 -153
  597. package/dist/core/worktree.test.js.map +0 -1
  598. package/dist/generators/base.test.d.ts +0 -2
  599. package/dist/generators/base.test.d.ts.map +0 -1
  600. package/dist/generators/base.test.js +0 -287
  601. package/dist/generators/base.test.js.map +0 -1
  602. package/dist/generators/knowledge-migration.test.d.ts +0 -2
  603. package/dist/generators/knowledge-migration.test.d.ts.map +0 -1
  604. package/dist/generators/knowledge-migration.test.js +0 -22
  605. package/dist/generators/knowledge-migration.test.js.map +0 -1
  606. package/dist/generators/plan-generator.test.d.ts +0 -2
  607. package/dist/generators/plan-generator.test.d.ts.map +0 -1
  608. package/dist/generators/plan-generator.test.js +0 -220
  609. package/dist/generators/plan-generator.test.js.map +0 -1
  610. package/dist/generators/skills-generator.test.d.ts +0 -2
  611. package/dist/generators/skills-generator.test.d.ts.map +0 -1
  612. package/dist/generators/skills-generator.test.js +0 -79
  613. package/dist/generators/skills-generator.test.js.map +0 -1
  614. package/dist/gui/assets/AgentsPage-B4fx43rd.js +0 -3
  615. package/dist/gui/assets/CSPContext-DBUu9tE0.js +0 -1
  616. package/dist/gui/assets/DashboardPage-DH52G8IG.js +0 -1
  617. package/dist/gui/assets/GettingStartedPage-BlTrQhvi.js +0 -1
  618. package/dist/gui/assets/ProjectsPage-BPVSLPXv.js +0 -1
  619. package/dist/gui/assets/ScaffoldRepoInline-ywcpq9PX.js +0 -1
  620. package/dist/gui/assets/SettingsPage-RQr8SalV.js +0 -1
  621. package/dist/gui/assets/SkillsPage-CYoSRrwx.js +0 -6
  622. package/dist/gui/assets/StartWorkPage-DBKK4lFi.js +0 -1
  623. package/dist/gui/assets/WorkspacesPage-DxEONvSq.js +0 -6
  624. package/dist/gui/assets/alert-dialog-CQAX2yRx.js +0 -1
  625. package/dist/gui/assets/badge-CAohJGj6.js +0 -1
  626. package/dist/gui/assets/bot-C2HWrAft.js +0 -1
  627. package/dist/gui/assets/button-BDDZnIHx.js +0 -1
  628. package/dist/gui/assets/card-BKXzd_5L.js +0 -1
  629. package/dist/gui/assets/chevron-down-qTL8HPug.js +0 -1
  630. package/dist/gui/assets/cpu-Cdwd84Jz.js +0 -1
  631. package/dist/gui/assets/empty-BdlgbifD.js +0 -1
  632. package/dist/gui/assets/index-BF6pre6G.css +0 -2
  633. package/dist/gui/assets/index-C_rwnBzF.js +0 -52
  634. package/dist/gui/assets/isElementDisabled-uy_EOitH.js +0 -1
  635. package/dist/gui/assets/label-CG5exjag.js +0 -1
  636. package/dist/gui/assets/square-DlIYnlSq.js +0 -1
  637. package/dist/gui/assets/textarea-ByfoCb54.js +0 -1
  638. package/dist/gui/assets/useAriaLabelledBy-ClesgjMe.js +0 -1
  639. package/dist/gui/assets/useValueChanged-DeYvGnKs.js +0 -1
  640. package/dist/gui/assets/zap-CWG_Lvki.js +0 -1
  641. package/dist/harness/contract.test.d.ts +0 -2
  642. package/dist/harness/contract.test.d.ts.map +0 -1
  643. package/dist/harness/contract.test.js +0 -426
  644. package/dist/harness/contract.test.js.map +0 -1
  645. package/dist/harness/harness.test.d.ts +0 -2
  646. package/dist/harness/harness.test.d.ts.map +0 -1
  647. package/dist/harness/harness.test.js +0 -215
  648. package/dist/harness/harness.test.js.map +0 -1
  649. package/dist/mcp/tools.test.d.ts +0 -2
  650. package/dist/mcp/tools.test.d.ts.map +0 -1
  651. package/dist/mcp/tools.test.js +0 -194
  652. package/dist/mcp/tools.test.js.map +0 -1
  653. package/dist/orchestration/log-tail.test.d.ts +0 -2
  654. package/dist/orchestration/log-tail.test.d.ts.map +0 -1
  655. package/dist/orchestration/log-tail.test.js +0 -72
  656. package/dist/orchestration/log-tail.test.js.map +0 -1
  657. package/dist/orchestration/orchestrator.test.d.ts +0 -2
  658. package/dist/orchestration/orchestrator.test.d.ts.map +0 -1
  659. package/dist/orchestration/orchestrator.test.js +0 -85
  660. package/dist/orchestration/orchestrator.test.js.map +0 -1
  661. package/dist/orchestration/runner.test.d.ts +0 -2
  662. package/dist/orchestration/runner.test.d.ts.map +0 -1
  663. package/dist/orchestration/runner.test.js +0 -233
  664. package/dist/orchestration/runner.test.js.map +0 -1
  665. package/dist/resources/agents-catalog.test.d.ts +0 -2
  666. package/dist/resources/agents-catalog.test.d.ts.map +0 -1
  667. package/dist/resources/agents-catalog.test.js +0 -50
  668. package/dist/resources/agents-catalog.test.js.map +0 -1
  669. package/dist/resources/codex-agent.test.d.ts +0 -2
  670. package/dist/resources/codex-agent.test.d.ts.map +0 -1
  671. package/dist/resources/codex-agent.test.js +0 -27
  672. package/dist/resources/codex-agent.test.js.map +0 -1
  673. package/dist/resources/materializer.test.d.ts +0 -2
  674. package/dist/resources/materializer.test.d.ts.map +0 -1
  675. package/dist/resources/materializer.test.js +0 -149
  676. package/dist/resources/materializer.test.js.map +0 -1
  677. package/dist/resources/service.test.d.ts +0 -2
  678. package/dist/resources/service.test.d.ts.map +0 -1
  679. package/dist/resources/service.test.js +0 -58
  680. package/dist/resources/service.test.js.map +0 -1
  681. package/dist/server.test.d.ts +0 -2
  682. package/dist/server.test.d.ts.map +0 -1
  683. package/dist/server.test.js +0 -1697
  684. package/dist/server.test.js.map +0 -1
  685. package/dist/utils/detect-ai.test.d.ts +0 -2
  686. package/dist/utils/detect-ai.test.d.ts.map +0 -1
  687. package/dist/utils/detect-ai.test.js +0 -70
  688. package/dist/utils/detect-ai.test.js.map +0 -1
  689. package/dist/utils/detect-editors.test.d.ts +0 -2
  690. package/dist/utils/detect-editors.test.d.ts.map +0 -1
  691. package/dist/utils/detect-editors.test.js +0 -85
  692. package/dist/utils/detect-editors.test.js.map +0 -1
  693. package/dist/utils/git.test.d.ts +0 -2
  694. package/dist/utils/git.test.d.ts.map +0 -1
  695. package/dist/utils/git.test.js +0 -125
  696. package/dist/utils/git.test.js.map +0 -1
  697. package/dist/utils/multi-git.test.d.ts +0 -2
  698. package/dist/utils/multi-git.test.d.ts.map +0 -1
  699. package/dist/utils/multi-git.test.js +0 -330
  700. package/dist/utils/multi-git.test.js.map +0 -1
  701. package/dist/utils/pr.test.d.ts +0 -2
  702. package/dist/utils/pr.test.d.ts.map +0 -1
  703. package/dist/utils/pr.test.js +0 -47
  704. package/dist/utils/pr.test.js.map +0 -1
  705. package/dist/utils/session-finder.test.d.ts +0 -2
  706. package/dist/utils/session-finder.test.d.ts.map +0 -1
  707. package/dist/utils/session-finder.test.js +0 -304
  708. package/dist/utils/session-finder.test.js.map +0 -1
  709. package/dist/utils/skills-catalog.test.d.ts +0 -2
  710. package/dist/utils/skills-catalog.test.d.ts.map +0 -1
  711. package/dist/utils/skills-catalog.test.js +0 -281
  712. package/dist/utils/skills-catalog.test.js.map +0 -1
  713. package/dist/utils/system-scanner.test.d.ts +0 -2
  714. package/dist/utils/system-scanner.test.d.ts.map +0 -1
  715. package/dist/utils/system-scanner.test.js +0 -73
  716. package/dist/utils/system-scanner.test.js.map +0 -1
  717. package/dist/utils/terminal-launch.test.d.ts +0 -2
  718. package/dist/utils/terminal-launch.test.d.ts.map +0 -1
  719. package/dist/utils/terminal-launch.test.js +0 -117
  720. package/dist/utils/terminal-launch.test.js.map +0 -1
  721. package/dist/utils/update-check.test.d.ts +0 -2
  722. package/dist/utils/update-check.test.d.ts.map +0 -1
  723. package/dist/utils/update-check.test.js +0 -30
  724. package/dist/utils/update-check.test.js.map +0 -1
  725. package/dist/utils/workflows.test.d.ts +0 -2
  726. package/dist/utils/workflows.test.d.ts.map +0 -1
  727. package/dist/utils/workflows.test.js +0 -86
  728. package/dist/utils/workflows.test.js.map +0 -1
  729. package/dist/utils/workspace-launch.test.d.ts +0 -2
  730. package/dist/utils/workspace-launch.test.d.ts.map +0 -1
  731. package/dist/utils/workspace-launch.test.js +0 -128
  732. package/dist/utils/workspace-launch.test.js.map +0 -1
package/dist/server.js CHANGED
@@ -6,19 +6,19 @@ import { Hono } from 'hono';
6
6
  import { serve } from '@hono/node-server';
7
7
  import { serveStatic } from '@hono/node-server/serve-static';
8
8
  import { cors } from 'hono/cors';
9
+ import { deleteCookie, getCookie, setCookie } from 'hono/cookie';
9
10
  import { streamSSE } from 'hono/streaming';
10
11
  import { createNodeWebSocket } from '@hono/node-ws';
11
12
  import * as fs from 'node:fs/promises';
12
- import { createWriteStream, existsSync } from 'node:fs';
13
+ import { existsSync } from 'node:fs';
13
14
  import * as path from 'node:path';
14
15
  import { fileURLToPath } from 'node:url';
15
16
  import { execa } from 'execa';
16
17
  import * as os from 'node:os';
17
18
  import { createHash } from 'node:crypto';
18
- import { Readable } from 'node:stream';
19
- import { pipeline } from 'node:stream/promises';
20
- import { spawn } from 'node:child_process';
21
19
  import { loadConfig, saveConfig, getConfigDir } from './core/config.js';
20
+ import { saveChatThread, loadChatThread, clearChatThread } from './storage/db.js';
21
+ import { PRIMARY_LOGS_DIR, LEGACY_LOGS_DIR, WORKROOM_BOOTSTRAP_HEADERS, WORKROOM_BOOTSTRAP_COOKIES, ENGINE_ID, LEGACY_ENGINE_ID, ENGINE_NPM_PACKAGE, LEGACY_ENGINE_NPM_PACKAGE, resolveWorkspaceChatLedger, readWorkspaceChatMessages, } from './core/constants.js';
22
22
  import { configPatchSchema } from './core/config-schema.js';
23
23
  import { listStorageProviders } from './core/adapters/registry.js';
24
24
  import { scanForRepos } from './core/scanner.js';
@@ -35,9 +35,10 @@ import { detectEditors } from './utils/detect-editors.js';
35
35
  import { buildWorkspaceLaunchPrompt, detectWorkspaceLaunchTargets, launchTargetIdForEditorCommand, launchWorkspaceTarget, } from './utils/workspace-launch.js';
36
36
  import { isBinaryOnPath, launchWorkspaceTerminal, SUPPORTED_ASSISTANTS } from './utils/terminal-launch.js';
37
37
  import { openInEditor } from './utils/open-editor.js';
38
- import { canOpenCodexSessionInWorkspace, canTransferClaudeSessionInWorkspace, findSessions, getSessionTranscript, } from './utils/session-finder.js';
38
+ import { canOpenCodexSessionInWorkspace, canTransferClaudeSessionInWorkspace, findActiveAssistants, findSessions, getSessionTranscript, } from './utils/session-finder.js';
39
39
  import { ProviderRegistry } from './agent/adapters.js';
40
40
  import { isValidSessionUuid } from './agent/session.js';
41
+ import { defaultTurnSessionManager, AgentTurnGate, dispatchAgentInput } from './agent/TurnSessionManager.js';
41
42
  import { getRepoStatus } from './utils/multi-git.js';
42
43
  import { syncWorkspace } from './core/sync.js';
43
44
  import { commitWorkspace } from './core/commit.js';
@@ -54,6 +55,12 @@ import { deleteAgent, getAllAgents, importAgentToml, saveAgent, } from './resour
54
55
  import { ResourceConflictError } from './resources/materializer.js';
55
56
  import { ResourceSelectionError, validateResourceSelections, withResourceAdministrationLock, } from './resources/service.js';
56
57
  import { suggestWorkflow } from './utils/workflow-advisor.js';
58
+ import { WorkroomAuthorizationError, WorkroomRevisionError, WorkroomValidationError, documentNameSchema, workflowStepSchema, } from './workrooms/contracts.js';
59
+ import { listWorkroomNetworkInterfaces } from './workrooms/host.js';
60
+ import * as workroomManagerModule from './workrooms/manager.js';
61
+ const { workroomManager } = workroomManagerModule;
62
+ import { buildPortableWorkroomPreview, digestPortableWorkroomContext } from './workrooms/portable.js';
63
+ import { randomToken, tokenDigest } from './workrooms/crypto.js';
57
64
  // Resolve static files directory
58
65
  const __filename = fileURLToPath(import.meta.url);
59
66
  const __dirname = path.dirname(__filename);
@@ -73,34 +80,7 @@ function hasTrustedLocalOrigin(origin) {
73
80
  return false;
74
81
  }
75
82
  }
76
- /** Validate and dispatch one renderer turn without letting malformed authority reach a harness. */
77
- export function dispatchAgentInput(agent, provider, payload) {
78
- if (typeof payload.input !== 'string' || !payload.input.trim()) {
79
- return 'A non-empty input message is required.';
80
- }
81
- const executionProfile = ProviderRegistry.resolveExecutionProfile(provider, payload.executionProfile);
82
- if (executionProfile === null) {
83
- return 'Select a supported execution profile before sending this turn.';
84
- }
85
- void agent.send(payload.input, executionProfile);
86
- return null;
87
- }
88
- /** Per-socket admission guard so a second prompt is rejected, never dropped. */
89
- export class AgentTurnGate {
90
- active = false;
91
- tryBegin() {
92
- if (this.active)
93
- return false;
94
- this.active = true;
95
- return true;
96
- }
97
- settle() {
98
- this.active = false;
99
- }
100
- isActive() {
101
- return this.active;
102
- }
103
- }
83
+ export { dispatchAgentInput, AgentTurnGate };
104
84
  app.get('/ws', async (c, next) => {
105
85
  // Prevent Cross-Site WebSocket Hijacking (CSWSH)
106
86
  const origin = c.req.header('origin');
@@ -123,11 +103,11 @@ app.get('/ws', async (c, next) => {
123
103
  // | {type:'rejected', reason:'busy', message, turnId?}
124
104
  // | {type:'close', code} | {type:'pong'}
125
105
  return upgradeWebSocket((c) => {
126
- let agent = null;
127
- let activeProvider = null;
128
- const turnGate = new AgentTurnGate();
106
+ let currentCwd = null;
107
+ let currentWs = null;
129
108
  return {
130
109
  async onMessage(event, ws) {
110
+ currentWs = ws;
131
111
  if (typeof event.data === 'string') {
132
112
  if (event.data === 'ping') {
133
113
  ws.send(JSON.stringify({ type: 'pong' }));
@@ -139,35 +119,24 @@ app.get('/ws', async (c, next) => {
139
119
  ws.send(JSON.stringify({ type: 'pong' }));
140
120
  }
141
121
  else if (payload.type === 'start') {
142
- turnGate.settle();
143
- if (agent) {
144
- const previousAgent = agent;
145
- agent = null;
146
- activeProvider = null;
147
- previousAgent.stop();
122
+ const command = payload.command;
123
+ const cwd = payload.cwd;
124
+ if (typeof cwd !== 'string' || !cwd.trim()) {
125
+ ws.send(JSON.stringify({ type: 'error', message: 'Workspace directory cwd is required.' }));
126
+ return;
148
127
  }
149
- const provider = ProviderRegistry.getProvider(payload.command);
128
+ const provider = ProviderRegistry.getProvider(command);
150
129
  if (!provider) {
151
- ws.send(JSON.stringify({ type: 'error', message: `No provider found for ${payload.command}. Please create a dedicated adapter.` }));
130
+ ws.send(JSON.stringify({ type: 'error', message: `No provider found for ${command}. Please create a dedicated adapter.` }));
152
131
  return;
153
132
  }
154
133
  const config = await loadConfig();
155
- if (typeof payload.cwd !== 'string' || !payload.cwd.trim()) {
156
- ws.send(JSON.stringify({ type: 'error', message: 'Workspace directory cwd is required.' }));
157
- return;
158
- }
159
- const safeCwd = await resolveExactLaunchWorkspace(config.workspacesDir, payload.cwd);
134
+ const safeCwd = await resolveExactLaunchWorkspace(config.workspacesDir, cwd);
160
135
  if (!safeCwd) {
161
136
  ws.send(JSON.stringify({ type: 'error', message: 'Invalid or uncontained workspace directory.' }));
162
137
  return;
163
138
  }
164
- const freshness = await checkGenerationLock(safeCwd, { markDocuments: true });
165
- if (!freshness.fresh) {
166
- ws.send(JSON.stringify({
167
- type: 'system',
168
- message: `⚠ NexusFlow generated context is stale or drifted. ${freshness.drift.map((item) => item.message).join(' ')}`,
169
- }));
170
- }
139
+ currentCwd = safeCwd;
171
140
  let session;
172
141
  const model = ProviderRegistry.resolveModel(provider, payload.model);
173
142
  if (model === null) {
@@ -177,120 +146,95 @@ app.get('/ws', async (c, next) => {
177
146
  }));
178
147
  return;
179
148
  }
149
+ const effort = typeof payload.effort === 'string' && payload.effort.trim() ? payload.effort.trim() : undefined;
180
150
  if (payload.sessionId !== undefined && payload.sessionId !== null) {
181
151
  if (!isValidSessionUuid(payload.sessionId)) {
182
152
  ws.send(JSON.stringify({ type: 'error', message: 'Invalid session id.' }));
183
153
  return;
184
154
  }
185
- session = { id: payload.sessionId, resume: Boolean(payload.resume), model };
155
+ session = { id: payload.sessionId, resume: Boolean(payload.resume), model, effort };
186
156
  }
187
- else if (model) {
188
- session = { id: crypto.randomUUID(), resume: false, model };
157
+ else if (model || effort) {
158
+ session = { id: crypto.randomUUID(), resume: false, model, effort };
189
159
  }
190
- const startedAgent = provider.createInstance();
191
- agent = startedAgent;
192
- activeProvider = provider;
193
- const isCurrentAgent = () => agent === startedAgent;
194
- startedAgent.on('data', (text) => {
195
- if (!isCurrentAgent())
196
- return;
197
- ws.send(JSON.stringify({ type: 'stream', text }));
160
+ await defaultTurnSessionManager.startSession({
161
+ workspaceCwd: safeCwd,
162
+ command,
163
+ client: ws,
164
+ provider,
165
+ session,
198
166
  });
199
- startedAgent.on('system', (message) => {
200
- if (!isCurrentAgent())
201
- return;
202
- ws.send(JSON.stringify({ type: 'system', message }));
203
- });
204
- startedAgent.on('session', (id) => {
205
- if (!isCurrentAgent())
206
- return;
207
- // Provider output is still boundary input. Only inert UUIDs
208
- // may be persisted by the renderer and sent back in argv.
209
- if (isValidSessionUuid(id)) {
210
- ws.send(JSON.stringify({ type: 'session', id }));
211
- }
212
- });
213
- startedAgent.on('idle', () => {
214
- if (!isCurrentAgent())
215
- return;
216
- turnGate.settle();
217
- ws.send(JSON.stringify({ type: 'status', state: 'idle' }));
218
- });
219
- startedAgent.on('close', (code) => {
220
- if (!isCurrentAgent())
221
- return;
222
- turnGate.settle();
223
- ws.send(JSON.stringify({ type: 'close', code }));
224
- });
225
- startedAgent.on('usage', (usage) => {
226
- if (!isCurrentAgent())
227
- return;
228
- ws.send(JSON.stringify({ type: 'usage', usage }));
229
- });
230
- startedAgent.on('error', (error) => {
231
- if (!isCurrentAgent())
232
- return;
233
- ws.send(JSON.stringify({ type: 'error', message: error?.message ?? String(error) }));
234
- });
235
- startedAgent.start(safeCwd, session);
236
167
  }
237
168
  else if (payload.type === 'input') {
238
- if (agent && activeProvider) {
239
- if (!turnGate.tryBegin()) {
240
- const turnId = isValidSessionUuid(payload.turnId) ? payload.turnId : undefined;
241
- ws.send(JSON.stringify({
242
- type: 'rejected',
243
- reason: 'busy',
244
- message: 'The agent is still processing the current turn.',
245
- ...(turnId ? { turnId } : {}),
246
- }));
247
- return;
248
- }
249
- const error = dispatchAgentInput(agent, activeProvider, payload);
250
- if (error) {
251
- turnGate.settle();
252
- ws.send(JSON.stringify({ type: 'error', message: error }));
169
+ const cwd = typeof payload.cwd === 'string' ? payload.cwd : currentCwd;
170
+ if (!cwd) {
171
+ ws.send(JSON.stringify({
172
+ type: 'error',
173
+ message: 'No active agent session. Please start or reconnect the agent.',
174
+ }));
175
+ return;
176
+ }
177
+ const config = await loadConfig();
178
+ const safeCwd = await resolveExactLaunchWorkspace(config.workspacesDir, cwd);
179
+ if (!safeCwd) {
180
+ ws.send(JSON.stringify({ type: 'error', message: 'Invalid workspace directory.' }));
181
+ return;
182
+ }
183
+ currentCwd = safeCwd;
184
+ const existingSession = defaultTurnSessionManager.getSession(safeCwd);
185
+ if (!existingSession) {
186
+ const command = typeof payload.command === 'string' ? payload.command : 'antigravity-cli';
187
+ const provider = ProviderRegistry.getProvider(command);
188
+ if (!provider) {
189
+ ws.send(JSON.stringify({ type: 'error', message: `No provider found for ${command}.` }));
253
190
  return;
254
191
  }
255
- // Some harness validation failures emit error + idle
256
- // synchronously from send(); do not overwrite that settled
257
- // state with a late busy frame.
258
- if (turnGate.isActive()) {
259
- const turnId = isValidSessionUuid(payload.turnId) ? payload.turnId : undefined;
260
- ws.send(JSON.stringify({
261
- type: 'accepted',
262
- ...(turnId ? { turnId } : {}),
263
- }));
264
- ws.send(JSON.stringify({ type: 'status', state: 'busy' }));
265
- }
192
+ await defaultTurnSessionManager.startSession({
193
+ workspaceCwd: safeCwd,
194
+ command,
195
+ client: ws,
196
+ provider,
197
+ });
198
+ }
199
+ const result = defaultTurnSessionManager.dispatchInput(safeCwd, payload);
200
+ if (result.error) {
201
+ ws.send(JSON.stringify({ type: 'error', message: result.error }));
202
+ }
203
+ else if (result.rejected) {
204
+ const turnId = isValidSessionUuid(payload.turnId) ? payload.turnId : undefined;
205
+ ws.send(JSON.stringify({
206
+ type: 'rejected',
207
+ reason: 'busy',
208
+ message: 'The agent is still processing the current turn.',
209
+ ...(turnId ? { turnId } : {}),
210
+ }));
211
+ }
212
+ }
213
+ else if (payload.type === 'approval_response') {
214
+ if (currentCwd) {
215
+ const requestId = typeof payload.requestId === 'string' ? payload.requestId : '';
216
+ const decision = payload.decision === 'allow' ? 'allow' : 'deny';
217
+ const message = typeof payload.message === 'string' ? payload.message : undefined;
218
+ defaultTurnSessionManager.respondToApproval(currentCwd, requestId, decision, message);
266
219
  }
267
220
  }
268
221
  else if (payload.type === 'stop') {
269
- turnGate.settle();
270
- if (agent) {
271
- const stoppedAgent = agent;
272
- agent = null;
273
- activeProvider = null;
274
- stoppedAgent.stop();
222
+ if (currentCwd) {
223
+ defaultTurnSessionManager.stopSession(currentCwd);
275
224
  }
276
225
  }
277
226
  }
278
227
  catch (err) {
279
- console.error('[WS] Failed to parse message', err);
228
+ console.error('Error handling WebSocket message:', err);
280
229
  }
281
230
  }
282
231
  },
283
- onOpen(_event, _ws) {
284
- console.log('[WS] Client connected');
232
+ onOpen(_event, ws) {
233
+ currentWs = ws;
285
234
  },
286
- onClose(_event, _ws) {
287
- console.log('[WS] Client disconnected');
288
- turnGate.settle();
289
- if (agent) {
290
- const disconnectedAgent = agent;
291
- agent = null;
292
- activeProvider = null;
293
- disconnectedAgent.stop();
235
+ onClose(_event, ws) {
236
+ if (currentCwd && ws) {
237
+ defaultTurnSessionManager.unregisterClient(currentCwd, ws);
294
238
  }
295
239
  },
296
240
  };
@@ -401,6 +345,19 @@ function errorResponse(c, error) {
401
345
  missingAgents: error.missingAgents,
402
346
  }, 400);
403
347
  }
348
+ if (error instanceof WorkroomAuthorizationError) {
349
+ return c.json({ error: error.message }, 401);
350
+ }
351
+ if (error instanceof WorkroomRevisionError) {
352
+ return c.json({
353
+ error: error.message,
354
+ expectedRevision: error.expected,
355
+ actualRevision: error.actual,
356
+ }, 409);
357
+ }
358
+ if (error instanceof WorkroomValidationError) {
359
+ return c.json({ error: error.message }, 400);
360
+ }
404
361
  const msg = error instanceof Error ? error.message : String(error);
405
362
  return c.json({ error: msg }, 500);
406
363
  }
@@ -431,35 +388,18 @@ async function findResourceAssignments(workspacesDir, resourceId, kind) {
431
388
  }
432
389
  return assignments;
433
390
  }
434
- /**
435
- * Restrict the self-update download to GitHub release hosts over HTTPS so a
436
- * cross-origin caller cannot make the server fetch and later execute an
437
- * arbitrary binary.
438
- */
439
- export function isAllowedUpdateUrl(candidate) {
440
- try {
441
- const url = new URL(candidate);
442
- if (url.protocol !== 'https:')
443
- return false;
444
- const host = url.hostname.toLowerCase();
445
- if (host === 'github.com') {
446
- return url.pathname.includes('/releases/download/');
447
- }
448
- return (host === 'objects.githubusercontent.com' ||
449
- host === 'github-releases.githubusercontent.com');
450
- }
451
- catch {
452
- return false;
453
- }
454
- }
455
391
  // Enable CORS for the local GUI only. This server can spawn processes, run
456
392
  // package installs and delete worktrees, so a wildcard origin would let any
457
393
  // web page the developer has open drive it cross-origin.
458
394
  app.use('/api/*', cors({
459
- origin: (origin) => {
395
+ credentials: true,
396
+ origin: (origin, c) => {
460
397
  // No Origin header → same-origin or a non-browser client (CLI/desktop).
461
398
  if (!origin)
462
399
  return origin;
400
+ if (c.req.path.startsWith('/api/workrooms/')) {
401
+ return hasExactDashboardOrigin(origin, c.req.url) ? origin : null;
402
+ }
463
403
  try {
464
404
  const { hostname } = new URL(origin);
465
405
  const isLocal = hostname === 'localhost' ||
@@ -493,10 +433,18 @@ app.use('*', async (c, next) => {
493
433
  // cross-site request forgery and browser form posts from untrusted web pages.
494
434
  app.use('/api/*', async (c, next) => {
495
435
  if (['POST', 'PUT', 'DELETE', 'PATCH'].includes(c.req.method)) {
436
+ const secFetchSite = c.req.header('sec-fetch-site');
437
+ if (secFetchSite === 'cross-site') {
438
+ return c.json({ error: 'Forbidden cross-site request.' }, 403);
439
+ }
496
440
  const origin = c.req.header('origin');
497
441
  if (origin && !hasTrustedLocalOrigin(origin)) {
498
442
  return c.json({ error: 'Forbidden cross-origin request.' }, 403);
499
443
  }
444
+ const referer = c.req.header('referer');
445
+ if (referer && !hasTrustedLocalOrigin(referer)) {
446
+ return c.json({ error: 'Forbidden cross-origin referer.' }, 403);
447
+ }
500
448
  }
501
449
  await next();
502
450
  });
@@ -525,6 +473,116 @@ app.post('/api/adapters/status/refresh', async (c) => {
525
473
  }
526
474
  return c.json(ProviderRegistry.getAllStatus({ refreshProviderId: providerId }));
527
475
  });
476
+ app.post('/api/chat/upload-attachment', async (c) => {
477
+ if (!hasTrustedLocalOrigin(c.req.header('origin'))) {
478
+ return c.json({ error: 'A local browser origin is required.' }, 403);
479
+ }
480
+ try {
481
+ const body = await c.req.json().catch(() => null);
482
+ if (!body || typeof body.dataUrl !== 'string' || !body.dataUrl.startsWith('data:image/')) {
483
+ return c.json({ error: 'A valid image dataUrl is required.' }, 400);
484
+ }
485
+ const matches = body.dataUrl.match(/^data:image\/([a-zA-Z0-9+.-]+);base64,(.+)$/);
486
+ if (!matches) {
487
+ return c.json({ error: 'Malformed image dataUrl format.' }, 400);
488
+ }
489
+ const MAX_ATTACHMENT_BASE64_CHARS = Math.ceil((20 * 1024 * 1024 * 4) / 3);
490
+ if (matches[2].length > MAX_ATTACHMENT_BASE64_CHARS) {
491
+ return c.json({ error: 'Image attachment exceeds maximum allowed size of 20MB.' }, 413);
492
+ }
493
+ const rawExt = matches[1].toLowerCase();
494
+ const ext = rawExt === 'jpeg' ? 'jpg' : rawExt.replace(/[^a-zA-Z0-9]/g, '') || 'png';
495
+ const buffer = Buffer.from(matches[2], 'base64');
496
+ const imageId = crypto.randomUUID();
497
+ const config = await loadConfig();
498
+ const workspaceDir = typeof body.workspacePath === 'string'
499
+ ? await resolveExactLaunchWorkspace(config.workspacesDir, body.workspacePath)
500
+ : null;
501
+ const targetDir = workspaceDir
502
+ ? path.join(workspaceDir, '.nexusflow', 'attachments')
503
+ : path.join(getConfigDir(), 'attachments');
504
+ await fs.mkdir(targetDir, { recursive: true });
505
+ const targetFile = path.join(targetDir, `${imageId}.${ext}`);
506
+ await fs.writeFile(targetFile, buffer);
507
+ return c.json({
508
+ id: imageId,
509
+ filename: typeof body.filename === 'string' ? body.filename : `image-${imageId}.${ext}`,
510
+ path: targetFile,
511
+ size: buffer.length,
512
+ });
513
+ }
514
+ catch (error) {
515
+ return errorResponse(c, error);
516
+ }
517
+ });
518
+ app.get('/api/chat/thread/:workspaceId', async (c) => {
519
+ const workspaceId = c.req.param('workspaceId');
520
+ if (!workspaceId) {
521
+ return c.json({ error: 'workspaceId is required' }, 400);
522
+ }
523
+ try {
524
+ const thread = loadChatThread(workspaceId);
525
+ const config = await loadConfig().catch(() => null);
526
+ let isBusy = false;
527
+ if (config) {
528
+ try {
529
+ const workspacePath = resolveWorkspacePath(config.workspacesDir, workspaceId);
530
+ isBusy = defaultTurnSessionManager.hasActiveTurn(workspacePath);
531
+ }
532
+ catch {
533
+ // Workspace not found on disk or invalid
534
+ }
535
+ }
536
+ return c.json({ thread, isBusy });
537
+ }
538
+ catch (error) {
539
+ return errorResponse(c, error);
540
+ }
541
+ });
542
+ app.post('/api/chat/thread/:workspaceId', async (c) => {
543
+ if (!hasTrustedLocalOrigin(c.req.header('origin'))) {
544
+ return c.json({ error: 'A local browser origin is required.' }, 403);
545
+ }
546
+ const workspaceId = c.req.param('workspaceId');
547
+ if (!workspaceId) {
548
+ return c.json({ error: 'workspaceId is required' }, 400);
549
+ }
550
+ try {
551
+ const body = await c.req.json().catch(() => null);
552
+ if (!body || typeof body !== 'object') {
553
+ return c.json({ error: 'Valid chat thread payload is required.' }, 400);
554
+ }
555
+ saveChatThread({
556
+ workspaceId,
557
+ providerId: typeof body.providerId === 'string' ? body.providerId : null,
558
+ sessions: typeof body.sessions === 'object' && body.sessions !== null ? body.sessions : {},
559
+ profilesByProvider: typeof body.profilesByProvider === 'object' && body.profilesByProvider !== null ? body.profilesByProvider : {},
560
+ modelsByProvider: typeof body.modelsByProvider === 'object' && body.modelsByProvider !== null ? body.modelsByProvider : {},
561
+ effortsByProvider: typeof body.effortsByProvider === 'object' && body.effortsByProvider !== null ? body.effortsByProvider : {},
562
+ messages: Array.isArray(body.messages) ? body.messages : [],
563
+ });
564
+ return c.json({ success: true });
565
+ }
566
+ catch (error) {
567
+ return errorResponse(c, error);
568
+ }
569
+ });
570
+ app.delete('/api/chat/thread/:workspaceId', async (c) => {
571
+ if (!hasTrustedLocalOrigin(c.req.header('origin'))) {
572
+ return c.json({ error: 'A local browser origin is required.' }, 403);
573
+ }
574
+ const workspaceId = c.req.param('workspaceId');
575
+ if (!workspaceId) {
576
+ return c.json({ error: 'workspaceId is required' }, 400);
577
+ }
578
+ try {
579
+ clearChatThread(workspaceId);
580
+ return c.json({ success: true });
581
+ }
582
+ catch (error) {
583
+ return errorResponse(c, error);
584
+ }
585
+ });
528
586
  // 1. Get current configuration
529
587
  app.get('/api/config', async (c) => {
530
588
  try {
@@ -736,6 +794,7 @@ app.get('/api/workspaces/status', async (c) => {
736
794
  runningServices: 0,
737
795
  syncStatus: 'unknown',
738
796
  pendingValidation: false,
797
+ activeAssistants: [],
739
798
  };
740
799
  try {
741
800
  // Uncommitted changes across the workspace's repos: worktrees inside
@@ -758,6 +817,8 @@ app.get('/api/workspaces/status', async (c) => {
758
817
  const repoStates = Object.values(wsState.repos);
759
818
  status.syncStatus = worstSyncStatus(repoStates);
760
819
  status.pendingValidation = repoStates.some((r) => r.pendingValidation);
820
+ // Active AI assistant sessions
821
+ status.activeAssistants = await findActiveAssistants(workspacePath, ws.repos);
761
822
  }
762
823
  catch {
763
824
  // Leave defaults on any per-workspace failure so one bad repo doesn't
@@ -1126,7 +1187,6 @@ app.post('/api/workspace/:id/isolate', async (c) => {
1126
1187
  app.post('/api/workspace/suggest-workflow', async (c) => {
1127
1188
  try {
1128
1189
  const { description, repos } = await c.req.json();
1129
- const config = await loadConfig();
1130
1190
  const suggestion = await suggestWorkflow(description, repos);
1131
1191
  return c.json({
1132
1192
  success: true,
@@ -1332,6 +1392,15 @@ app.get('/api/workspace/:id/services', async (c) => {
1332
1392
  return errorResponse(c, error);
1333
1393
  }
1334
1394
  });
1395
+ function getWorkspaceLogDir(workspacePath) {
1396
+ const primary = path.join(workspacePath, PRIMARY_LOGS_DIR);
1397
+ const legacy = path.join(workspacePath, LEGACY_LOGS_DIR);
1398
+ if (existsSync(primary))
1399
+ return primary;
1400
+ if (existsSync(legacy))
1401
+ return legacy;
1402
+ return primary;
1403
+ }
1335
1404
  // 10. Start services in workspace. Configs are re-detected server-side —
1336
1405
  // the client only says "start", never what to execute.
1337
1406
  app.post('/api/workspace/:id/services/start', async (c) => {
@@ -1339,7 +1408,7 @@ app.post('/api/workspace/:id/services/start', async (c) => {
1339
1408
  const id = c.req.param('id');
1340
1409
  const config = await loadConfig();
1341
1410
  const workspacePath = resolveWorkspacePath(config.workspacesDir, id);
1342
- const logDir = path.join(workspacePath, '.nexusflow-logs');
1411
+ const logDir = getWorkspaceLogDir(workspacePath);
1343
1412
  const services = await detectAllServices(workspacePath);
1344
1413
  await startServices(services, workspacePath, logDir);
1345
1414
  return c.json({ success: true });
@@ -1370,7 +1439,7 @@ app.post('/api/workspace/:id/services/:serviceName/:action{start|stop|restart}',
1370
1439
  const action = c.req.param('action');
1371
1440
  const config = await loadConfig();
1372
1441
  const workspacePath = resolveWorkspacePath(config.workspacesDir, id);
1373
- const logDir = path.join(workspacePath, '.nexusflow-logs');
1442
+ const logDir = getWorkspaceLogDir(workspacePath);
1374
1443
  if (action === 'stop') {
1375
1444
  const stopped = await stopService(workspacePath, serviceName);
1376
1445
  return c.json({ success: true, stopped });
@@ -1391,7 +1460,7 @@ app.post('/api/workspace/:id/services/:serviceName/:action{start|stop|restart}',
1391
1460
  });
1392
1461
  /** Resolve + contain a service log path; names may contain '/' (repo/sub). */
1393
1462
  function resolveServiceLogFile(workspacePath, serviceName) {
1394
- const logDir = path.join(workspacePath, '.nexusflow-logs');
1463
+ const logDir = getWorkspaceLogDir(workspacePath);
1395
1464
  return assertWithin(logDir, path.join(logDir, `${serviceName}.log`));
1396
1465
  }
1397
1466
  // 12. Get service logs (backfill). Returns the trailing 50KB and the byte
@@ -1406,14 +1475,18 @@ app.get('/api/workspace/:id/services/logs/:serviceName', async (c) => {
1406
1475
  let content = '';
1407
1476
  let size = 0;
1408
1477
  try {
1409
- const stats = await fs.stat(logFile);
1410
- size = stats.size;
1411
- const start = Math.max(0, size - 50000);
1412
1478
  const fd = await fs.open(logFile, 'r');
1413
- const buffer = Buffer.alloc(size - start);
1414
- await fd.read(buffer, 0, buffer.length, start);
1415
- await fd.close();
1416
- content = buffer.toString('utf-8');
1479
+ try {
1480
+ const stats = await fd.stat();
1481
+ size = stats.size;
1482
+ const start = Math.max(0, size - 50000);
1483
+ const buffer = Buffer.alloc(size - start);
1484
+ await fd.read(buffer, 0, buffer.length, start);
1485
+ content = buffer.toString('utf-8');
1486
+ }
1487
+ finally {
1488
+ await fd.close();
1489
+ }
1417
1490
  }
1418
1491
  catch {
1419
1492
  content = 'No logs available yet.';
@@ -1482,7 +1555,7 @@ app.post('/api/workspace/:id/orchestrators/:action{start|stop}', async (c) => {
1482
1555
  }
1483
1556
  const config = await loadConfig();
1484
1557
  const workspacePath = resolveWorkspacePath(config.workspacesDir, id);
1485
- const logDir = path.join(workspacePath, '.nexusflow-logs');
1558
+ const logDir = getWorkspaceLogDir(workspacePath);
1486
1559
  const tools = await detectOrchestrationTools(workspacePath);
1487
1560
  const detection = tools.find((t) => t.id === body.id);
1488
1561
  if (!detection) {
@@ -1641,6 +1714,214 @@ app.get('/api/workspace/:id/changes/diff', async (c) => {
1641
1714
  return errorResponse(c, error);
1642
1715
  }
1643
1716
  });
1717
+ // 13b. Revert changes to specific files in workspace sub-repositories
1718
+ app.post('/api/workspace/:id/changes/revert', async (c) => {
1719
+ if (!hasTrustedLocalOrigin(c.req.header('origin'))) {
1720
+ return c.json({ error: 'A local browser origin is required.' }, 403);
1721
+ }
1722
+ try {
1723
+ const id = c.req.param('id');
1724
+ const body = await c.req.json().catch(() => null);
1725
+ const files = Array.isArray(body?.files) ? body.files.filter((f) => typeof f === 'string' && f.trim().length > 0) : [];
1726
+ if (files.length === 0) {
1727
+ return c.json({ error: 'Missing files array in request body.' }, 400);
1728
+ }
1729
+ const config = await loadConfig();
1730
+ const workspacePath = resolveWorkspacePath(config.workspacesDir, id);
1731
+ const feature = await loadFeatureConfig(workspacePath);
1732
+ const repoName = typeof body?.repo === 'string' && body.repo ? body.repo : (feature?.repos[0] ? path.basename(feature.repos[0]) : null);
1733
+ let worktreePath;
1734
+ if (feature && isInPlace(feature)) {
1735
+ const matches = repoName ? feature.repos.filter((r) => path.basename(r) === repoName) : feature.repos;
1736
+ if (matches.length === 0) {
1737
+ return c.json({ error: `Unknown repo in this workspace.` }, 404);
1738
+ }
1739
+ if (matches.length > 1) {
1740
+ return c.json({ error: `Ambiguous repository '${repoName}' in this workspace. Multiple repositories match this name.` }, 400);
1741
+ }
1742
+ worktreePath = matches[0];
1743
+ }
1744
+ else {
1745
+ worktreePath = repoName ? resolveRepoPath(workspacePath, repoName) : workspacePath;
1746
+ }
1747
+ // Snapshot git index so staged changes can be restored with 100% fidelity on rollback
1748
+ let indexPath = null;
1749
+ let originalIndexBuf = null;
1750
+ try {
1751
+ const { stdout: gitPathOut } = await execa('git', ['rev-parse', '--git-path', 'index'], { cwd: worktreePath });
1752
+ const relIndexPath = gitPathOut.trim();
1753
+ if (relIndexPath) {
1754
+ indexPath = path.resolve(worktreePath, relIndexPath);
1755
+ try {
1756
+ originalIndexBuf = await fs.readFile(indexPath);
1757
+ }
1758
+ catch (readErr) {
1759
+ if (readErr?.code !== 'ENOENT') {
1760
+ return c.json({
1761
+ error: `Failed to snapshot Git index before revert: ${readErr?.message ?? String(readErr)}`,
1762
+ }, 500);
1763
+ }
1764
+ originalIndexBuf = null;
1765
+ }
1766
+ }
1767
+ }
1768
+ catch (gitErr) {
1769
+ return c.json({
1770
+ error: `Failed to resolve Git index path before revert: ${gitErr?.message ?? String(gitErr)}`,
1771
+ }, 500);
1772
+ }
1773
+ const preflightItems = [];
1774
+ for (const file of files) {
1775
+ const resolvedFile = assertWithin(worktreePath, path.join(worktreePath, file));
1776
+ const relFile = path.relative(worktreePath, resolvedFile);
1777
+ const { stdout: statusOut } = await execa('git', ['status', '--porcelain', '--', relFile], { cwd: worktreePath });
1778
+ const statusLine = statusOut.trim();
1779
+ if (!statusLine) {
1780
+ continue;
1781
+ }
1782
+ // Preflight unsupported statuses: conflicts (U, AA, DD), submodules (S), typechanges (T)
1783
+ const code = statusLine.slice(0, 2);
1784
+ if (code.includes('U') || code === 'AA' || code === 'DD') {
1785
+ return c.json({
1786
+ error: `Cannot revert '${relFile}': file has unmerged git conflicts ('${statusLine}'). Resolve conflicts first.`,
1787
+ }, 400);
1788
+ }
1789
+ if (code.includes('S') || code.includes('T')) {
1790
+ return c.json({
1791
+ error: `Cannot revert '${relFile}': file has unsupported git status ('${statusLine}').`,
1792
+ }, 400);
1793
+ }
1794
+ const isUntracked = statusLine.startsWith('??');
1795
+ let exists = false;
1796
+ let originalContent;
1797
+ let originalMode;
1798
+ try {
1799
+ let handle = null;
1800
+ try {
1801
+ if (typeof fs.open === 'function') {
1802
+ handle = await fs.open(resolvedFile, 'r');
1803
+ }
1804
+ if (handle && typeof handle.readFile === 'function') {
1805
+ const [content, stat] = await Promise.all([
1806
+ handle.readFile(),
1807
+ typeof handle.stat === 'function' ? handle.stat() : Promise.resolve(undefined),
1808
+ ]);
1809
+ originalContent = content;
1810
+ originalMode = stat?.mode;
1811
+ exists = true;
1812
+ }
1813
+ else {
1814
+ originalContent = await fs.readFile(resolvedFile);
1815
+ exists = true;
1816
+ }
1817
+ }
1818
+ finally {
1819
+ if (handle && typeof handle.close === 'function') {
1820
+ await handle.close();
1821
+ }
1822
+ }
1823
+ }
1824
+ catch (err) {
1825
+ if (err?.code !== 'ENOENT') {
1826
+ throw err;
1827
+ }
1828
+ exists = false;
1829
+ }
1830
+ preflightItems.push({
1831
+ resolvedFile,
1832
+ relFile,
1833
+ isUntracked,
1834
+ exists,
1835
+ originalContent,
1836
+ originalMode,
1837
+ });
1838
+ }
1839
+ // Execution with rollback protection: if any operation fails, restore previous state
1840
+ const executedReverts = [];
1841
+ try {
1842
+ for (const item of preflightItems) {
1843
+ if (item.isUntracked) {
1844
+ if (item.exists) {
1845
+ await fs.unlink(item.resolvedFile);
1846
+ }
1847
+ }
1848
+ else {
1849
+ await execa('git', ['checkout', 'HEAD', '--', item.relFile], { cwd: worktreePath });
1850
+ }
1851
+ executedReverts.push(item);
1852
+ }
1853
+ }
1854
+ catch (revertErr) {
1855
+ const rollbackErrors = [];
1856
+ // 1. Restore the Git index first if it was snapshotted
1857
+ if (indexPath && originalIndexBuf) {
1858
+ try {
1859
+ await fs.writeFile(indexPath, originalIndexBuf);
1860
+ }
1861
+ catch (indexErr) {
1862
+ rollbackErrors.push(`Failed to restore git index: ${indexErr?.message ?? String(indexErr)}`);
1863
+ }
1864
+ }
1865
+ // 2. Restore modified working tree files and metadata
1866
+ for (const item of executedReverts) {
1867
+ try {
1868
+ if (item.isUntracked) {
1869
+ if (item.exists && item.originalContent) {
1870
+ await fs.writeFile(item.resolvedFile, item.originalContent);
1871
+ if (item.originalMode !== undefined && typeof fs.chmod === 'function') {
1872
+ try {
1873
+ await fs.chmod(item.resolvedFile, item.originalMode);
1874
+ }
1875
+ catch (chmodErr) {
1876
+ rollbackErrors.push(`Failed to restore mode on ${item.relFile}: ${chmodErr?.message ?? String(chmodErr)}`);
1877
+ }
1878
+ }
1879
+ }
1880
+ }
1881
+ else {
1882
+ if (item.exists && item.originalContent) {
1883
+ await fs.writeFile(item.resolvedFile, item.originalContent);
1884
+ if (item.originalMode !== undefined && typeof fs.chmod === 'function') {
1885
+ try {
1886
+ await fs.chmod(item.resolvedFile, item.originalMode);
1887
+ }
1888
+ catch (chmodErr) {
1889
+ rollbackErrors.push(`Failed to restore mode on ${item.relFile}: ${chmodErr?.message ?? String(chmodErr)}`);
1890
+ }
1891
+ }
1892
+ }
1893
+ else if (!item.exists) {
1894
+ try {
1895
+ await fs.unlink(item.resolvedFile);
1896
+ }
1897
+ catch (unlinkErr) {
1898
+ if (unlinkErr?.code !== 'ENOENT') {
1899
+ rollbackErrors.push(`Failed to remove checked-out file ${item.relFile}: ${unlinkErr?.message ?? String(unlinkErr)}`);
1900
+ }
1901
+ }
1902
+ }
1903
+ }
1904
+ }
1905
+ catch (restoreErr) {
1906
+ rollbackErrors.push(`Failed to restore ${item.relFile}: ${restoreErr?.message ?? String(restoreErr)}`);
1907
+ }
1908
+ }
1909
+ if (rollbackErrors.length > 0) {
1910
+ return c.json({
1911
+ error: `Failed to revert files: ${revertErr?.message ?? String(revertErr)}. Warning: Rollback could not fully restore state: ${rollbackErrors.join('; ')}`,
1912
+ }, 500);
1913
+ }
1914
+ return c.json({
1915
+ error: `Failed to revert files: ${revertErr?.message ?? String(revertErr)}. Any modified files were rolled back.`,
1916
+ }, 500);
1917
+ }
1918
+ const reverted = preflightItems.map((item) => item.relFile);
1919
+ return c.json({ success: true, reverted });
1920
+ }
1921
+ catch (error) {
1922
+ return errorResponse(c, error);
1923
+ }
1924
+ });
1644
1925
  // 13a. Get workspace knowledge (nexusflow-knowledge.md)
1645
1926
  // Routed through the active storage adapter so the GUI edits the same file the
1646
1927
  // generators write, whichever backend is active.
@@ -1665,7 +1946,14 @@ app.put('/api/workspace/:id/knowledge', async (c) => {
1665
1946
  const workspacePath = resolveWorkspacePath(config.workspacesDir, id);
1666
1947
  const feature = await loadFeatureConfig(workspacePath);
1667
1948
  const featureId = feature?.id ?? path.basename(workspacePath);
1668
- await writeWorkspaceFile(workspacePath, featureId, 'nexusflow-knowledge.md', content);
1949
+ const { PRIMARY_KNOWLEDGE_FILE, LEGACY_KNOWLEDGE_FILE } = await import('./core/constants.js');
1950
+ const { workspaceFileExists } = await import('./core/storage.js');
1951
+ const filename = (await workspaceFileExists(workspacePath, featureId, PRIMARY_KNOWLEDGE_FILE))
1952
+ ? PRIMARY_KNOWLEDGE_FILE
1953
+ : (await workspaceFileExists(workspacePath, featureId, LEGACY_KNOWLEDGE_FILE))
1954
+ ? LEGACY_KNOWLEDGE_FILE
1955
+ : PRIMARY_KNOWLEDGE_FILE;
1956
+ await writeWorkspaceFile(workspacePath, featureId, filename, content);
1669
1957
  return c.json({ success: true });
1670
1958
  }
1671
1959
  catch (error) {
@@ -1706,16 +1994,17 @@ app.post('/api/workspace/:id/knowledge/entry', async (c) => {
1706
1994
  return errorResponse(c, error);
1707
1995
  }
1708
1996
  });
1709
- // 13c. Get workspace plan (nexusflow-plan.md)
1997
+ // 13c. Get workspace plan (contextspace-plan.md / nexusflow-plan.md)
1710
1998
  app.get('/api/workspace/:id/plan', async (c) => {
1711
1999
  try {
1712
2000
  const id = c.req.param('id');
1713
2001
  const config = await loadConfig();
1714
2002
  const workspacePath = resolveWorkspacePath(config.workspacesDir, id);
1715
- const planFile = path.join(workspacePath, 'nexusflow-plan.md');
2003
+ const { resolveWorkspaceFilePath } = await import('./core/brand-config.js');
2004
+ const resolved = await resolveWorkspaceFilePath(workspacePath, 'plan');
1716
2005
  let content = '';
1717
2006
  try {
1718
- content = await fs.readFile(planFile, 'utf-8');
2007
+ content = await fs.readFile(resolved.path, 'utf-8');
1719
2008
  }
1720
2009
  catch {
1721
2010
  content = '# Workspace Plan\n\nNo implementation plan file yet.';
@@ -1726,70 +2015,206 @@ app.get('/api/workspace/:id/plan', async (c) => {
1726
2015
  return errorResponse(c, error);
1727
2016
  }
1728
2017
  });
1729
- // 13d. Sync all repositories in workspace
1730
- app.post('/api/workspace/:id/sync', async (c) => {
2018
+ // 13c-2. Check workspace migration status (legacy NexusFlow files detected)
2019
+ app.get('/api/workspace/:id/migration-status', async (c) => {
1731
2020
  try {
1732
2021
  const id = c.req.param('id');
1733
2022
  const config = await loadConfig();
1734
2023
  const workspacePath = resolveWorkspacePath(config.workspacesDir, id);
1735
- const report = await syncWorkspace(workspacePath);
1736
- const results = report.repos.map((repo) => ({
1737
- repoName: repo.name,
1738
- success: repo.status !== 'conflict' && repo.status !== 'error',
1739
- status: repo.status,
1740
- message: repo.message,
1741
- conflict: repo.conflict,
1742
- }));
1743
- return c.json({
1744
- results,
1745
- syncedCount: report.syncedCount,
1746
- conflictCount: report.conflictCount,
1747
- errorCount: report.errorCount,
1748
- });
2024
+ const { isLegacyWorkspace } = await import('./core/migrate.js');
2025
+ const isLegacy = await isLegacyWorkspace(workspacePath);
2026
+ return c.json({ isLegacy });
1749
2027
  }
1750
2028
  catch (error) {
1751
2029
  return errorResponse(c, error);
1752
2030
  }
1753
2031
  });
1754
- // 13e. Commit changes in all repositories in workspace
1755
- app.post('/api/workspace/:id/commit', async (c) => {
2032
+ // 13c-3. Migrate legacy workspace to native ContextSpace
2033
+ app.post('/api/workspace/:id/migrate', async (c) => {
1756
2034
  try {
1757
2035
  const id = c.req.param('id');
1758
- const { message } = await c.req.json();
1759
2036
  const config = await loadConfig();
1760
2037
  const workspacePath = resolveWorkspacePath(config.workspacesDir, id);
1761
- const report = await commitWorkspace(workspacePath, message);
1762
- const results = report.repos.map((repo) => ({
1763
- repoName: repo.name,
1764
- success: repo.success,
1765
- commitHash: repo.commitHash,
1766
- filesChanged: repo.filesChanged,
1767
- message: repo.message,
1768
- }));
1769
- return c.json({ results });
2038
+ const { migrateWorkspace } = await import('./core/migrate.js');
2039
+ const report = await migrateWorkspace(workspacePath, { dryRun: false, refresh: true });
2040
+ return c.json(report);
1770
2041
  }
1771
2042
  catch (error) {
1772
2043
  return errorResponse(c, error);
1773
2044
  }
1774
2045
  });
1775
- // 14. Resume session in workspace (copies CLI resume command and opens editor)
1776
- app.post('/api/workspace/:id/resume', async (c) => {
1777
- const origin = c.req.header('origin');
1778
- if (origin && !hasTrustedLocalOrigin(origin)) {
1779
- return c.json({ error: 'Forbidden cross-origin request.' }, 403);
1780
- }
2046
+ // 13c-4. Get workspace collaboration stream (.nexusflow/chat.jsonl or .contextspace/chat.jsonl)
2047
+ app.get('/api/workspace/:id/stream', async (c) => {
1781
2048
  try {
1782
- const id = decodeURIComponent(c.req.param('id'));
2049
+ const id = c.req.param('id');
1783
2050
  const config = await loadConfig();
1784
2051
  const workspacePath = resolveWorkspacePath(config.workspacesDir, id);
1785
- // Load feature config to get repo paths
1786
- const feature = await loadFeatureConfig(workspacePath);
1787
- if (!feature) {
1788
- return c.json({ error: 'Workspace configuration not found.' }, 404);
2052
+ const { messages, ledgerInfo } = await readWorkspaceChatMessages(workspacePath);
2053
+ let isRemoteActive = false;
2054
+ let remoteStatus = null;
2055
+ let workflowProgress = null;
2056
+ try {
2057
+ const client = await workroomManagerModule.loadPinnedWorkroomClientForWorkspace(id);
2058
+ const snapshot = await client.snapshot();
2059
+ isRemoteActive = true;
2060
+ workflowProgress = snapshot.workflowProgress ?? null;
2061
+ remoteStatus = {
2062
+ roomId: snapshot.roomId,
2063
+ name: snapshot.name,
2064
+ };
1789
2065
  }
1790
- const body = await c.req.json().catch(() => ({}));
1791
- const targetSessionId = typeof body.sessionId === 'string' && isValidSessionUuid(body.sessionId) ? body.sessionId : undefined;
1792
- if (body.sessionId !== undefined && targetSessionId === undefined) {
2066
+ catch {
2067
+ // Fallback to in-process workroomManager
2068
+ try {
2069
+ if (workroomManagerModule.workroomManager.hasActiveRoom()) {
2070
+ const status = await workroomManagerModule.workroomManager.status();
2071
+ if ((status.mode === 'host' || status.mode === 'guest') && status.localWorkspaceId === id) {
2072
+ isRemoteActive = true;
2073
+ workflowProgress = status.snapshot?.workflowProgress ?? null;
2074
+ remoteStatus = {
2075
+ roomId: status.roomId,
2076
+ url: status.url,
2077
+ name: status.name,
2078
+ };
2079
+ }
2080
+ }
2081
+ }
2082
+ catch {
2083
+ // ignore
2084
+ }
2085
+ }
2086
+ // Reconcile messages with live authoritative workflow progress by stepId
2087
+ const stepMap = new Map();
2088
+ if (workflowProgress?.steps) {
2089
+ for (const step of workflowProgress.steps) {
2090
+ stepMap.set(step.stepId, step);
2091
+ }
2092
+ }
2093
+ const reconciledMessages = messages.map((msg) => {
2094
+ if (!msg.stepId)
2095
+ return msg;
2096
+ const liveStep = stepMap.get(msg.stepId);
2097
+ if (!liveStep)
2098
+ return msg;
2099
+ return {
2100
+ ...msg,
2101
+ stepProposal: liveStep,
2102
+ };
2103
+ });
2104
+ const relLedgerPath = ledgerInfo.isLegacy ? '.nexusflow/chat.jsonl' : '.contextspace/chat.jsonl';
2105
+ return c.json({
2106
+ workspaceId: id,
2107
+ messages: reconciledMessages,
2108
+ workflowProgress,
2109
+ isRemoteActive,
2110
+ remoteStatus,
2111
+ isLegacy: ledgerInfo.isLegacy,
2112
+ ledgerPath: relLedgerPath,
2113
+ });
2114
+ }
2115
+ catch (error) {
2116
+ return errorResponse(c, error);
2117
+ }
2118
+ });
2119
+ // 13c-5. Post a message to workspace collaboration stream
2120
+ app.post('/api/workspace/:id/stream', async (c) => {
2121
+ try {
2122
+ const id = c.req.param('id');
2123
+ const body = (await c.req.json());
2124
+ const message = String(body.message ?? '').trim();
2125
+ if (!message) {
2126
+ return c.json({ error: 'Message cannot be empty.' }, 400);
2127
+ }
2128
+ const config = await loadConfig();
2129
+ const workspacePath = resolveWorkspacePath(config.workspacesDir, id);
2130
+ const ledger = await resolveWorkspaceChatLedger(workspacePath);
2131
+ await fs.mkdir(ledger.chatDir, { recursive: true });
2132
+ const chatFile = ledger.chatPath;
2133
+ const isHuman = body.author === 'human' || body.harness === 'developer' || body.harness === 'human';
2134
+ const status = body.status ? String(body.status).trim() : (body.stepId ? 'proposed' : undefined);
2135
+ const effectiveStatus = (!isHuman && status === 'completed') ? 'proposed' : status;
2136
+ const entry = {
2137
+ id: createHash('sha256').update(Date.now() + message).digest('hex').slice(0, 12),
2138
+ timestamp: new Date().toISOString(),
2139
+ harness: body.harness || (isHuman ? 'developer' : 'agent'),
2140
+ author: isHuman ? 'human' : 'agent',
2141
+ status: effectiveStatus,
2142
+ message,
2143
+ ...(body.stepId ? { stepId: body.stepId } : {}),
2144
+ ...(body.evidence ? { evidence: body.evidence } : {}),
2145
+ ...(isHuman && effectiveStatus === 'completed' ? { confirmedBy: 'human', confirmedAt: new Date().toISOString() } : {}),
2146
+ };
2147
+ await fs.appendFile(chatFile, JSON.stringify(entry) + '\n', 'utf-8');
2148
+ return c.json({ success: true, entry });
2149
+ }
2150
+ catch (error) {
2151
+ return errorResponse(c, error);
2152
+ }
2153
+ });
2154
+ // 13d. Sync all repositories in workspace
2155
+ app.post('/api/workspace/:id/sync', async (c) => {
2156
+ try {
2157
+ const id = c.req.param('id');
2158
+ const config = await loadConfig();
2159
+ const workspacePath = resolveWorkspacePath(config.workspacesDir, id);
2160
+ const report = await syncWorkspace(workspacePath);
2161
+ const results = report.repos.map((repo) => ({
2162
+ repoName: repo.name,
2163
+ success: repo.status !== 'conflict' && repo.status !== 'error',
2164
+ status: repo.status,
2165
+ message: repo.message,
2166
+ conflict: repo.conflict,
2167
+ }));
2168
+ return c.json({
2169
+ results,
2170
+ syncedCount: report.syncedCount,
2171
+ conflictCount: report.conflictCount,
2172
+ errorCount: report.errorCount,
2173
+ });
2174
+ }
2175
+ catch (error) {
2176
+ return errorResponse(c, error);
2177
+ }
2178
+ });
2179
+ // 13e. Commit changes in all repositories in workspace
2180
+ app.post('/api/workspace/:id/commit', async (c) => {
2181
+ try {
2182
+ const id = c.req.param('id');
2183
+ const { message } = await c.req.json();
2184
+ const config = await loadConfig();
2185
+ const workspacePath = resolveWorkspacePath(config.workspacesDir, id);
2186
+ const report = await commitWorkspace(workspacePath, message);
2187
+ const results = report.repos.map((repo) => ({
2188
+ repoName: repo.name,
2189
+ success: repo.success,
2190
+ commitHash: repo.commitHash,
2191
+ filesChanged: repo.filesChanged,
2192
+ message: repo.message,
2193
+ }));
2194
+ return c.json({ results });
2195
+ }
2196
+ catch (error) {
2197
+ return errorResponse(c, error);
2198
+ }
2199
+ });
2200
+ // 14. Resume session in workspace (copies CLI resume command and opens editor)
2201
+ app.post('/api/workspace/:id/resume', async (c) => {
2202
+ const origin = c.req.header('origin');
2203
+ if (origin && !hasTrustedLocalOrigin(origin)) {
2204
+ return c.json({ error: 'Forbidden cross-origin request.' }, 403);
2205
+ }
2206
+ try {
2207
+ const id = decodeURIComponent(c.req.param('id'));
2208
+ const config = await loadConfig();
2209
+ const workspacePath = resolveWorkspacePath(config.workspacesDir, id);
2210
+ // Load feature config to get repo paths
2211
+ const feature = await loadFeatureConfig(workspacePath);
2212
+ if (!feature) {
2213
+ return c.json({ error: 'Workspace configuration not found.' }, 404);
2214
+ }
2215
+ const body = await c.req.json().catch(() => ({}));
2216
+ const targetSessionId = typeof body.sessionId === 'string' && isValidSessionUuid(body.sessionId) ? body.sessionId : undefined;
2217
+ if (body.sessionId !== undefined && targetSessionId === undefined) {
1793
2218
  return c.json({ error: 'Invalid session UUID format.' }, 400);
1794
2219
  }
1795
2220
  if (body.command !== undefined && (typeof body.command !== 'string' || !ALLOWED_EDITORS.has(body.command))) {
@@ -1981,7 +2406,8 @@ app.post('/api/updates/install', async (c) => {
1981
2406
  try {
1982
2407
  const { toolId } = await c.req.json();
1983
2408
  const tools = [
1984
- { id: 'nexusflow', cmd: 'npm', args: ['install', '-g', '@mrpatronz/nexusflow'] },
2409
+ { id: ENGINE_ID, cmd: 'npm', args: ['install', '-g', ENGINE_NPM_PACKAGE] },
2410
+ { id: LEGACY_ENGINE_ID, cmd: 'npm', args: ['install', '-g', LEGACY_ENGINE_NPM_PACKAGE] },
1985
2411
  { id: 'antigravity', cmd: 'agy', args: ['update'] },
1986
2412
  { id: 'claude', cmd: 'npm', args: ['install', '-g', '@anthropic-ai/claude-code'] },
1987
2413
  ];
@@ -2004,96 +2430,6 @@ app.post('/api/updates/install', async (c) => {
2004
2430
  return errorResponse(c, error);
2005
2431
  }
2006
2432
  });
2007
- let downloadedInstallerPath = null;
2008
- // 17.7. Download matching GitHub Release installer to temporary folder
2009
- app.post('/api/updates/download', async (c) => {
2010
- try {
2011
- const { downloadUrl } = await c.req.json();
2012
- if (!downloadUrl) {
2013
- return c.json({ error: 'Download URL is required' }, 400);
2014
- }
2015
- if (!isAllowedUpdateUrl(downloadUrl)) {
2016
- return c.json({ error: 'Download URL is not an allowed update host' }, 400);
2017
- }
2018
- const tempDir = os.tmpdir();
2019
- const fileName = 'NexusFlowSetup_Update.exe';
2020
- const targetPath = path.join(tempDir, fileName);
2021
- const response = await fetch(downloadUrl);
2022
- if (!response.ok) {
2023
- throw new Error(`HTTP error ${response.status}`);
2024
- }
2025
- // Node fetch body stream download and SHA-256 computation
2026
- const hash = createHash('sha256');
2027
- const fileStream = createWriteStream(targetPath);
2028
- if (!response.body) {
2029
- throw new Error('Response body is null');
2030
- }
2031
- const nodeStream = Readable.fromWeb(response.body);
2032
- nodeStream.on('data', (chunk) => hash.update(chunk));
2033
- await pipeline(nodeStream, fileStream);
2034
- const computedHash = hash.digest('hex').toLowerCase();
2035
- // Verify SHA-256 against release checksum file if published
2036
- try {
2037
- const shaUrl = `${downloadUrl}.sha256`;
2038
- if (isAllowedUpdateUrl(shaUrl)) {
2039
- const shaRes = await fetch(shaUrl);
2040
- if (shaRes.ok) {
2041
- const shaText = await shaRes.text();
2042
- const expectedHash = shaText.trim().split(/\s+/)[0]?.toLowerCase();
2043
- if (expectedHash && expectedHash.length === 64 && expectedHash !== computedHash) {
2044
- await fs.unlink(targetPath).catch(() => { });
2045
- throw new Error(`SHA-256 checksum mismatch (expected ${expectedHash}, got ${computedHash})`);
2046
- }
2047
- }
2048
- }
2049
- }
2050
- catch (shaErr) {
2051
- if (shaErr instanceof Error && shaErr.message.includes('SHA-256 checksum mismatch')) {
2052
- throw shaErr;
2053
- }
2054
- // Non-fatal if sha256 sidecar does not exist on older releases
2055
- }
2056
- downloadedInstallerPath = targetPath;
2057
- return c.json({ success: true, path: targetPath, sha256: computedHash });
2058
- }
2059
- catch (error) {
2060
- const msg = error instanceof Error ? error.message : String(error);
2061
- return c.json({ error: `Download failed: ${msg}` }, 500);
2062
- }
2063
- });
2064
- // 17.8. Launch the silent installer detached and exit server
2065
- app.post('/api/updates/apply', async (c) => {
2066
- if (!downloadedInstallerPath || !existsSync(downloadedInstallerPath)) {
2067
- return c.json({ error: 'No downloaded installer found on disk' }, 400);
2068
- }
2069
- try {
2070
- const isWin = process.platform === 'win32';
2071
- if (isWin) {
2072
- // The desktop app ships an electron-builder NSIS installer (build target
2073
- // 'nsis'), whose silent-install switch is `/S` — NOT Inno Setup's
2074
- // /VERYSILENT. With the wrong flags the one-click installer waits for UI
2075
- // that a detached (stdio: 'ignore') process can never provide, so the
2076
- // update silently fails to apply. `/S` runs it unattended and relaunches
2077
- // the app on finish (electron-builder default).
2078
- console.log(`Applying update: Spawning detached installer at: ${downloadedInstallerPath}`);
2079
- const child = spawn(downloadedInstallerPath, ['/S'], {
2080
- detached: true,
2081
- stdio: 'ignore',
2082
- });
2083
- child.unref();
2084
- }
2085
- // Gracefully exit server process in 1 second to release file locks
2086
- setTimeout(() => {
2087
- console.log('Update installer successfully spawned. Exiting Hono server process...');
2088
- process.exit(0);
2089
- }, 1000);
2090
- return c.json({ success: true, message: 'Installer spawned, app shutting down...' });
2091
- }
2092
- catch (error) {
2093
- const msg = error instanceof Error ? error.message : String(error);
2094
- return c.json({ error: `Failed to execute update: ${msg}` }, 500);
2095
- }
2096
- });
2097
2433
  // 17.9. Get available workflow templates
2098
2434
  app.get('/api/workflows/templates', async (c) => {
2099
2435
  try {
@@ -2399,6 +2735,505 @@ app.post('/api/skills/workspace/:id/assign', async (c) => {
2399
2735
  return errorResponse(c, error);
2400
2736
  }
2401
2737
  });
2738
+ // ─── Workrooms: opt-in LAN/VPN collaboration ─────────────────────────────
2739
+ const WORKROOM_HUMAN_SESSION_COOKIES = [
2740
+ 'contextspace_workroom_human',
2741
+ 'nexusflow_workroom_human',
2742
+ ];
2743
+ const WORKROOM_BOOTSTRAP_TOKEN = randomToken();
2744
+ function establishWorkroomBootstrap(c) {
2745
+ for (const cookieName of WORKROOM_BOOTSTRAP_COOKIES) {
2746
+ setCookie(c, cookieName, WORKROOM_BOOTSTRAP_TOKEN, {
2747
+ httpOnly: true,
2748
+ sameSite: 'Strict',
2749
+ path: '/api/workrooms',
2750
+ });
2751
+ }
2752
+ }
2753
+ function hasValidWorkroomBootstrap(c) {
2754
+ const cookieToken = WORKROOM_BOOTSTRAP_COOKIES
2755
+ .map((name) => getCookie(c, name))
2756
+ .find((val) => typeof val === 'string' && val);
2757
+ const headerToken = WORKROOM_BOOTSTRAP_HEADERS
2758
+ .map((header) => c.req.header(header))
2759
+ .find((val) => typeof val === 'string' && val);
2760
+ return Boolean(cookieToken && headerToken
2761
+ && tokenDigest(cookieToken) === tokenDigest(WORKROOM_BOOTSTRAP_TOKEN)
2762
+ && tokenDigest(headerToken) === tokenDigest(WORKROOM_BOOTSTRAP_TOKEN));
2763
+ }
2764
+ function establishWorkroomHumanSession(c, token = workroomManager.beginHumanSession()) {
2765
+ for (const cookieName of WORKROOM_HUMAN_SESSION_COOKIES) {
2766
+ setCookie(c, cookieName, token, {
2767
+ httpOnly: true,
2768
+ sameSite: 'Strict',
2769
+ path: '/api/workrooms',
2770
+ });
2771
+ }
2772
+ }
2773
+ function getWorkroomHumanSessionToken(c) {
2774
+ return WORKROOM_HUMAN_SESSION_COOKIES
2775
+ .map((name) => getCookie(c, name))
2776
+ .find((val) => typeof val === 'string' && val);
2777
+ }
2778
+ function clearWorkroomHumanSession(c) {
2779
+ for (const cookieName of WORKROOM_HUMAN_SESSION_COOKIES) {
2780
+ deleteCookie(c, cookieName, { path: '/api/workrooms' });
2781
+ }
2782
+ }
2783
+ function hasExactDashboardOrigin(origin, requestUrl) {
2784
+ if (!origin)
2785
+ return false;
2786
+ try {
2787
+ const normalizedOrigin = new URL(origin).origin;
2788
+ const requestOrigin = new URL(requestUrl).origin;
2789
+ const devOrigin = process.env.CONTEXTSPACE_DASHBOARD_ORIGIN || process.env.NEXUSFLOW_DASHBOARD_ORIGIN;
2790
+ const configuredDevelopmentOrigin = devOrigin ? new URL(devOrigin).origin : undefined;
2791
+ return normalizedOrigin === requestOrigin || normalizedOrigin === configuredDevelopmentOrigin;
2792
+ }
2793
+ catch {
2794
+ return false;
2795
+ }
2796
+ }
2797
+ function hasExactWorkroomBrowserBoundary(c) {
2798
+ const origin = c.req.header('origin');
2799
+ if (origin)
2800
+ return hasExactDashboardOrigin(origin, c.req.url);
2801
+ const referer = c.req.header('referer');
2802
+ if (referer)
2803
+ return hasExactDashboardOrigin(referer, c.req.url);
2804
+ return c.req.header('sec-fetch-site') === 'same-origin';
2805
+ }
2806
+ app.use('/api/workrooms/*', async (c, next) => {
2807
+ const pathname = new URL(c.req.url).pathname;
2808
+ c.header('Cache-Control', 'no-store');
2809
+ if (!hasExactWorkroomBrowserBoundary(c)) {
2810
+ return c.json({ error: 'Workroom access requires the local browser dashboard.' }, 403);
2811
+ }
2812
+ if (pathname === '/api/workrooms/bootstrap')
2813
+ return next();
2814
+ if (!hasValidWorkroomBootstrap(c)) {
2815
+ return c.json({ error: 'Workroom access requires a same-origin dashboard bootstrap.' }, 403);
2816
+ }
2817
+ if (pathname === '/api/workrooms/session' || pathname === '/api/workrooms/session/reclaim'
2818
+ || pathname === '/api/workrooms/session/abandon')
2819
+ return next();
2820
+ const isMutation = ['POST', 'PUT', 'PATCH', 'DELETE'].includes(c.req.method);
2821
+ const needsHumanReadSession = c.req.method === 'GET' && (pathname === '/api/workrooms/snapshot'
2822
+ || pathname === '/api/workrooms/local-resources'
2823
+ || (pathname === '/api/workrooms/status' && workroomManager.hasActiveRoom()));
2824
+ if (!isMutation && !needsHumanReadSession)
2825
+ return next();
2826
+ const doesNotRequireExistingHumanSession = pathname === '/api/workrooms/start'
2827
+ || pathname === '/api/workrooms/join'
2828
+ || pathname === '/api/workrooms/import'
2829
+ || /^\/api\/workrooms\/quarantined\/room-[a-f0-9]{32}\/discard$/.test(pathname)
2830
+ || /^\/api\/workrooms\/room-[a-f0-9]{32}\/resume$/.test(pathname);
2831
+ if (needsHumanReadSession || (isMutation && !doesNotRequireExistingHumanSession)) {
2832
+ try {
2833
+ const authority = workroomManager.assertHumanSession(getWorkroomHumanSessionToken(c));
2834
+ if (pathname === '/api/workrooms/stop')
2835
+ return next();
2836
+ await workroomManager.runWithHumanAuthority(authority, () => next());
2837
+ return;
2838
+ }
2839
+ catch (error) {
2840
+ return errorResponse(c, error);
2841
+ }
2842
+ }
2843
+ await next();
2844
+ });
2845
+ app.post('/api/workrooms/bootstrap', (c) => {
2846
+ establishWorkroomBootstrap(c);
2847
+ return c.json({ token: WORKROOM_BOOTSTRAP_TOKEN });
2848
+ });
2849
+ app.get('/api/workrooms/session', (c) => {
2850
+ const active = workroomManager.hasActiveRoom();
2851
+ if (!active)
2852
+ return c.json({ active: false, locked: false });
2853
+ const roomType = workroomManager.activeRoomType();
2854
+ if (!roomType)
2855
+ return c.json({ active: false, locked: false });
2856
+ try {
2857
+ workroomManager.assertHumanSession(getWorkroomHumanSessionToken(c));
2858
+ return c.json({ active: true, locked: false, roomType });
2859
+ }
2860
+ catch {
2861
+ return c.json({ active: true, locked: true, roomType });
2862
+ }
2863
+ });
2864
+ app.post('/api/workrooms/session/reclaim', async (c) => {
2865
+ try {
2866
+ const body = await c.req.json();
2867
+ if (!workroomManager.hasActiveRoom())
2868
+ return c.json({ error: 'No active Workroom can be reconnected.' }, 409);
2869
+ const token = await workroomManager.reclaimHostHumanSession(String(body.password ?? ''));
2870
+ establishWorkroomHumanSession(c, token);
2871
+ return c.json({ success: true });
2872
+ }
2873
+ catch (error) {
2874
+ return errorResponse(c, error);
2875
+ }
2876
+ });
2877
+ app.post('/api/workrooms/session/abandon', async (c) => {
2878
+ try {
2879
+ const body = await c.req.json();
2880
+ if (body.confirm !== true)
2881
+ return c.json({ error: 'Confirm leaving this locked guest connection.' }, 400);
2882
+ await workroomManager.abandonLockedGuest();
2883
+ clearWorkroomHumanSession(c);
2884
+ return c.json({ success: true });
2885
+ }
2886
+ catch (error) {
2887
+ return errorResponse(c, error);
2888
+ }
2889
+ });
2890
+ app.get('/api/workrooms/interfaces', (c) => {
2891
+ return c.json({ interfaces: listWorkroomNetworkInterfaces() });
2892
+ });
2893
+ app.get('/api/workrooms/status', async (c) => {
2894
+ try {
2895
+ const status = await workroomManager.status();
2896
+ if (status.mode !== 'idle') {
2897
+ workroomManager.assertHumanSession(getWorkroomHumanSessionToken(c));
2898
+ }
2899
+ return c.json({ status });
2900
+ }
2901
+ catch (error) {
2902
+ return errorResponse(c, error);
2903
+ }
2904
+ });
2905
+ app.get('/api/workrooms/paused', async (c) => {
2906
+ try {
2907
+ return c.json({ rooms: await workroomManager.listPaused() });
2908
+ }
2909
+ catch (error) {
2910
+ return errorResponse(c, error);
2911
+ }
2912
+ });
2913
+ app.get('/api/workrooms/quarantined', async (c) => {
2914
+ try {
2915
+ return c.json({ rooms: await workroomManager.listQuarantined() });
2916
+ }
2917
+ catch (error) {
2918
+ return errorResponse(c, error);
2919
+ }
2920
+ });
2921
+ app.post('/api/workrooms/quarantined/:roomId/discard', async (c) => {
2922
+ try {
2923
+ const roomId = c.req.param('roomId');
2924
+ const body = await c.req.json();
2925
+ if (body.confirmRoomId !== roomId) {
2926
+ return c.json({ error: 'Confirm the exact quarantined Workroom ID before discarding it.' }, 400);
2927
+ }
2928
+ await workroomManager.discardQuarantined(roomId);
2929
+ return c.json({ success: true });
2930
+ }
2931
+ catch (error) {
2932
+ return errorResponse(c, error);
2933
+ }
2934
+ });
2935
+ app.post('/api/workrooms/:roomId/resume', async (c) => {
2936
+ try {
2937
+ const body = await c.req.json();
2938
+ const status = await workroomManager.resumeHost(c.req.param('roomId'), String(body.password ?? ''));
2939
+ establishWorkroomHumanSession(c);
2940
+ return c.json({ status });
2941
+ }
2942
+ catch (error) {
2943
+ return errorResponse(c, error);
2944
+ }
2945
+ });
2946
+ app.get('/api/workrooms/preview/:workspaceId', async (c) => {
2947
+ try {
2948
+ const workspaceId = decodeURIComponent(c.req.param('workspaceId'));
2949
+ const config = await loadConfig();
2950
+ const workspacePath = await resolveExactWorkspaceById(config.workspacesDir, workspaceId);
2951
+ if (!workspacePath)
2952
+ return c.json({ error: 'Workspace not found.' }, 404);
2953
+ return c.json({ preview: await buildPortableWorkroomPreview(workspacePath) });
2954
+ }
2955
+ catch (error) {
2956
+ return errorResponse(c, error);
2957
+ }
2958
+ });
2959
+ app.post('/api/workrooms/start', async (c) => {
2960
+ try {
2961
+ const body = await c.req.json();
2962
+ const workspaceId = typeof body.workspaceId === 'string' ? body.workspaceId : '';
2963
+ const config = await loadConfig();
2964
+ const workspacePath = await resolveExactWorkspaceById(config.workspacesDir, workspaceId);
2965
+ if (!workspacePath)
2966
+ return c.json({ error: 'Workspace not found.' }, 404);
2967
+ const preview = await buildPortableWorkroomPreview(workspacePath);
2968
+ if (body.contextConfirmed !== true) {
2969
+ return c.json({ error: 'Review the exact shared context and confirm it before starting the Workroom.' }, 400);
2970
+ }
2971
+ const documentOverrides = typeof body.documents === 'object' && body.documents !== null
2972
+ ? body.documents
2973
+ : {};
2974
+ const documents = { plan: '', decisions: '', handoff: '' };
2975
+ for (const name of ['plan', 'decisions', 'handoff']) {
2976
+ if (typeof documentOverrides[name] === 'string')
2977
+ documents[name] = documentOverrides[name];
2978
+ }
2979
+ if (body.contextDigest !== digestPortableWorkroomContext(preview.bundle, documents)) {
2980
+ return c.json({ error: 'The exact selected context changed after review. Refresh the sharing preview and confirm it again.' }, 409);
2981
+ }
2982
+ const status = await workroomManager.startHost({
2983
+ ...preview,
2984
+ documents,
2985
+ name: String(body.name ?? '').trim() || `${preview.bundle.feature.id} workroom`,
2986
+ address: String(body.address ?? ''),
2987
+ port: Number(body.port ?? 4242),
2988
+ password: String(body.password ?? ''),
2989
+ hostDisplayName: String(body.hostDisplayName ?? '').trim() || os.userInfo().username || 'Host',
2990
+ });
2991
+ establishWorkroomHumanSession(c);
2992
+ return c.json({ status }, 201);
2993
+ }
2994
+ catch (error) {
2995
+ return errorResponse(c, error);
2996
+ }
2997
+ });
2998
+ app.post('/api/workrooms/stop', async (c) => {
2999
+ try {
3000
+ const authority = workroomManager.assertHumanSession(getWorkroomHumanSessionToken(c));
3001
+ await workroomManager.stopOrLeave(authority);
3002
+ clearWorkroomHumanSession(c);
3003
+ return c.json({ success: true });
3004
+ }
3005
+ catch (error) {
3006
+ return errorResponse(c, error);
3007
+ }
3008
+ });
3009
+ app.post('/api/workrooms/invites', async (c) => {
3010
+ try {
3011
+ return c.json(await workroomManager.createInvite(), 201);
3012
+ }
3013
+ catch (error) {
3014
+ return errorResponse(c, error);
3015
+ }
3016
+ });
3017
+ app.post('/api/workrooms/join', async (c) => {
3018
+ try {
3019
+ const body = await c.req.json();
3020
+ let localWorkspaceId;
3021
+ if (typeof body.workspaceId === 'string' && body.workspaceId) {
3022
+ const config = await loadConfig();
3023
+ const workspacePath = await resolveExactWorkspaceById(config.workspacesDir, body.workspaceId);
3024
+ if (!workspacePath)
3025
+ return c.json({ error: 'Select an existing local workspace for the Workroom mapping.' }, 404);
3026
+ localWorkspaceId = body.workspaceId;
3027
+ }
3028
+ const status = await workroomManager.join(String(body.invite ?? ''), String(body.password ?? ''), String(body.displayName ?? '').trim() || os.userInfo().username || 'Developer', localWorkspaceId);
3029
+ establishWorkroomHumanSession(c);
3030
+ return c.json({ status }, 202);
3031
+ }
3032
+ catch (error) {
3033
+ return errorResponse(c, error);
3034
+ }
3035
+ });
3036
+ app.post('/api/workrooms/join/poll', async (c) => {
3037
+ try {
3038
+ return c.json({ status: await workroomManager.pollJoin() });
3039
+ }
3040
+ catch (error) {
3041
+ return errorResponse(c, error);
3042
+ }
3043
+ });
3044
+ app.get('/api/workrooms/snapshot', async (c) => {
3045
+ try {
3046
+ return c.json({ snapshot: await workroomManager.snapshot() });
3047
+ }
3048
+ catch (error) {
3049
+ return errorResponse(c, error);
3050
+ }
3051
+ });
3052
+ app.put('/api/workrooms/documents/:name', async (c) => {
3053
+ try {
3054
+ const name = documentNameSchema.parse(c.req.param('name'));
3055
+ const body = await c.req.json();
3056
+ if (typeof body.content !== 'string' || !Number.isInteger(body.expectedRevision)) {
3057
+ return c.json({ error: 'content and an integer expectedRevision are required.' }, 400);
3058
+ }
3059
+ return c.json({ document: await workroomManager.updateDocument(name, body.content, body.expectedRevision) });
3060
+ }
3061
+ catch (error) {
3062
+ return errorResponse(c, error);
3063
+ }
3064
+ });
3065
+ app.post('/api/workrooms/handoff', async (c) => {
3066
+ try {
3067
+ const body = await c.req.json();
3068
+ const workspaceId = typeof body.workspaceId === 'string' ? body.workspaceId : '';
3069
+ const config = await loadConfig();
3070
+ const workspacePath = await resolveExactWorkspaceById(config.workspacesDir, workspaceId);
3071
+ if (!workspacePath)
3072
+ return c.json({ error: 'Select a local workspace before publishing a handoff.' }, 404);
3073
+ return c.json({ document: await workroomManager.publishHandoff(workspacePath) });
3074
+ }
3075
+ catch (error) {
3076
+ return errorResponse(c, error);
3077
+ }
3078
+ });
3079
+ app.post('/api/workrooms/members/:requestId/decision', async (c) => {
3080
+ try {
3081
+ const body = await c.req.json();
3082
+ if (typeof body.accept !== 'boolean')
3083
+ return c.json({ error: 'accept must be a boolean.' }, 400);
3084
+ await workroomManager.decideJoin(c.req.param('requestId'), body.accept);
3085
+ return c.json({ success: true });
3086
+ }
3087
+ catch (error) {
3088
+ return errorResponse(c, error);
3089
+ }
3090
+ });
3091
+ app.put('/api/workrooms/members/:memberId/role', async (c) => {
3092
+ try {
3093
+ const body = await c.req.json();
3094
+ if (body.role !== 'publisher' && body.role !== 'member')
3095
+ return c.json({ error: 'role must be publisher or member.' }, 400);
3096
+ await workroomManager.setRole(c.req.param('memberId'), body.role);
3097
+ return c.json({ success: true });
3098
+ }
3099
+ catch (error) {
3100
+ return errorResponse(c, error);
3101
+ }
3102
+ });
3103
+ app.delete('/api/workrooms/members/:memberId', async (c) => {
3104
+ try {
3105
+ await workroomManager.revokeMember(c.req.param('memberId'));
3106
+ return c.json({ success: true });
3107
+ }
3108
+ catch (error) {
3109
+ return errorResponse(c, error);
3110
+ }
3111
+ });
3112
+ app.post('/api/workrooms/password/rotate', async (c) => {
3113
+ try {
3114
+ const body = await c.req.json();
3115
+ await workroomManager.rotatePassword(String(body.password ?? ''), body.revokeDevices !== false);
3116
+ return c.json({ success: true });
3117
+ }
3118
+ catch (error) {
3119
+ return errorResponse(c, error);
3120
+ }
3121
+ });
3122
+ app.get('/api/workrooms/local-resources', async (c) => {
3123
+ try {
3124
+ return c.json({ resources: await workroomManager.listLocalResources() });
3125
+ }
3126
+ catch (error) {
3127
+ return errorResponse(c, error);
3128
+ }
3129
+ });
3130
+ app.post('/api/workrooms/resources/publish', async (c) => {
3131
+ try {
3132
+ const body = await c.req.json();
3133
+ if (!['skill', 'agent', 'workflow'].includes(body.kind) || typeof body.id !== 'string' || typeof body.version !== 'string') {
3134
+ return c.json({ error: 'kind, id, and semantic version are required.' }, 400);
3135
+ }
3136
+ return c.json({ package: await workroomManager.publishLocalResource(body.kind, body.id, body.version) }, 201);
3137
+ }
3138
+ catch (error) {
3139
+ return errorResponse(c, error);
3140
+ }
3141
+ });
3142
+ app.post('/api/workrooms/resources/:digest/download', async (c) => {
3143
+ try {
3144
+ return c.json(await workroomManager.downloadResource(c.req.param('digest')));
3145
+ }
3146
+ catch (error) {
3147
+ return errorResponse(c, error);
3148
+ }
3149
+ });
3150
+ app.post('/api/workrooms/resources/:digest/apply', async (c) => {
3151
+ try {
3152
+ const body = await c.req.json();
3153
+ return c.json({ applied: await workroomManager.applyResource(c.req.param('digest'), String(body.approvedDigest ?? ''), String(body.approvedLocalDigest ?? '')) });
3154
+ }
3155
+ catch (error) {
3156
+ return errorResponse(c, error);
3157
+ }
3158
+ });
3159
+ app.post('/api/workrooms/resources/:digest/quarantine', async (c) => {
3160
+ try {
3161
+ await workroomManager.quarantineResource(c.req.param('digest'));
3162
+ return c.json({ success: true });
3163
+ }
3164
+ catch (error) {
3165
+ return errorResponse(c, error);
3166
+ }
3167
+ });
3168
+ app.post('/api/workrooms/resources/:digest/purge', async (c) => {
3169
+ try {
3170
+ const body = await c.req.json();
3171
+ if (body.confirmDigest !== c.req.param('digest')) {
3172
+ return c.json({ error: 'Confirm the exact quarantined resource digest before purging it.' }, 400);
3173
+ }
3174
+ await workroomManager.purgeResource(c.req.param('digest'));
3175
+ return c.json({ success: true });
3176
+ }
3177
+ catch (error) {
3178
+ return errorResponse(c, error);
3179
+ }
3180
+ });
3181
+ app.post('/api/workrooms/workflow/select', async (c) => {
3182
+ try {
3183
+ const body = await c.req.json();
3184
+ const steps = Array.isArray(body.steps)
3185
+ ? body.steps.map((step) => workflowStepSchema.parse(step))
3186
+ : undefined;
3187
+ if (typeof body.workflowId !== 'string' || typeof body.version !== 'string' || !steps
3188
+ || !Number.isInteger(body.expectedRevision) || body.expectedRevision < 0) {
3189
+ return c.json({ error: 'workflowId, version, steps, and a nonnegative integer expectedRevision are required.' }, 400);
3190
+ }
3191
+ await workroomManager.selectLocalWorkflow(body.workflowId, body.version, steps, body.expectedRevision);
3192
+ return c.json({ success: true });
3193
+ }
3194
+ catch (error) {
3195
+ return errorResponse(c, error);
3196
+ }
3197
+ });
3198
+ app.post('/api/workrooms/workflow/steps/:stepId/transition', async (c) => {
3199
+ try {
3200
+ const body = await c.req.json();
3201
+ const allowed = ['pending', 'in_progress', 'completion_proposed', 'completed', 'skipped'];
3202
+ if (!allowed.includes(body.status) || !Number.isInteger(body.expectedRevision)) {
3203
+ return c.json({ error: 'A valid status and integer expectedRevision are required.' }, 400);
3204
+ }
3205
+ return c.json({ step: await workroomManager.transitionWorkflowStep(c.req.param('stepId'), body.status, body.expectedRevision, typeof body.evidence === 'string' ? body.evidence : undefined) });
3206
+ }
3207
+ catch (error) {
3208
+ return errorResponse(c, error);
3209
+ }
3210
+ });
3211
+ app.post('/api/workrooms/export', async (c) => {
3212
+ try {
3213
+ const body = await c.req.json();
3214
+ return c.json({ export: await workroomManager.exportRoom(String(body.passphrase ?? '')) });
3215
+ }
3216
+ catch (error) {
3217
+ return errorResponse(c, error);
3218
+ }
3219
+ });
3220
+ app.post('/api/workrooms/import', async (c) => {
3221
+ try {
3222
+ const body = await c.req.json();
3223
+ const status = await workroomManager.importRoom(body.export, String(body.exportPassphrase ?? ''), {
3224
+ name: typeof body.name === 'string' ? body.name : undefined,
3225
+ address: String(body.address ?? ''),
3226
+ port: Number(body.port ?? 4242),
3227
+ password: String(body.password ?? ''),
3228
+ hostDisplayName: String(body.hostDisplayName ?? '').trim() || os.userInfo().username || 'Host',
3229
+ });
3230
+ establishWorkroomHumanSession(c);
3231
+ return c.json({ status }, 201);
3232
+ }
3233
+ catch (error) {
3234
+ return errorResponse(c, error);
3235
+ }
3236
+ });
2402
3237
  // ─── Schedules: recurring workspace jobs (sync/refresh) ─────────────────
2403
3238
  // List all scheduled jobs (with computed next-due time)
2404
3239
  app.get('/api/schedules', async (c) => {
@@ -2500,6 +3335,11 @@ export function startServer(port = 3000, opts = {}) {
2500
3335
  resolve({ port: info.port, server });
2501
3336
  });
2502
3337
  injectWebSocket(server);
3338
+ server.on('close', () => {
3339
+ void workroomManager.stopOrLeave().catch((error) => {
3340
+ console.error('[workroom] failed to stop with the dashboard:', error instanceof Error ? error.message : String(error));
3341
+ });
3342
+ });
2503
3343
  server.on('error', (e) => {
2504
3344
  if (e.code === 'EADDRINUSE') {
2505
3345
  // Callers that own their backend (desktop/extension) pass strictPort so