@mrpatronz/nexusflow 2.9.0 → 2.10.1

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 (744) hide show
  1. package/README.md +177 -113
  2. package/desktop/assets/icon.png +0 -0
  3. package/dist/agent/AcpCliAdapter.d.ts +2 -0
  4. package/dist/agent/AcpCliAdapter.d.ts.map +1 -1
  5. package/dist/agent/AcpCliAdapter.js +50 -4
  6. package/dist/agent/AcpCliAdapter.js.map +1 -1
  7. package/dist/agent/AntigravityCliAdapter.d.ts +44 -2
  8. package/dist/agent/AntigravityCliAdapter.d.ts.map +1 -1
  9. package/dist/agent/AntigravityCliAdapter.js +363 -16
  10. package/dist/agent/AntigravityCliAdapter.js.map +1 -1
  11. package/dist/agent/ClaudeCliAdapter.d.ts +5 -2
  12. package/dist/agent/ClaudeCliAdapter.d.ts.map +1 -1
  13. package/dist/agent/ClaudeCliAdapter.js +43 -14
  14. package/dist/agent/ClaudeCliAdapter.js.map +1 -1
  15. package/dist/agent/ClaudeSdkAdapter.d.ts +1 -0
  16. package/dist/agent/ClaudeSdkAdapter.d.ts.map +1 -1
  17. package/dist/agent/ClaudeSdkAdapter.js +38 -11
  18. package/dist/agent/ClaudeSdkAdapter.js.map +1 -1
  19. package/dist/agent/CliAdapterBase.d.ts +2 -0
  20. package/dist/agent/CliAdapterBase.d.ts.map +1 -1
  21. package/dist/agent/CliAdapterBase.js +12 -1
  22. package/dist/agent/CliAdapterBase.js.map +1 -1
  23. package/dist/agent/CodexCliAdapter.d.ts +1 -1
  24. package/dist/agent/CodexCliAdapter.d.ts.map +1 -1
  25. package/dist/agent/CodexCliAdapter.js +5 -4
  26. package/dist/agent/CodexCliAdapter.js.map +1 -1
  27. package/dist/agent/CodexSdkAdapter.d.ts.map +1 -1
  28. package/dist/agent/CodexSdkAdapter.js +4 -1
  29. package/dist/agent/CodexSdkAdapter.js.map +1 -1
  30. package/dist/agent/CopilotAcpAdapter.d.ts.map +1 -1
  31. package/dist/agent/CopilotAcpAdapter.js +2 -2
  32. package/dist/agent/CopilotAcpAdapter.js.map +1 -1
  33. package/dist/agent/ProviderRegistry.d.ts +8 -1
  34. package/dist/agent/ProviderRegistry.d.ts.map +1 -1
  35. package/dist/agent/ProviderRegistry.js.map +1 -1
  36. package/dist/agent/TurnSessionManager.d.ts +65 -0
  37. package/dist/agent/TurnSessionManager.d.ts.map +1 -0
  38. package/dist/agent/TurnSessionManager.js +330 -0
  39. package/dist/agent/TurnSessionManager.js.map +1 -0
  40. package/dist/agent/adapters.d.ts.map +1 -1
  41. package/dist/agent/adapters.js +23 -6
  42. package/dist/agent/adapters.js.map +1 -1
  43. package/dist/agent/cliAvailability.d.ts +2 -5
  44. package/dist/agent/cliAvailability.d.ts.map +1 -1
  45. package/dist/agent/cliAvailability.js +18 -41
  46. package/dist/agent/cliAvailability.js.map +1 -1
  47. package/dist/agent/mcp-config.d.ts +0 -5
  48. package/dist/agent/mcp-config.d.ts.map +1 -1
  49. package/dist/agent/mcp-config.js +2 -1
  50. package/dist/agent/mcp-config.js.map +1 -1
  51. package/dist/agent/models.d.ts +13 -0
  52. package/dist/agent/models.d.ts.map +1 -1
  53. package/dist/agent/models.js +262 -8
  54. package/dist/agent/models.js.map +1 -1
  55. package/dist/agent/nativeTools.d.ts.map +1 -1
  56. package/dist/agent/nativeTools.js +1 -0
  57. package/dist/agent/nativeTools.js.map +1 -1
  58. package/dist/agent/orchestrator.d.ts.map +1 -1
  59. package/dist/agent/orchestrator.js +3 -2
  60. package/dist/agent/orchestrator.js.map +1 -1
  61. package/dist/agent/session.d.ts +6 -0
  62. package/dist/agent/session.d.ts.map +1 -1
  63. package/dist/agent/session.js +8 -0
  64. package/dist/agent/session.js.map +1 -1
  65. package/dist/analyzers/detect-deps.js +16 -1
  66. package/dist/analyzers/detect-deps.js.map +1 -1
  67. package/dist/analyzers/run-analyzer.d.ts.map +1 -1
  68. package/dist/analyzers/run-analyzer.js.map +1 -1
  69. package/dist/commands/adapter.d.ts.map +1 -1
  70. package/dist/commands/adapter.js +15 -14
  71. package/dist/commands/adapter.js.map +1 -1
  72. package/dist/commands/add-repo.d.ts.map +1 -1
  73. package/dist/commands/add-repo.js +6 -5
  74. package/dist/commands/add-repo.js.map +1 -1
  75. package/dist/commands/commit.js +6 -8
  76. package/dist/commands/commit.js.map +1 -1
  77. package/dist/commands/config.d.ts.map +1 -1
  78. package/dist/commands/config.js +2 -1
  79. package/dist/commands/config.js.map +1 -1
  80. package/dist/commands/create.d.ts +4 -4
  81. package/dist/commands/create.d.ts.map +1 -1
  82. package/dist/commands/create.js +48 -7
  83. package/dist/commands/create.js.map +1 -1
  84. package/dist/commands/desktop.d.ts +46 -3
  85. package/dist/commands/desktop.d.ts.map +1 -1
  86. package/dist/commands/desktop.js +343 -33
  87. package/dist/commands/desktop.js.map +1 -1
  88. package/dist/commands/diff.d.ts.map +1 -1
  89. package/dist/commands/diff.js +3 -2
  90. package/dist/commands/diff.js.map +1 -1
  91. package/dist/commands/doctor.d.ts.map +1 -1
  92. package/dist/commands/doctor.js +3 -2
  93. package/dist/commands/doctor.js.map +1 -1
  94. package/dist/commands/finish.js +2 -1
  95. package/dist/commands/finish.js.map +1 -1
  96. package/dist/commands/handoff.d.ts.map +1 -1
  97. package/dist/commands/handoff.js +12 -12
  98. package/dist/commands/handoff.js.map +1 -1
  99. package/dist/commands/init.d.ts +2 -2
  100. package/dist/commands/init.d.ts.map +1 -1
  101. package/dist/commands/init.js +8 -7
  102. package/dist/commands/init.js.map +1 -1
  103. package/dist/commands/isolate.d.ts.map +1 -1
  104. package/dist/commands/isolate.js +2 -1
  105. package/dist/commands/isolate.js.map +1 -1
  106. package/dist/commands/knowledge.d.ts.map +1 -1
  107. package/dist/commands/knowledge.js +3 -2
  108. package/dist/commands/knowledge.js.map +1 -1
  109. package/dist/commands/list.d.ts.map +1 -1
  110. package/dist/commands/list.js +3 -2
  111. package/dist/commands/list.js.map +1 -1
  112. package/dist/commands/logs.js +3 -3
  113. package/dist/commands/logs.js.map +1 -1
  114. package/dist/commands/mcp.d.ts +1 -2
  115. package/dist/commands/mcp.d.ts.map +1 -1
  116. package/dist/commands/mcp.js +19 -12
  117. package/dist/commands/mcp.js.map +1 -1
  118. package/dist/commands/migrate.d.ts +12 -0
  119. package/dist/commands/migrate.d.ts.map +1 -0
  120. package/dist/commands/migrate.js +128 -0
  121. package/dist/commands/migrate.js.map +1 -0
  122. package/dist/commands/open.d.ts.map +1 -1
  123. package/dist/commands/open.js +2 -1
  124. package/dist/commands/open.js.map +1 -1
  125. package/dist/commands/progress.d.ts.map +1 -1
  126. package/dist/commands/progress.js +2 -1
  127. package/dist/commands/progress.js.map +1 -1
  128. package/dist/commands/project.d.ts.map +1 -1
  129. package/dist/commands/project.js +7 -6
  130. package/dist/commands/project.js.map +1 -1
  131. package/dist/commands/refresh.d.ts.map +1 -1
  132. package/dist/commands/refresh.js +8 -11
  133. package/dist/commands/refresh.js.map +1 -1
  134. package/dist/commands/remove.d.ts.map +1 -1
  135. package/dist/commands/remove.js +2 -1
  136. package/dist/commands/remove.js.map +1 -1
  137. package/dist/commands/schedule.js +16 -18
  138. package/dist/commands/schedule.js.map +1 -1
  139. package/dist/commands/start.d.ts.map +1 -1
  140. package/dist/commands/start.js +9 -8
  141. package/dist/commands/start.js.map +1 -1
  142. package/dist/commands/status.d.ts.map +1 -1
  143. package/dist/commands/status.js +2 -1
  144. package/dist/commands/status.js.map +1 -1
  145. package/dist/commands/stop.d.ts.map +1 -1
  146. package/dist/commands/stop.js +2 -1
  147. package/dist/commands/stop.js.map +1 -1
  148. package/dist/commands/strategy.d.ts.map +1 -1
  149. package/dist/commands/strategy.js +6 -5
  150. package/dist/commands/strategy.js.map +1 -1
  151. package/dist/commands/sync.js +8 -10
  152. package/dist/commands/sync.js.map +1 -1
  153. package/dist/commands/tui.d.ts.map +1 -1
  154. package/dist/commands/tui.js +11 -10
  155. package/dist/commands/tui.js.map +1 -1
  156. package/dist/commands/ui.d.ts.map +1 -1
  157. package/dist/commands/ui.js +4 -3
  158. package/dist/commands/ui.js.map +1 -1
  159. package/dist/core/adapters/local-storage.d.ts +3 -1
  160. package/dist/core/adapters/local-storage.d.ts.map +1 -1
  161. package/dist/core/adapters/local-storage.js +62 -4
  162. package/dist/core/adapters/local-storage.js.map +1 -1
  163. package/dist/core/analysis-cache.d.ts.map +1 -1
  164. package/dist/core/analysis-cache.js +8 -7
  165. package/dist/core/analysis-cache.js.map +1 -1
  166. package/dist/core/brand-config.d.ts +261 -0
  167. package/dist/core/brand-config.d.ts.map +1 -0
  168. package/dist/core/brand-config.js +514 -0
  169. package/dist/core/brand-config.js.map +1 -0
  170. package/dist/core/config.d.ts +1 -1
  171. package/dist/core/config.d.ts.map +1 -1
  172. package/dist/core/config.js +18 -8
  173. package/dist/core/config.js.map +1 -1
  174. package/dist/core/constants.d.ts +88 -0
  175. package/dist/core/constants.d.ts.map +1 -0
  176. package/dist/core/constants.js +103 -0
  177. package/dist/core/constants.js.map +1 -0
  178. package/dist/core/doctor.d.ts.map +1 -1
  179. package/dist/core/doctor.js +11 -3
  180. package/dist/core/doctor.js.map +1 -1
  181. package/dist/core/generation-lock.d.ts +2 -2
  182. package/dist/core/generation-lock.d.ts.map +1 -1
  183. package/dist/core/generation-lock.js +49 -22
  184. package/dist/core/generation-lock.js.map +1 -1
  185. package/dist/core/knowledge.d.ts +19 -0
  186. package/dist/core/knowledge.d.ts.map +1 -1
  187. package/dist/core/knowledge.js +150 -24
  188. package/dist/core/knowledge.js.map +1 -1
  189. package/dist/core/migrate.d.ts +53 -0
  190. package/dist/core/migrate.d.ts.map +1 -0
  191. package/dist/core/migrate.js +607 -0
  192. package/dist/core/migrate.js.map +1 -0
  193. package/dist/core/new-repo.js +1 -1
  194. package/dist/core/new-repo.js.map +1 -1
  195. package/dist/core/projects.d.ts.map +1 -1
  196. package/dist/core/projects.js +13 -4
  197. package/dist/core/projects.js.map +1 -1
  198. package/dist/core/refresh.d.ts.map +1 -1
  199. package/dist/core/refresh.js +6 -4
  200. package/dist/core/refresh.js.map +1 -1
  201. package/dist/core/scheduler.d.ts +1 -1
  202. package/dist/core/scheduler.d.ts.map +1 -1
  203. package/dist/core/scheduler.js +11 -9
  204. package/dist/core/scheduler.js.map +1 -1
  205. package/dist/core/sync.js +1 -1
  206. package/dist/core/sync.js.map +1 -1
  207. package/dist/core/workspace-git.d.ts.map +1 -1
  208. package/dist/core/workspace-git.js +34 -10
  209. package/dist/core/workspace-git.js.map +1 -1
  210. package/dist/core/workspace-state.d.ts +4 -0
  211. package/dist/core/workspace-state.d.ts.map +1 -1
  212. package/dist/core/workspace-state.js +8 -8
  213. package/dist/core/workspace-state.js.map +1 -1
  214. package/dist/core/workspace.d.ts +13 -5
  215. package/dist/core/workspace.d.ts.map +1 -1
  216. package/dist/core/workspace.js +83 -34
  217. package/dist/core/workspace.js.map +1 -1
  218. package/dist/core/worktree.d.ts +10 -4
  219. package/dist/core/worktree.d.ts.map +1 -1
  220. package/dist/core/worktree.js +34 -47
  221. package/dist/core/worktree.js.map +1 -1
  222. package/dist/desktop-server.js +6 -3
  223. package/dist/desktop-server.js.map +1 -1
  224. package/dist/generators/base.d.ts.map +1 -1
  225. package/dist/generators/base.js +6 -5
  226. package/dist/generators/base.js.map +1 -1
  227. package/dist/generators/claude.d.ts.map +1 -1
  228. package/dist/generators/claude.js +2 -1
  229. package/dist/generators/claude.js.map +1 -1
  230. package/dist/generators/cursor.d.ts +1 -1
  231. package/dist/generators/cursor.d.ts.map +1 -1
  232. package/dist/generators/cursor.js +9 -4
  233. package/dist/generators/cursor.js.map +1 -1
  234. package/dist/generators/index.d.ts +4 -8
  235. package/dist/generators/index.d.ts.map +1 -1
  236. package/dist/generators/index.js +34 -69
  237. package/dist/generators/index.js.map +1 -1
  238. package/dist/generators/plan-generator.d.ts.map +1 -1
  239. package/dist/generators/plan-generator.js +20 -11
  240. package/dist/generators/plan-generator.js.map +1 -1
  241. package/dist/generators/skills-generator.d.ts.map +1 -1
  242. package/dist/generators/skills-generator.js +26 -11
  243. package/dist/generators/skills-generator.js.map +1 -1
  244. package/dist/gui/app_logo.png +0 -0
  245. package/dist/gui/assets/AgentsPage-DbivhmK0.js +3 -0
  246. package/dist/gui/assets/DashboardPage-D50Qrm3e.js +1 -0
  247. package/dist/gui/assets/GettingStartedPage-1MOTng3B.js +1 -0
  248. package/dist/gui/assets/NullStore-tdPen1kp.js +1 -0
  249. package/dist/gui/assets/ProjectsPage-srKgf7VN.js +1 -0
  250. package/dist/gui/assets/ScaffoldRepoInline-CP0yn5mw.js +1 -0
  251. package/dist/gui/assets/SettingsPage-DWmrLV8o.js +1 -0
  252. package/dist/gui/assets/SkillsPage-CKjTXdrd.js +6 -0
  253. package/dist/gui/assets/StartWorkPage-BCUnJ9k8.js +1 -0
  254. package/dist/gui/assets/{StrategiesPage-C36TZgDX.js → StrategiesPage-CdCbbIu-.js} +2 -2
  255. package/dist/gui/assets/WorkroomsPage-BcT-mGtT.js +4 -0
  256. package/dist/gui/assets/WorkspacesPage-CYFCm-7_.js +2 -0
  257. package/dist/gui/assets/alert-QnMXzwpU.js +1 -0
  258. package/dist/gui/assets/alert-dialog-u7qgb9Hq.js +1 -0
  259. package/dist/gui/assets/badge-DwXvB44H.js +1 -0
  260. package/dist/gui/assets/brand-hHByyyzq.js +1 -0
  261. package/dist/gui/assets/button-CREn1mup.js +1 -0
  262. package/dist/gui/assets/card-yrg6ov8B.js +1 -0
  263. package/dist/gui/assets/chevron-down-B_Mwbim8.js +1 -0
  264. package/dist/gui/assets/chunk-BV7QT456-DfUBKG-z.js +3 -0
  265. package/dist/gui/assets/{circle-plus-lmU1yNJd.js → circle-plus--zM3-3r3.js} +1 -1
  266. package/dist/gui/assets/{code-xml-jFAa3-TT.js → code-xml-DH-38Tt7.js} +1 -1
  267. package/dist/gui/assets/{git-branch-Cfj9D8yW.js → git-branch-Ce0I2663.js} +1 -1
  268. package/dist/gui/assets/index-3tD_IeyQ.js +55 -0
  269. package/dist/gui/assets/index-CK5YRfPV.css +2 -0
  270. package/dist/gui/assets/label-DC730X8s.js +1 -0
  271. package/dist/gui/assets/layers-j_brOuoa.js +1 -0
  272. package/dist/gui/assets/shield-check-Dd1Yv3l2.js +1 -0
  273. package/dist/gui/assets/square-check-big-BSIiYXPI.js +1 -0
  274. package/dist/gui/assets/tabs-BrHKp5bD.js +1 -0
  275. package/dist/gui/assets/{trash-2-CtR35_eF.js → trash-2-CMdv3eD3.js} +1 -1
  276. package/dist/gui/assets/useAriaLabelledBy-C4zV-1Ra.js +1 -0
  277. package/dist/gui/assets/{vsc-DFh0-53h.js → vsc-D80c4gBT.js} +1 -1
  278. package/dist/gui/favicon-aurora.svg +8 -0
  279. package/dist/gui/favicon.ico +0 -0
  280. package/dist/gui/favicon.svg +8 -1
  281. package/dist/gui/index.html +18 -12
  282. package/dist/harness/claude.d.ts.map +1 -1
  283. package/dist/harness/claude.js +97 -7
  284. package/dist/harness/claude.js.map +1 -1
  285. package/dist/harness/codex.d.ts +1 -0
  286. package/dist/harness/codex.d.ts.map +1 -1
  287. package/dist/harness/codex.js +51 -34
  288. package/dist/harness/codex.js.map +1 -1
  289. package/dist/index.js +57 -27
  290. package/dist/index.js.map +1 -1
  291. package/dist/mcp/server.d.ts +12 -1
  292. package/dist/mcp/server.d.ts.map +1 -1
  293. package/dist/mcp/server.js +24 -6
  294. package/dist/mcp/server.js.map +1 -1
  295. package/dist/mcp/tools.d.ts +4 -2
  296. package/dist/mcp/tools.d.ts.map +1 -1
  297. package/dist/mcp/tools.js +372 -17
  298. package/dist/mcp/tools.js.map +1 -1
  299. package/dist/orchestration/detect.d.ts.map +1 -1
  300. package/dist/orchestration/detect.js +12 -5
  301. package/dist/orchestration/detect.js.map +1 -1
  302. package/dist/orchestration/log-tail.js +17 -17
  303. package/dist/orchestration/log-tail.js.map +1 -1
  304. package/dist/orchestration/runner.d.ts +1 -1
  305. package/dist/orchestration/runner.d.ts.map +1 -1
  306. package/dist/orchestration/runner.js +41 -18
  307. package/dist/orchestration/runner.js.map +1 -1
  308. package/dist/resources/agents-catalog.d.ts +3 -1
  309. package/dist/resources/agents-catalog.d.ts.map +1 -1
  310. package/dist/resources/agents-catalog.js +12 -6
  311. package/dist/resources/agents-catalog.js.map +1 -1
  312. package/dist/resources/contracts.d.ts +7 -0
  313. package/dist/resources/contracts.d.ts.map +1 -1
  314. package/dist/resources/contracts.js +8 -1
  315. package/dist/resources/contracts.js.map +1 -1
  316. package/dist/resources/fs-safety.d.ts +7 -1
  317. package/dist/resources/fs-safety.d.ts.map +1 -1
  318. package/dist/resources/fs-safety.js +44 -1
  319. package/dist/resources/fs-safety.js.map +1 -1
  320. package/dist/resources/materializer.d.ts +1 -0
  321. package/dist/resources/materializer.d.ts.map +1 -1
  322. package/dist/resources/materializer.js +48 -17
  323. package/dist/resources/materializer.js.map +1 -1
  324. package/dist/resources/service.d.ts.map +1 -1
  325. package/dist/resources/service.js +3 -2
  326. package/dist/resources/service.js.map +1 -1
  327. package/dist/server.d.ts +2 -19
  328. package/dist/server.d.ts.map +1 -1
  329. package/dist/server.js +1294 -348
  330. package/dist/server.js.map +1 -1
  331. package/dist/static-resources/workflows/cost-aware-sol-luna.md +95 -0
  332. package/dist/storage/db.d.ts +94 -0
  333. package/dist/storage/db.d.ts.map +1 -0
  334. package/dist/storage/db.js +609 -0
  335. package/dist/storage/db.js.map +1 -0
  336. package/dist/types.d.ts +19 -0
  337. package/dist/types.d.ts.map +1 -1
  338. package/dist/utils/detect-ai.d.ts.map +1 -1
  339. package/dist/utils/detect-ai.js +1 -3
  340. package/dist/utils/detect-ai.js.map +1 -1
  341. package/dist/utils/multi-git.d.ts +0 -8
  342. package/dist/utils/multi-git.d.ts.map +1 -1
  343. package/dist/utils/multi-git.js +12 -4
  344. package/dist/utils/multi-git.js.map +1 -1
  345. package/dist/utils/pr.d.ts.map +1 -1
  346. package/dist/utils/pr.js +10 -5
  347. package/dist/utils/pr.js.map +1 -1
  348. package/dist/utils/repo-freshness.d.ts +106 -0
  349. package/dist/utils/repo-freshness.d.ts.map +1 -0
  350. package/dist/utils/repo-freshness.js +405 -0
  351. package/dist/utils/repo-freshness.js.map +1 -0
  352. package/dist/utils/resolve-workspace.d.ts +1 -1
  353. package/dist/utils/resolve-workspace.d.ts.map +1 -1
  354. package/dist/utils/resolve-workspace.js +10 -3
  355. package/dist/utils/resolve-workspace.js.map +1 -1
  356. package/dist/utils/session-finder.d.ts +12 -1
  357. package/dist/utils/session-finder.d.ts.map +1 -1
  358. package/dist/utils/session-finder.js +206 -8
  359. package/dist/utils/session-finder.js.map +1 -1
  360. package/dist/utils/skills-catalog.d.ts +5 -1
  361. package/dist/utils/skills-catalog.d.ts.map +1 -1
  362. package/dist/utils/skills-catalog.js +131 -75
  363. package/dist/utils/skills-catalog.js.map +1 -1
  364. package/dist/utils/terminal-launch.d.ts +8 -2
  365. package/dist/utils/terminal-launch.d.ts.map +1 -1
  366. package/dist/utils/terminal-launch.js +88 -19
  367. package/dist/utils/terminal-launch.js.map +1 -1
  368. package/dist/utils/update-check.d.ts +4 -1
  369. package/dist/utils/update-check.d.ts.map +1 -1
  370. package/dist/utils/update-check.js +66 -19
  371. package/dist/utils/update-check.js.map +1 -1
  372. package/dist/utils/user-paths.d.ts +23 -0
  373. package/dist/utils/user-paths.d.ts.map +1 -0
  374. package/dist/utils/user-paths.js +126 -0
  375. package/dist/utils/user-paths.js.map +1 -0
  376. package/dist/utils/workflows.d.ts +4 -1
  377. package/dist/utils/workflows.d.ts.map +1 -1
  378. package/dist/utils/workflows.js +51 -21
  379. package/dist/utils/workflows.js.map +1 -1
  380. package/dist/workrooms/client.d.ts +54 -0
  381. package/dist/workrooms/client.d.ts.map +1 -0
  382. package/dist/workrooms/client.js +191 -0
  383. package/dist/workrooms/client.js.map +1 -0
  384. package/dist/workrooms/contracts.d.ts +579 -0
  385. package/dist/workrooms/contracts.d.ts.map +1 -0
  386. package/dist/workrooms/contracts.js +231 -0
  387. package/dist/workrooms/contracts.js.map +1 -0
  388. package/dist/workrooms/crypto.d.ts +30 -0
  389. package/dist/workrooms/crypto.d.ts.map +1 -0
  390. package/dist/workrooms/crypto.js +72 -0
  391. package/dist/workrooms/crypto.js.map +1 -0
  392. package/dist/workrooms/host.d.ts +61 -0
  393. package/dist/workrooms/host.d.ts.map +1 -0
  394. package/dist/workrooms/host.js +747 -0
  395. package/dist/workrooms/host.js.map +1 -0
  396. package/dist/workrooms/local-resources.d.ts +26 -0
  397. package/dist/workrooms/local-resources.d.ts.map +1 -0
  398. package/dist/workrooms/local-resources.js +367 -0
  399. package/dist/workrooms/local-resources.js.map +1 -0
  400. package/dist/workrooms/manager.d.ts +228 -0
  401. package/dist/workrooms/manager.d.ts.map +1 -0
  402. package/dist/workrooms/manager.js +768 -0
  403. package/dist/workrooms/manager.js.map +1 -0
  404. package/dist/workrooms/milestones.d.ts +42 -0
  405. package/dist/workrooms/milestones.d.ts.map +1 -0
  406. package/dist/workrooms/milestones.js +94 -0
  407. package/dist/workrooms/milestones.js.map +1 -0
  408. package/dist/workrooms/portable.d.ts +21 -0
  409. package/dist/workrooms/portable.d.ts.map +1 -0
  410. package/dist/workrooms/portable.js +201 -0
  411. package/dist/workrooms/portable.js.map +1 -0
  412. package/dist/workrooms/resource-package.d.ts +7 -0
  413. package/dist/workrooms/resource-package.d.ts.map +1 -0
  414. package/dist/workrooms/resource-package.js +260 -0
  415. package/dist/workrooms/resource-package.js.map +1 -0
  416. package/dist/workrooms/service.d.ts +92 -0
  417. package/dist/workrooms/service.d.ts.map +1 -0
  418. package/dist/workrooms/service.js +788 -0
  419. package/dist/workrooms/service.js.map +1 -0
  420. package/dist/workrooms/sqlite-store.d.ts +31 -0
  421. package/dist/workrooms/sqlite-store.d.ts.map +1 -0
  422. package/dist/workrooms/sqlite-store.js +511 -0
  423. package/dist/workrooms/sqlite-store.js.map +1 -0
  424. package/dist/workrooms/store.d.ts +542 -0
  425. package/dist/workrooms/store.d.ts.map +1 -0
  426. package/dist/workrooms/store.js +71 -0
  427. package/dist/workrooms/store.js.map +1 -0
  428. package/package.json +21 -11
  429. package/resources/workflows/cost-aware-sol-luna.md +95 -0
  430. package/dist/agent/AcpCliAdapter.test.d.ts +0 -2
  431. package/dist/agent/AcpCliAdapter.test.d.ts.map +0 -1
  432. package/dist/agent/AcpCliAdapter.test.js +0 -235
  433. package/dist/agent/AcpCliAdapter.test.js.map +0 -1
  434. package/dist/agent/AntigravityCliAdapter.test.d.ts +0 -2
  435. package/dist/agent/AntigravityCliAdapter.test.d.ts.map +0 -1
  436. package/dist/agent/AntigravityCliAdapter.test.js +0 -222
  437. package/dist/agent/AntigravityCliAdapter.test.js.map +0 -1
  438. package/dist/agent/ClaudeCliAdapter.test.d.ts +0 -2
  439. package/dist/agent/ClaudeCliAdapter.test.d.ts.map +0 -1
  440. package/dist/agent/ClaudeCliAdapter.test.js +0 -320
  441. package/dist/agent/ClaudeCliAdapter.test.js.map +0 -1
  442. package/dist/agent/ClaudeSdkAdapter.test.d.ts +0 -2
  443. package/dist/agent/ClaudeSdkAdapter.test.d.ts.map +0 -1
  444. package/dist/agent/ClaudeSdkAdapter.test.js +0 -155
  445. package/dist/agent/ClaudeSdkAdapter.test.js.map +0 -1
  446. package/dist/agent/CliAdapterBase.test.d.ts +0 -2
  447. package/dist/agent/CliAdapterBase.test.d.ts.map +0 -1
  448. package/dist/agent/CliAdapterBase.test.js +0 -240
  449. package/dist/agent/CliAdapterBase.test.js.map +0 -1
  450. package/dist/agent/CodexCliAdapter.test.d.ts +0 -2
  451. package/dist/agent/CodexCliAdapter.test.d.ts.map +0 -1
  452. package/dist/agent/CodexCliAdapter.test.js +0 -331
  453. package/dist/agent/CodexCliAdapter.test.js.map +0 -1
  454. package/dist/agent/CodexSdkAdapter.test.d.ts +0 -2
  455. package/dist/agent/CodexSdkAdapter.test.d.ts.map +0 -1
  456. package/dist/agent/CodexSdkAdapter.test.js +0 -76
  457. package/dist/agent/CodexSdkAdapter.test.js.map +0 -1
  458. package/dist/agent/ProviderRegistry.test.d.ts +0 -2
  459. package/dist/agent/ProviderRegistry.test.d.ts.map +0 -1
  460. package/dist/agent/ProviderRegistry.test.js +0 -129
  461. package/dist/agent/ProviderRegistry.test.js.map +0 -1
  462. package/dist/agent/adapters.test.d.ts +0 -2
  463. package/dist/agent/adapters.test.d.ts.map +0 -1
  464. package/dist/agent/adapters.test.js +0 -71
  465. package/dist/agent/adapters.test.js.map +0 -1
  466. package/dist/agent/cliAvailability.test.d.ts +0 -2
  467. package/dist/agent/cliAvailability.test.d.ts.map +0 -1
  468. package/dist/agent/cliAvailability.test.js +0 -217
  469. package/dist/agent/cliAvailability.test.js.map +0 -1
  470. package/dist/agent/mcp-config.test.d.ts +0 -2
  471. package/dist/agent/mcp-config.test.d.ts.map +0 -1
  472. package/dist/agent/mcp-config.test.js +0 -22
  473. package/dist/agent/mcp-config.test.js.map +0 -1
  474. package/dist/agent/models.test.d.ts +0 -2
  475. package/dist/agent/models.test.d.ts.map +0 -1
  476. package/dist/agent/models.test.js +0 -40
  477. package/dist/agent/models.test.js.map +0 -1
  478. package/dist/agent/nativeAgent.test.d.ts +0 -2
  479. package/dist/agent/nativeAgent.test.d.ts.map +0 -1
  480. package/dist/agent/nativeAgent.test.js +0 -35
  481. package/dist/agent/nativeAgent.test.js.map +0 -1
  482. package/dist/agent/nativeTools.test.d.ts +0 -2
  483. package/dist/agent/nativeTools.test.d.ts.map +0 -1
  484. package/dist/agent/nativeTools.test.js +0 -67
  485. package/dist/agent/nativeTools.test.js.map +0 -1
  486. package/dist/agent/orchestrator.test.d.ts +0 -2
  487. package/dist/agent/orchestrator.test.d.ts.map +0 -1
  488. package/dist/agent/orchestrator.test.js +0 -284
  489. package/dist/agent/orchestrator.test.js.map +0 -1
  490. package/dist/agent/session.test.d.ts +0 -2
  491. package/dist/agent/session.test.d.ts.map +0 -1
  492. package/dist/agent/session.test.js +0 -74
  493. package/dist/agent/session.test.js.map +0 -1
  494. package/dist/agent/smoke-chat.test.d.ts +0 -2
  495. package/dist/agent/smoke-chat.test.d.ts.map +0 -1
  496. package/dist/agent/smoke-chat.test.js +0 -166
  497. package/dist/agent/smoke-chat.test.js.map +0 -1
  498. package/dist/commands/add-repo.test.d.ts +0 -2
  499. package/dist/commands/add-repo.test.d.ts.map +0 -1
  500. package/dist/commands/add-repo.test.js +0 -30
  501. package/dist/commands/add-repo.test.js.map +0 -1
  502. package/dist/commands/commands.test.d.ts +0 -2
  503. package/dist/commands/commands.test.d.ts.map +0 -1
  504. package/dist/commands/commands.test.js +0 -156
  505. package/dist/commands/commands.test.js.map +0 -1
  506. package/dist/commands/commit.test.d.ts +0 -2
  507. package/dist/commands/commit.test.d.ts.map +0 -1
  508. package/dist/commands/commit.test.js +0 -54
  509. package/dist/commands/commit.test.js.map +0 -1
  510. package/dist/commands/isolate.test.d.ts +0 -2
  511. package/dist/commands/isolate.test.d.ts.map +0 -1
  512. package/dist/commands/isolate.test.js +0 -59
  513. package/dist/commands/isolate.test.js.map +0 -1
  514. package/dist/commands/knowledge.test.d.ts +0 -2
  515. package/dist/commands/knowledge.test.d.ts.map +0 -1
  516. package/dist/commands/knowledge.test.js +0 -90
  517. package/dist/commands/knowledge.test.js.map +0 -1
  518. package/dist/commands/status.test.d.ts +0 -2
  519. package/dist/commands/status.test.d.ts.map +0 -1
  520. package/dist/commands/status.test.js +0 -31
  521. package/dist/commands/status.test.js.map +0 -1
  522. package/dist/commands/ui.test.d.ts +0 -2
  523. package/dist/commands/ui.test.d.ts.map +0 -1
  524. package/dist/commands/ui.test.js +0 -58
  525. package/dist/commands/ui.test.js.map +0 -1
  526. package/dist/core/adapters/storage.test.d.ts +0 -2
  527. package/dist/core/adapters/storage.test.d.ts.map +0 -1
  528. package/dist/core/adapters/storage.test.js +0 -65
  529. package/dist/core/adapters/storage.test.js.map +0 -1
  530. package/dist/core/analysis-cache.test.d.ts +0 -2
  531. package/dist/core/analysis-cache.test.d.ts.map +0 -1
  532. package/dist/core/analysis-cache.test.js +0 -163
  533. package/dist/core/analysis-cache.test.js.map +0 -1
  534. package/dist/core/config.test.d.ts +0 -2
  535. package/dist/core/config.test.d.ts.map +0 -1
  536. package/dist/core/config.test.js +0 -90
  537. package/dist/core/config.test.js.map +0 -1
  538. package/dist/core/finish.test.d.ts +0 -2
  539. package/dist/core/finish.test.d.ts.map +0 -1
  540. package/dist/core/finish.test.js +0 -110
  541. package/dist/core/finish.test.js.map +0 -1
  542. package/dist/core/generation-lock.test.d.ts +0 -2
  543. package/dist/core/generation-lock.test.d.ts.map +0 -1
  544. package/dist/core/generation-lock.test.js +0 -134
  545. package/dist/core/generation-lock.test.js.map +0 -1
  546. package/dist/core/isolate.test.d.ts +0 -2
  547. package/dist/core/isolate.test.d.ts.map +0 -1
  548. package/dist/core/isolate.test.js +0 -320
  549. package/dist/core/isolate.test.js.map +0 -1
  550. package/dist/core/knowledge.test.d.ts +0 -2
  551. package/dist/core/knowledge.test.d.ts.map +0 -1
  552. package/dist/core/knowledge.test.js +0 -287
  553. package/dist/core/knowledge.test.js.map +0 -1
  554. package/dist/core/locks.test.d.ts +0 -2
  555. package/dist/core/locks.test.d.ts.map +0 -1
  556. package/dist/core/locks.test.js +0 -143
  557. package/dist/core/locks.test.js.map +0 -1
  558. package/dist/core/new-repo.test.d.ts +0 -2
  559. package/dist/core/new-repo.test.d.ts.map +0 -1
  560. package/dist/core/new-repo.test.js +0 -78
  561. package/dist/core/new-repo.test.js.map +0 -1
  562. package/dist/core/progress.test.d.ts +0 -2
  563. package/dist/core/progress.test.d.ts.map +0 -1
  564. package/dist/core/progress.test.js +0 -52
  565. package/dist/core/progress.test.js.map +0 -1
  566. package/dist/core/projects.test.d.ts +0 -2
  567. package/dist/core/projects.test.d.ts.map +0 -1
  568. package/dist/core/projects.test.js +0 -165
  569. package/dist/core/projects.test.js.map +0 -1
  570. package/dist/core/refresh.test.d.ts +0 -2
  571. package/dist/core/refresh.test.d.ts.map +0 -1
  572. package/dist/core/refresh.test.js +0 -93
  573. package/dist/core/refresh.test.js.map +0 -1
  574. package/dist/core/scanner.test.d.ts +0 -2
  575. package/dist/core/scanner.test.d.ts.map +0 -1
  576. package/dist/core/scanner.test.js +0 -53
  577. package/dist/core/scanner.test.js.map +0 -1
  578. package/dist/core/scheduler.test.d.ts +0 -2
  579. package/dist/core/scheduler.test.d.ts.map +0 -1
  580. package/dist/core/scheduler.test.js +0 -169
  581. package/dist/core/scheduler.test.js.map +0 -1
  582. package/dist/core/sync.test.d.ts +0 -2
  583. package/dist/core/sync.test.d.ts.map +0 -1
  584. package/dist/core/sync.test.js +0 -128
  585. package/dist/core/sync.test.js.map +0 -1
  586. package/dist/core/workspace-git.test.d.ts +0 -2
  587. package/dist/core/workspace-git.test.d.ts.map +0 -1
  588. package/dist/core/workspace-git.test.js +0 -52
  589. package/dist/core/workspace-git.test.js.map +0 -1
  590. package/dist/core/workspace-skills-workflow.integration.test.d.ts +0 -2
  591. package/dist/core/workspace-skills-workflow.integration.test.d.ts.map +0 -1
  592. package/dist/core/workspace-skills-workflow.integration.test.js +0 -177
  593. package/dist/core/workspace-skills-workflow.integration.test.js.map +0 -1
  594. package/dist/core/workspace-state.test.d.ts +0 -2
  595. package/dist/core/workspace-state.test.d.ts.map +0 -1
  596. package/dist/core/workspace-state.test.js +0 -84
  597. package/dist/core/workspace-state.test.js.map +0 -1
  598. package/dist/core/workspace.create.test.d.ts +0 -2
  599. package/dist/core/workspace.create.test.d.ts.map +0 -1
  600. package/dist/core/workspace.create.test.js +0 -224
  601. package/dist/core/workspace.create.test.js.map +0 -1
  602. package/dist/core/workspace.test.d.ts +0 -2
  603. package/dist/core/workspace.test.d.ts.map +0 -1
  604. package/dist/core/workspace.test.js +0 -53
  605. package/dist/core/workspace.test.js.map +0 -1
  606. package/dist/core/worktree.test.d.ts +0 -2
  607. package/dist/core/worktree.test.d.ts.map +0 -1
  608. package/dist/core/worktree.test.js +0 -153
  609. package/dist/core/worktree.test.js.map +0 -1
  610. package/dist/generators/base.test.d.ts +0 -2
  611. package/dist/generators/base.test.d.ts.map +0 -1
  612. package/dist/generators/base.test.js +0 -287
  613. package/dist/generators/base.test.js.map +0 -1
  614. package/dist/generators/knowledge-migration.test.d.ts +0 -2
  615. package/dist/generators/knowledge-migration.test.d.ts.map +0 -1
  616. package/dist/generators/knowledge-migration.test.js +0 -22
  617. package/dist/generators/knowledge-migration.test.js.map +0 -1
  618. package/dist/generators/plan-generator.test.d.ts +0 -2
  619. package/dist/generators/plan-generator.test.d.ts.map +0 -1
  620. package/dist/generators/plan-generator.test.js +0 -220
  621. package/dist/generators/plan-generator.test.js.map +0 -1
  622. package/dist/generators/skills-generator.test.d.ts +0 -2
  623. package/dist/generators/skills-generator.test.d.ts.map +0 -1
  624. package/dist/generators/skills-generator.test.js +0 -79
  625. package/dist/generators/skills-generator.test.js.map +0 -1
  626. package/dist/gui/assets/AgentsPage-B4fx43rd.js +0 -3
  627. package/dist/gui/assets/CSPContext-DBUu9tE0.js +0 -1
  628. package/dist/gui/assets/DashboardPage-DH52G8IG.js +0 -1
  629. package/dist/gui/assets/GettingStartedPage-BlTrQhvi.js +0 -1
  630. package/dist/gui/assets/ProjectsPage-BPVSLPXv.js +0 -1
  631. package/dist/gui/assets/ScaffoldRepoInline-ywcpq9PX.js +0 -1
  632. package/dist/gui/assets/SettingsPage-RQr8SalV.js +0 -1
  633. package/dist/gui/assets/SkillsPage-CYoSRrwx.js +0 -6
  634. package/dist/gui/assets/StartWorkPage-DBKK4lFi.js +0 -1
  635. package/dist/gui/assets/WorkspacesPage-DxEONvSq.js +0 -6
  636. package/dist/gui/assets/alert-dialog-CQAX2yRx.js +0 -1
  637. package/dist/gui/assets/badge-CAohJGj6.js +0 -1
  638. package/dist/gui/assets/bot-C2HWrAft.js +0 -1
  639. package/dist/gui/assets/button-BDDZnIHx.js +0 -1
  640. package/dist/gui/assets/card-BKXzd_5L.js +0 -1
  641. package/dist/gui/assets/chevron-down-qTL8HPug.js +0 -1
  642. package/dist/gui/assets/cpu-Cdwd84Jz.js +0 -1
  643. package/dist/gui/assets/empty-BdlgbifD.js +0 -1
  644. package/dist/gui/assets/index-BF6pre6G.css +0 -2
  645. package/dist/gui/assets/index-C_rwnBzF.js +0 -52
  646. package/dist/gui/assets/isElementDisabled-uy_EOitH.js +0 -1
  647. package/dist/gui/assets/label-CG5exjag.js +0 -1
  648. package/dist/gui/assets/square-DlIYnlSq.js +0 -1
  649. package/dist/gui/assets/textarea-ByfoCb54.js +0 -1
  650. package/dist/gui/assets/useAriaLabelledBy-ClesgjMe.js +0 -1
  651. package/dist/gui/assets/useValueChanged-DeYvGnKs.js +0 -1
  652. package/dist/gui/assets/zap-CWG_Lvki.js +0 -1
  653. package/dist/harness/contract.test.d.ts +0 -2
  654. package/dist/harness/contract.test.d.ts.map +0 -1
  655. package/dist/harness/contract.test.js +0 -426
  656. package/dist/harness/contract.test.js.map +0 -1
  657. package/dist/harness/harness.test.d.ts +0 -2
  658. package/dist/harness/harness.test.d.ts.map +0 -1
  659. package/dist/harness/harness.test.js +0 -215
  660. package/dist/harness/harness.test.js.map +0 -1
  661. package/dist/mcp/tools.test.d.ts +0 -2
  662. package/dist/mcp/tools.test.d.ts.map +0 -1
  663. package/dist/mcp/tools.test.js +0 -194
  664. package/dist/mcp/tools.test.js.map +0 -1
  665. package/dist/orchestration/log-tail.test.d.ts +0 -2
  666. package/dist/orchestration/log-tail.test.d.ts.map +0 -1
  667. package/dist/orchestration/log-tail.test.js +0 -72
  668. package/dist/orchestration/log-tail.test.js.map +0 -1
  669. package/dist/orchestration/orchestrator.test.d.ts +0 -2
  670. package/dist/orchestration/orchestrator.test.d.ts.map +0 -1
  671. package/dist/orchestration/orchestrator.test.js +0 -85
  672. package/dist/orchestration/orchestrator.test.js.map +0 -1
  673. package/dist/orchestration/runner.test.d.ts +0 -2
  674. package/dist/orchestration/runner.test.d.ts.map +0 -1
  675. package/dist/orchestration/runner.test.js +0 -233
  676. package/dist/orchestration/runner.test.js.map +0 -1
  677. package/dist/resources/agents-catalog.test.d.ts +0 -2
  678. package/dist/resources/agents-catalog.test.d.ts.map +0 -1
  679. package/dist/resources/agents-catalog.test.js +0 -50
  680. package/dist/resources/agents-catalog.test.js.map +0 -1
  681. package/dist/resources/codex-agent.test.d.ts +0 -2
  682. package/dist/resources/codex-agent.test.d.ts.map +0 -1
  683. package/dist/resources/codex-agent.test.js +0 -27
  684. package/dist/resources/codex-agent.test.js.map +0 -1
  685. package/dist/resources/materializer.test.d.ts +0 -2
  686. package/dist/resources/materializer.test.d.ts.map +0 -1
  687. package/dist/resources/materializer.test.js +0 -149
  688. package/dist/resources/materializer.test.js.map +0 -1
  689. package/dist/resources/service.test.d.ts +0 -2
  690. package/dist/resources/service.test.d.ts.map +0 -1
  691. package/dist/resources/service.test.js +0 -58
  692. package/dist/resources/service.test.js.map +0 -1
  693. package/dist/server.test.d.ts +0 -2
  694. package/dist/server.test.d.ts.map +0 -1
  695. package/dist/server.test.js +0 -1697
  696. package/dist/server.test.js.map +0 -1
  697. package/dist/utils/detect-ai.test.d.ts +0 -2
  698. package/dist/utils/detect-ai.test.d.ts.map +0 -1
  699. package/dist/utils/detect-ai.test.js +0 -70
  700. package/dist/utils/detect-ai.test.js.map +0 -1
  701. package/dist/utils/detect-editors.test.d.ts +0 -2
  702. package/dist/utils/detect-editors.test.d.ts.map +0 -1
  703. package/dist/utils/detect-editors.test.js +0 -85
  704. package/dist/utils/detect-editors.test.js.map +0 -1
  705. package/dist/utils/git.test.d.ts +0 -2
  706. package/dist/utils/git.test.d.ts.map +0 -1
  707. package/dist/utils/git.test.js +0 -125
  708. package/dist/utils/git.test.js.map +0 -1
  709. package/dist/utils/multi-git.test.d.ts +0 -2
  710. package/dist/utils/multi-git.test.d.ts.map +0 -1
  711. package/dist/utils/multi-git.test.js +0 -330
  712. package/dist/utils/multi-git.test.js.map +0 -1
  713. package/dist/utils/pr.test.d.ts +0 -2
  714. package/dist/utils/pr.test.d.ts.map +0 -1
  715. package/dist/utils/pr.test.js +0 -47
  716. package/dist/utils/pr.test.js.map +0 -1
  717. package/dist/utils/session-finder.test.d.ts +0 -2
  718. package/dist/utils/session-finder.test.d.ts.map +0 -1
  719. package/dist/utils/session-finder.test.js +0 -304
  720. package/dist/utils/session-finder.test.js.map +0 -1
  721. package/dist/utils/skills-catalog.test.d.ts +0 -2
  722. package/dist/utils/skills-catalog.test.d.ts.map +0 -1
  723. package/dist/utils/skills-catalog.test.js +0 -281
  724. package/dist/utils/skills-catalog.test.js.map +0 -1
  725. package/dist/utils/system-scanner.test.d.ts +0 -2
  726. package/dist/utils/system-scanner.test.d.ts.map +0 -1
  727. package/dist/utils/system-scanner.test.js +0 -73
  728. package/dist/utils/system-scanner.test.js.map +0 -1
  729. package/dist/utils/terminal-launch.test.d.ts +0 -2
  730. package/dist/utils/terminal-launch.test.d.ts.map +0 -1
  731. package/dist/utils/terminal-launch.test.js +0 -117
  732. package/dist/utils/terminal-launch.test.js.map +0 -1
  733. package/dist/utils/update-check.test.d.ts +0 -2
  734. package/dist/utils/update-check.test.d.ts.map +0 -1
  735. package/dist/utils/update-check.test.js +0 -30
  736. package/dist/utils/update-check.test.js.map +0 -1
  737. package/dist/utils/workflows.test.d.ts +0 -2
  738. package/dist/utils/workflows.test.d.ts.map +0 -1
  739. package/dist/utils/workflows.test.js +0 -86
  740. package/dist/utils/workflows.test.js.map +0 -1
  741. package/dist/utils/workspace-launch.test.d.ts +0 -2
  742. package/dist/utils/workspace-launch.test.d.ts.map +0 -1
  743. package/dist/utils/workspace-launch.test.js +0 -128
  744. 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';
@@ -26,6 +26,7 @@ import { createNewRepo, isValidProjectName } from './core/new-repo.js';
26
26
  import { loadProjects, createProject, updateProject, removeProject, slugifyProjectName } from './core/projects.js';
27
27
  import { getSessionCwd, isInPlace, resolveFeatureRepoPath } from './utils/feature.js';
28
28
  import { listBranches } from './utils/git.js';
29
+ import { checkRepoFreshness, checkReposFreshness, fastForwardRepos, } from './utils/repo-freshness.js';
29
30
  import { createWorkspace, listWorkspaces, loadFeatureConfig, loadWorkspaceManifest, deleteWorkspace, addRepoToWorkspace, isolateWorkspaceRepo } from './core/workspace.js';
30
31
  import { loadWorkspaceState } from './core/workspace-state.js';
31
32
  import { analyzeAllRepos } from './analyzers/index.js';
@@ -35,9 +36,10 @@ import { detectEditors } from './utils/detect-editors.js';
35
36
  import { buildWorkspaceLaunchPrompt, detectWorkspaceLaunchTargets, launchTargetIdForEditorCommand, launchWorkspaceTarget, } from './utils/workspace-launch.js';
36
37
  import { isBinaryOnPath, launchWorkspaceTerminal, SUPPORTED_ASSISTANTS } from './utils/terminal-launch.js';
37
38
  import { openInEditor } from './utils/open-editor.js';
38
- import { canOpenCodexSessionInWorkspace, canTransferClaudeSessionInWorkspace, findSessions, getSessionTranscript, } from './utils/session-finder.js';
39
+ import { canOpenCodexSessionInWorkspace, canTransferClaudeSessionInWorkspace, findActiveAssistants, findSessions, getSessionTranscript, } from './utils/session-finder.js';
39
40
  import { ProviderRegistry } from './agent/adapters.js';
40
- import { isValidSessionUuid } from './agent/session.js';
41
+ import { isValidSessionId, isValidSessionUuid } from './agent/session.js';
42
+ import { defaultTurnSessionManager, AgentTurnGate, dispatchAgentInput } from './agent/TurnSessionManager.js';
41
43
  import { getRepoStatus } from './utils/multi-git.js';
42
44
  import { syncWorkspace } from './core/sync.js';
43
45
  import { commitWorkspace } from './core/commit.js';
@@ -54,6 +56,12 @@ import { deleteAgent, getAllAgents, importAgentToml, saveAgent, } from './resour
54
56
  import { ResourceConflictError } from './resources/materializer.js';
55
57
  import { ResourceSelectionError, validateResourceSelections, withResourceAdministrationLock, } from './resources/service.js';
56
58
  import { suggestWorkflow } from './utils/workflow-advisor.js';
59
+ import { WorkroomAuthorizationError, WorkroomRevisionError, WorkroomValidationError, documentNameSchema, workflowStepSchema, } from './workrooms/contracts.js';
60
+ import { listWorkroomNetworkInterfaces } from './workrooms/host.js';
61
+ import * as workroomManagerModule from './workrooms/manager.js';
62
+ const { workroomManager } = workroomManagerModule;
63
+ import { buildPortableWorkroomPreview, digestPortableWorkroomContext } from './workrooms/portable.js';
64
+ import { randomToken, tokenDigest } from './workrooms/crypto.js';
57
65
  // Resolve static files directory
58
66
  const __filename = fileURLToPath(import.meta.url);
59
67
  const __dirname = path.dirname(__filename);
@@ -73,34 +81,7 @@ function hasTrustedLocalOrigin(origin) {
73
81
  return false;
74
82
  }
75
83
  }
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
- }
84
+ export { dispatchAgentInput, AgentTurnGate };
104
85
  app.get('/ws', async (c, next) => {
105
86
  // Prevent Cross-Site WebSocket Hijacking (CSWSH)
106
87
  const origin = c.req.header('origin');
@@ -123,11 +104,11 @@ app.get('/ws', async (c, next) => {
123
104
  // | {type:'rejected', reason:'busy', message, turnId?}
124
105
  // | {type:'close', code} | {type:'pong'}
125
106
  return upgradeWebSocket((c) => {
126
- let agent = null;
127
- let activeProvider = null;
128
- const turnGate = new AgentTurnGate();
107
+ let currentCwd = null;
108
+ let currentWs = null;
129
109
  return {
130
110
  async onMessage(event, ws) {
111
+ currentWs = ws;
131
112
  if (typeof event.data === 'string') {
132
113
  if (event.data === 'ping') {
133
114
  ws.send(JSON.stringify({ type: 'pong' }));
@@ -139,35 +120,24 @@ app.get('/ws', async (c, next) => {
139
120
  ws.send(JSON.stringify({ type: 'pong' }));
140
121
  }
141
122
  else if (payload.type === 'start') {
142
- turnGate.settle();
143
- if (agent) {
144
- const previousAgent = agent;
145
- agent = null;
146
- activeProvider = null;
147
- previousAgent.stop();
123
+ const command = payload.command;
124
+ const cwd = payload.cwd;
125
+ if (typeof cwd !== 'string' || !cwd.trim()) {
126
+ ws.send(JSON.stringify({ type: 'error', message: 'Workspace directory cwd is required.' }));
127
+ return;
148
128
  }
149
- const provider = ProviderRegistry.getProvider(payload.command);
129
+ const provider = ProviderRegistry.getProvider(command);
150
130
  if (!provider) {
151
- ws.send(JSON.stringify({ type: 'error', message: `No provider found for ${payload.command}. Please create a dedicated adapter.` }));
131
+ ws.send(JSON.stringify({ type: 'error', message: `No provider found for ${command}. Please create a dedicated adapter.` }));
152
132
  return;
153
133
  }
154
134
  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);
135
+ const safeCwd = await resolveExactLaunchWorkspace(config.workspacesDir, cwd);
160
136
  if (!safeCwd) {
161
137
  ws.send(JSON.stringify({ type: 'error', message: 'Invalid or uncontained workspace directory.' }));
162
138
  return;
163
139
  }
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
- }
140
+ currentCwd = safeCwd;
171
141
  let session;
172
142
  const model = ProviderRegistry.resolveModel(provider, payload.model);
173
143
  if (model === null) {
@@ -177,120 +147,95 @@ app.get('/ws', async (c, next) => {
177
147
  }));
178
148
  return;
179
149
  }
150
+ const effort = typeof payload.effort === 'string' && payload.effort.trim() ? payload.effort.trim() : undefined;
180
151
  if (payload.sessionId !== undefined && payload.sessionId !== null) {
181
152
  if (!isValidSessionUuid(payload.sessionId)) {
182
153
  ws.send(JSON.stringify({ type: 'error', message: 'Invalid session id.' }));
183
154
  return;
184
155
  }
185
- session = { id: payload.sessionId, resume: Boolean(payload.resume), model };
156
+ session = { id: payload.sessionId, resume: Boolean(payload.resume), model, effort };
186
157
  }
187
- else if (model) {
188
- session = { id: crypto.randomUUID(), resume: false, model };
158
+ else if (model || effort) {
159
+ session = { id: crypto.randomUUID(), resume: false, model, effort };
189
160
  }
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 }));
161
+ await defaultTurnSessionManager.startSession({
162
+ workspaceCwd: safeCwd,
163
+ command,
164
+ client: ws,
165
+ provider,
166
+ session,
198
167
  });
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
168
  }
237
169
  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 }));
170
+ const cwd = typeof payload.cwd === 'string' ? payload.cwd : currentCwd;
171
+ if (!cwd) {
172
+ ws.send(JSON.stringify({
173
+ type: 'error',
174
+ message: 'No active agent session. Please start or reconnect the agent.',
175
+ }));
176
+ return;
177
+ }
178
+ const config = await loadConfig();
179
+ const safeCwd = await resolveExactLaunchWorkspace(config.workspacesDir, cwd);
180
+ if (!safeCwd) {
181
+ ws.send(JSON.stringify({ type: 'error', message: 'Invalid workspace directory.' }));
182
+ return;
183
+ }
184
+ currentCwd = safeCwd;
185
+ const existingSession = defaultTurnSessionManager.getSession(safeCwd);
186
+ if (!existingSession) {
187
+ const command = typeof payload.command === 'string' ? payload.command : 'antigravity-cli';
188
+ const provider = ProviderRegistry.getProvider(command);
189
+ if (!provider) {
190
+ ws.send(JSON.stringify({ type: 'error', message: `No provider found for ${command}.` }));
253
191
  return;
254
192
  }
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
- }
193
+ await defaultTurnSessionManager.startSession({
194
+ workspaceCwd: safeCwd,
195
+ command,
196
+ client: ws,
197
+ provider,
198
+ });
199
+ }
200
+ const result = defaultTurnSessionManager.dispatchInput(safeCwd, payload);
201
+ if (result.error) {
202
+ ws.send(JSON.stringify({ type: 'error', message: result.error }));
203
+ }
204
+ else if (result.rejected) {
205
+ const turnId = isValidSessionUuid(payload.turnId) ? payload.turnId : undefined;
206
+ ws.send(JSON.stringify({
207
+ type: 'rejected',
208
+ reason: 'busy',
209
+ message: 'The agent is still processing the current turn.',
210
+ ...(turnId ? { turnId } : {}),
211
+ }));
212
+ }
213
+ }
214
+ else if (payload.type === 'approval_response') {
215
+ if (currentCwd) {
216
+ const requestId = typeof payload.requestId === 'string' ? payload.requestId : '';
217
+ const decision = payload.decision === 'allow' ? 'allow' : 'deny';
218
+ const message = typeof payload.message === 'string' ? payload.message : undefined;
219
+ defaultTurnSessionManager.respondToApproval(currentCwd, requestId, decision, message);
266
220
  }
267
221
  }
268
222
  else if (payload.type === 'stop') {
269
- turnGate.settle();
270
- if (agent) {
271
- const stoppedAgent = agent;
272
- agent = null;
273
- activeProvider = null;
274
- stoppedAgent.stop();
223
+ if (currentCwd) {
224
+ defaultTurnSessionManager.stopSession(currentCwd);
275
225
  }
276
226
  }
277
227
  }
278
228
  catch (err) {
279
- console.error('[WS] Failed to parse message', err);
229
+ console.error('Error handling WebSocket message:', err);
280
230
  }
281
231
  }
282
232
  },
283
- onOpen(_event, _ws) {
284
- console.log('[WS] Client connected');
233
+ onOpen(_event, ws) {
234
+ currentWs = ws;
285
235
  },
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();
236
+ onClose(_event, ws) {
237
+ if (currentCwd && ws) {
238
+ defaultTurnSessionManager.unregisterClient(currentCwd, ws);
294
239
  }
295
240
  },
296
241
  };
@@ -314,9 +259,13 @@ export class PathAccessError extends Error {
314
259
  function assertWithin(baseDir, target) {
315
260
  const base = path.resolve(baseDir);
316
261
  const resolved = path.resolve(target);
262
+ const isWin = process.platform === 'win32';
263
+ const normBase = isWin ? base.toLowerCase() : base;
264
+ const normResolved = isWin ? resolved.toLowerCase() : resolved;
265
+ const sep = isWin ? path.sep.toLowerCase() : path.sep;
317
266
  // Trailing separator prevents a sibling like `feat-secret` from passing the
318
267
  // prefix test for base `feat`.
319
- if (resolved !== base && !resolved.startsWith(base + path.sep)) {
268
+ if (normResolved !== normBase && !normResolved.startsWith(normBase + (normBase.endsWith(sep) ? '' : sep))) {
320
269
  throw new PathAccessError();
321
270
  }
322
271
  return resolved;
@@ -371,7 +320,10 @@ async function resolveExactLaunchWorkspace(workspacesDir, candidatePath) {
371
320
  return null;
372
321
  try {
373
322
  const declaredWorkspace = await fs.realpath(path.resolve(feature.workspacePath));
374
- return path.resolve(declaredWorkspace) === path.resolve(safeWorkspacePath)
323
+ const isWin = process.platform === 'win32';
324
+ const normDeclared = isWin ? path.resolve(declaredWorkspace).toLowerCase() : path.resolve(declaredWorkspace);
325
+ const normSafe = isWin ? path.resolve(safeWorkspacePath).toLowerCase() : path.resolve(safeWorkspacePath);
326
+ return normDeclared === normSafe
375
327
  ? safeWorkspacePath
376
328
  : null;
377
329
  }
@@ -401,6 +353,19 @@ function errorResponse(c, error) {
401
353
  missingAgents: error.missingAgents,
402
354
  }, 400);
403
355
  }
356
+ if (error instanceof WorkroomAuthorizationError) {
357
+ return c.json({ error: error.message }, 401);
358
+ }
359
+ if (error instanceof WorkroomRevisionError) {
360
+ return c.json({
361
+ error: error.message,
362
+ expectedRevision: error.expected,
363
+ actualRevision: error.actual,
364
+ }, 409);
365
+ }
366
+ if (error instanceof WorkroomValidationError) {
367
+ return c.json({ error: error.message }, 400);
368
+ }
404
369
  const msg = error instanceof Error ? error.message : String(error);
405
370
  return c.json({ error: msg }, 500);
406
371
  }
@@ -431,35 +396,18 @@ async function findResourceAssignments(workspacesDir, resourceId, kind) {
431
396
  }
432
397
  return assignments;
433
398
  }
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
399
  // Enable CORS for the local GUI only. This server can spawn processes, run
456
400
  // package installs and delete worktrees, so a wildcard origin would let any
457
401
  // web page the developer has open drive it cross-origin.
458
402
  app.use('/api/*', cors({
459
- origin: (origin) => {
403
+ credentials: true,
404
+ origin: (origin, c) => {
460
405
  // No Origin header → same-origin or a non-browser client (CLI/desktop).
461
406
  if (!origin)
462
407
  return origin;
408
+ if (c.req.path.startsWith('/api/workrooms/')) {
409
+ return hasExactDashboardOrigin(origin, c.req.url) ? origin : null;
410
+ }
463
411
  try {
464
412
  const { hostname } = new URL(origin);
465
413
  const isLocal = hostname === 'localhost' ||
@@ -493,10 +441,18 @@ app.use('*', async (c, next) => {
493
441
  // cross-site request forgery and browser form posts from untrusted web pages.
494
442
  app.use('/api/*', async (c, next) => {
495
443
  if (['POST', 'PUT', 'DELETE', 'PATCH'].includes(c.req.method)) {
444
+ const secFetchSite = c.req.header('sec-fetch-site');
445
+ if (secFetchSite === 'cross-site') {
446
+ return c.json({ error: 'Forbidden cross-site request.' }, 403);
447
+ }
496
448
  const origin = c.req.header('origin');
497
449
  if (origin && !hasTrustedLocalOrigin(origin)) {
498
450
  return c.json({ error: 'Forbidden cross-origin request.' }, 403);
499
451
  }
452
+ const referer = c.req.header('referer');
453
+ if (referer && !hasTrustedLocalOrigin(referer)) {
454
+ return c.json({ error: 'Forbidden cross-origin referer.' }, 403);
455
+ }
500
456
  }
501
457
  await next();
502
458
  });
@@ -525,6 +481,116 @@ app.post('/api/adapters/status/refresh', async (c) => {
525
481
  }
526
482
  return c.json(ProviderRegistry.getAllStatus({ refreshProviderId: providerId }));
527
483
  });
484
+ app.post('/api/chat/upload-attachment', async (c) => {
485
+ if (!hasTrustedLocalOrigin(c.req.header('origin'))) {
486
+ return c.json({ error: 'A local browser origin is required.' }, 403);
487
+ }
488
+ try {
489
+ const body = await c.req.json().catch(() => null);
490
+ if (!body || typeof body.dataUrl !== 'string' || !body.dataUrl.startsWith('data:image/')) {
491
+ return c.json({ error: 'A valid image dataUrl is required.' }, 400);
492
+ }
493
+ const matches = body.dataUrl.match(/^data:image\/([a-zA-Z0-9+.-]+);base64,(.+)$/);
494
+ if (!matches) {
495
+ return c.json({ error: 'Malformed image dataUrl format.' }, 400);
496
+ }
497
+ const MAX_ATTACHMENT_BASE64_CHARS = Math.ceil((20 * 1024 * 1024 * 4) / 3);
498
+ if (matches[2].length > MAX_ATTACHMENT_BASE64_CHARS) {
499
+ return c.json({ error: 'Image attachment exceeds maximum allowed size of 20MB.' }, 413);
500
+ }
501
+ const rawExt = matches[1].toLowerCase();
502
+ const ext = rawExt === 'jpeg' ? 'jpg' : rawExt.replace(/[^a-zA-Z0-9]/g, '') || 'png';
503
+ const buffer = Buffer.from(matches[2], 'base64');
504
+ const imageId = crypto.randomUUID();
505
+ const config = await loadConfig();
506
+ const workspaceDir = typeof body.workspacePath === 'string'
507
+ ? await resolveExactLaunchWorkspace(config.workspacesDir, body.workspacePath)
508
+ : null;
509
+ const targetDir = workspaceDir
510
+ ? path.join(workspaceDir, '.nexusflow', 'attachments')
511
+ : path.join(getConfigDir(), 'attachments');
512
+ await fs.mkdir(targetDir, { recursive: true });
513
+ const targetFile = path.join(targetDir, `${imageId}.${ext}`);
514
+ await fs.writeFile(targetFile, buffer);
515
+ return c.json({
516
+ id: imageId,
517
+ filename: typeof body.filename === 'string' ? body.filename : `image-${imageId}.${ext}`,
518
+ path: targetFile,
519
+ size: buffer.length,
520
+ });
521
+ }
522
+ catch (error) {
523
+ return errorResponse(c, error);
524
+ }
525
+ });
526
+ app.get('/api/chat/thread/:workspaceId', async (c) => {
527
+ const workspaceId = c.req.param('workspaceId');
528
+ if (!workspaceId) {
529
+ return c.json({ error: 'workspaceId is required' }, 400);
530
+ }
531
+ try {
532
+ const thread = loadChatThread(workspaceId);
533
+ const config = await loadConfig().catch(() => null);
534
+ let isBusy = false;
535
+ if (config) {
536
+ try {
537
+ const workspacePath = resolveWorkspacePath(config.workspacesDir, workspaceId);
538
+ isBusy = defaultTurnSessionManager.hasActiveTurn(workspacePath);
539
+ }
540
+ catch {
541
+ // Workspace not found on disk or invalid
542
+ }
543
+ }
544
+ return c.json({ thread, isBusy });
545
+ }
546
+ catch (error) {
547
+ return errorResponse(c, error);
548
+ }
549
+ });
550
+ app.post('/api/chat/thread/:workspaceId', async (c) => {
551
+ if (!hasTrustedLocalOrigin(c.req.header('origin'))) {
552
+ return c.json({ error: 'A local browser origin is required.' }, 403);
553
+ }
554
+ const workspaceId = c.req.param('workspaceId');
555
+ if (!workspaceId) {
556
+ return c.json({ error: 'workspaceId is required' }, 400);
557
+ }
558
+ try {
559
+ const body = await c.req.json().catch(() => null);
560
+ if (!body || typeof body !== 'object') {
561
+ return c.json({ error: 'Valid chat thread payload is required.' }, 400);
562
+ }
563
+ saveChatThread({
564
+ workspaceId,
565
+ providerId: typeof body.providerId === 'string' ? body.providerId : null,
566
+ sessions: typeof body.sessions === 'object' && body.sessions !== null ? body.sessions : {},
567
+ profilesByProvider: typeof body.profilesByProvider === 'object' && body.profilesByProvider !== null ? body.profilesByProvider : {},
568
+ modelsByProvider: typeof body.modelsByProvider === 'object' && body.modelsByProvider !== null ? body.modelsByProvider : {},
569
+ effortsByProvider: typeof body.effortsByProvider === 'object' && body.effortsByProvider !== null ? body.effortsByProvider : {},
570
+ messages: Array.isArray(body.messages) ? body.messages : [],
571
+ });
572
+ return c.json({ success: true });
573
+ }
574
+ catch (error) {
575
+ return errorResponse(c, error);
576
+ }
577
+ });
578
+ app.delete('/api/chat/thread/:workspaceId', async (c) => {
579
+ if (!hasTrustedLocalOrigin(c.req.header('origin'))) {
580
+ return c.json({ error: 'A local browser origin is required.' }, 403);
581
+ }
582
+ const workspaceId = c.req.param('workspaceId');
583
+ if (!workspaceId) {
584
+ return c.json({ error: 'workspaceId is required' }, 400);
585
+ }
586
+ try {
587
+ clearChatThread(workspaceId);
588
+ return c.json({ success: true });
589
+ }
590
+ catch (error) {
591
+ return errorResponse(c, error);
592
+ }
593
+ });
528
594
  // 1. Get current configuration
529
595
  app.get('/api/config', async (c) => {
530
596
  try {
@@ -597,6 +663,80 @@ app.get('/api/repos/branches', async (c) => {
597
663
  return errorResponse(c, error);
598
664
  }
599
665
  });
666
+ // 3a.1. Base repository freshness inspection
667
+ app.get('/api/repos/freshness', async (c) => {
668
+ try {
669
+ const repoPath = c.req.query('path');
670
+ if (!repoPath) {
671
+ return c.json({ error: 'Missing "path" query parameter' }, 400);
672
+ }
673
+ const branch = c.req.query('branch') || undefined;
674
+ const fetchParam = c.req.query('fetch');
675
+ const config = await loadConfig();
676
+ const resolved = assertWithin(config.devDir, repoPath);
677
+ const freshness = await checkRepoFreshness(resolved, branch, {
678
+ fetch: fetchParam !== 'false' && fetchParam !== '0',
679
+ });
680
+ return c.json(freshness);
681
+ }
682
+ catch (error) {
683
+ return errorResponse(c, error);
684
+ }
685
+ });
686
+ app.post('/api/repos/freshness', async (c) => {
687
+ try {
688
+ const body = await c.req.json();
689
+ if (!body.repos || !Array.isArray(body.repos)) {
690
+ return c.json({ error: 'Missing "repos" array in request body' }, 400);
691
+ }
692
+ const config = await loadConfig();
693
+ const validatedRepos = body.repos.map((r) => ({
694
+ path: assertWithin(config.devDir, r.path),
695
+ branch: r.branch,
696
+ }));
697
+ const results = await checkReposFreshness(validatedRepos, {
698
+ fetch: body.fetch !== false,
699
+ });
700
+ return c.json(results);
701
+ }
702
+ catch (error) {
703
+ return errorResponse(c, error);
704
+ }
705
+ });
706
+ // 3a.2. Fast-forward base repository branch to remote tracking branch
707
+ app.post('/api/repos/pull', async (c) => {
708
+ try {
709
+ const body = await c.req.json();
710
+ const config = await loadConfig();
711
+ let reposToUpdate = [];
712
+ if (body.repos && Array.isArray(body.repos)) {
713
+ reposToUpdate = body.repos.map((r) => ({
714
+ path: assertWithin(config.devDir, r.path),
715
+ branch: r.branch,
716
+ }));
717
+ }
718
+ else if (body.path) {
719
+ reposToUpdate = [
720
+ {
721
+ path: assertWithin(config.devDir, body.path),
722
+ branch: body.branch,
723
+ },
724
+ ];
725
+ }
726
+ else {
727
+ return c.json({ error: 'Missing "path" or "repos" in request body' }, 400);
728
+ }
729
+ const results = await fastForwardRepos(reposToUpdate);
730
+ const allSuccessful = results.every((r) => r.success);
731
+ return c.json({
732
+ success: allSuccessful,
733
+ results,
734
+ });
735
+ }
736
+ catch (error) {
737
+ return errorResponse(c, error);
738
+ }
739
+ });
600
740
  // 3b. Scaffold a brand-new local git repository in devDir
601
741
  app.post('/api/repos/new', async (c) => {
602
742
  try {
@@ -736,6 +876,7 @@ app.get('/api/workspaces/status', async (c) => {
736
876
  runningServices: 0,
737
877
  syncStatus: 'unknown',
738
878
  pendingValidation: false,
879
+ activeAssistants: [],
739
880
  };
740
881
  try {
741
882
  // Uncommitted changes across the workspace's repos: worktrees inside
@@ -758,6 +899,8 @@ app.get('/api/workspaces/status', async (c) => {
758
899
  const repoStates = Object.values(wsState.repos);
759
900
  status.syncStatus = worstSyncStatus(repoStates);
760
901
  status.pendingValidation = repoStates.some((r) => r.pendingValidation);
902
+ // Active AI assistant sessions
903
+ status.activeAssistants = await findActiveAssistants(workspacePath, ws.repos);
761
904
  }
762
905
  catch {
763
906
  // Leave defaults on any per-workspace failure so one bad repo doesn't
@@ -892,7 +1035,9 @@ async function runCreationJob(jobId, body, config) {
892
1035
  // Step 1: Materialize the workspace (worktrees, or just the lightweight
893
1036
  // dir). One stable step id for both modes — only the wording differs.
894
1037
  updateJobStep(jobId, 'workspace', 'running', inPlace ? 'Registering workspace...' : 'Creating git worktrees...');
895
- await createWorkspace(feature, body.repos);
1038
+ await createWorkspace(feature, body.repos, undefined, {
1039
+ autoUpdateBase: body.autoUpdateBase !== false,
1040
+ });
896
1041
  if (Array.isArray(body.enabledSkills) || Array.isArray(body.enabledAgents) || Array.isArray(body.enabledCategories)) {
897
1042
  await saveWorkspaceSkillsConfig(workspacePath, {
898
1043
  enabledSkills: Array.isArray(body.enabledSkills) ? body.enabledSkills : [],
@@ -1126,7 +1271,6 @@ app.post('/api/workspace/:id/isolate', async (c) => {
1126
1271
  app.post('/api/workspace/suggest-workflow', async (c) => {
1127
1272
  try {
1128
1273
  const { description, repos } = await c.req.json();
1129
- const config = await loadConfig();
1130
1274
  const suggestion = await suggestWorkflow(description, repos);
1131
1275
  return c.json({
1132
1276
  success: true,
@@ -1221,15 +1365,15 @@ app.post('/api/workspace/:id/terminal', async (c) => {
1221
1365
  }
1222
1366
  try {
1223
1367
  const id = decodeURIComponent(c.req.param('id'));
1224
- const { command, assistant, sessionId, title } = await c.req.json().catch(() => ({}));
1368
+ const { command, assistant, sessionId, title, cwd } = await c.req.json().catch(() => ({}));
1225
1369
  if (assistant !== undefined) {
1226
1370
  if (typeof assistant !== 'string' || !SUPPORTED_ASSISTANTS.has(assistant.trim().toLowerCase())) {
1227
1371
  return c.json({ error: 'Invalid or unsupported assistant parameter.' }, 400);
1228
1372
  }
1229
1373
  }
1230
1374
  if (sessionId !== undefined) {
1231
- if (typeof sessionId !== 'string' || !isValidSessionUuid(sessionId)) {
1232
- return c.json({ error: 'Invalid session UUID format.' }, 400);
1375
+ if (typeof sessionId !== 'string' || !isValidSessionId(sessionId)) {
1376
+ return c.json({ error: 'Invalid session ID format.' }, 400);
1233
1377
  }
1234
1378
  }
1235
1379
  if (command !== undefined && (typeof command !== 'string' || !command.trim())) {
@@ -1238,6 +1382,9 @@ app.post('/api/workspace/:id/terminal', async (c) => {
1238
1382
  if (title !== undefined && typeof title !== 'string') {
1239
1383
  return c.json({ error: 'Title must be a string.' }, 400);
1240
1384
  }
1385
+ if (cwd !== undefined && typeof cwd !== 'string') {
1386
+ return c.json({ error: 'Cwd must be a string.' }, 400);
1387
+ }
1241
1388
  const config = await loadConfig();
1242
1389
  const requestedWorkspacePath = resolveWorkspacePath(config.workspacesDir, id);
1243
1390
  let workspacePath;
@@ -1253,7 +1400,21 @@ app.post('/api/workspace/:id/terminal', async (c) => {
1253
1400
  return c.json({ error: 'Workspace configuration not found.' }, 404);
1254
1401
  }
1255
1402
  await checkGenerationLock(workspacePath, { markDocuments: true });
1256
- const res = await launchWorkspaceTerminal(workspacePath, {
1403
+ let launchPath = workspacePath;
1404
+ if (typeof cwd === 'string' && cwd.trim()) {
1405
+ try {
1406
+ const canonicalWorkspace = await fs.realpath(workspacePath);
1407
+ const canonicalCwd = await fs.realpath(path.resolve(workspacePath, cwd.trim()));
1408
+ launchPath = assertWithin(canonicalWorkspace, canonicalCwd);
1409
+ }
1410
+ catch (error) {
1411
+ if (error instanceof PathAccessError) {
1412
+ return c.json({ error: 'Launch directory must be within workspace.' }, 400);
1413
+ }
1414
+ return c.json({ error: 'Launch directory not found or invalid.' }, 400);
1415
+ }
1416
+ }
1417
+ const res = await launchWorkspaceTerminal(launchPath, {
1257
1418
  command: typeof command === 'string' ? command : undefined,
1258
1419
  assistant: typeof assistant === 'string' ? assistant : undefined,
1259
1420
  sessionId: typeof sessionId === 'string' ? sessionId : undefined,
@@ -1332,6 +1493,15 @@ app.get('/api/workspace/:id/services', async (c) => {
1332
1493
  return errorResponse(c, error);
1333
1494
  }
1334
1495
  });
1496
+ function getWorkspaceLogDir(workspacePath) {
1497
+ const primary = path.join(workspacePath, PRIMARY_LOGS_DIR);
1498
+ const legacy = path.join(workspacePath, LEGACY_LOGS_DIR);
1499
+ if (existsSync(primary))
1500
+ return primary;
1501
+ if (existsSync(legacy))
1502
+ return legacy;
1503
+ return primary;
1504
+ }
1335
1505
  // 10. Start services in workspace. Configs are re-detected server-side —
1336
1506
  // the client only says "start", never what to execute.
1337
1507
  app.post('/api/workspace/:id/services/start', async (c) => {
@@ -1339,7 +1509,7 @@ app.post('/api/workspace/:id/services/start', async (c) => {
1339
1509
  const id = c.req.param('id');
1340
1510
  const config = await loadConfig();
1341
1511
  const workspacePath = resolveWorkspacePath(config.workspacesDir, id);
1342
- const logDir = path.join(workspacePath, '.nexusflow-logs');
1512
+ const logDir = getWorkspaceLogDir(workspacePath);
1343
1513
  const services = await detectAllServices(workspacePath);
1344
1514
  await startServices(services, workspacePath, logDir);
1345
1515
  return c.json({ success: true });
@@ -1370,7 +1540,7 @@ app.post('/api/workspace/:id/services/:serviceName/:action{start|stop|restart}',
1370
1540
  const action = c.req.param('action');
1371
1541
  const config = await loadConfig();
1372
1542
  const workspacePath = resolveWorkspacePath(config.workspacesDir, id);
1373
- const logDir = path.join(workspacePath, '.nexusflow-logs');
1543
+ const logDir = getWorkspaceLogDir(workspacePath);
1374
1544
  if (action === 'stop') {
1375
1545
  const stopped = await stopService(workspacePath, serviceName);
1376
1546
  return c.json({ success: true, stopped });
@@ -1391,7 +1561,7 @@ app.post('/api/workspace/:id/services/:serviceName/:action{start|stop|restart}',
1391
1561
  });
1392
1562
  /** Resolve + contain a service log path; names may contain '/' (repo/sub). */
1393
1563
  function resolveServiceLogFile(workspacePath, serviceName) {
1394
- const logDir = path.join(workspacePath, '.nexusflow-logs');
1564
+ const logDir = getWorkspaceLogDir(workspacePath);
1395
1565
  return assertWithin(logDir, path.join(logDir, `${serviceName}.log`));
1396
1566
  }
1397
1567
  // 12. Get service logs (backfill). Returns the trailing 50KB and the byte
@@ -1406,14 +1576,18 @@ app.get('/api/workspace/:id/services/logs/:serviceName', async (c) => {
1406
1576
  let content = '';
1407
1577
  let size = 0;
1408
1578
  try {
1409
- const stats = await fs.stat(logFile);
1410
- size = stats.size;
1411
- const start = Math.max(0, size - 50000);
1412
1579
  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');
1580
+ try {
1581
+ const stats = await fd.stat();
1582
+ size = stats.size;
1583
+ const start = Math.max(0, size - 50000);
1584
+ const buffer = Buffer.alloc(size - start);
1585
+ await fd.read(buffer, 0, buffer.length, start);
1586
+ content = buffer.toString('utf-8');
1587
+ }
1588
+ finally {
1589
+ await fd.close();
1590
+ }
1417
1591
  }
1418
1592
  catch {
1419
1593
  content = 'No logs available yet.';
@@ -1482,7 +1656,7 @@ app.post('/api/workspace/:id/orchestrators/:action{start|stop}', async (c) => {
1482
1656
  }
1483
1657
  const config = await loadConfig();
1484
1658
  const workspacePath = resolveWorkspacePath(config.workspacesDir, id);
1485
- const logDir = path.join(workspacePath, '.nexusflow-logs');
1659
+ const logDir = getWorkspaceLogDir(workspacePath);
1486
1660
  const tools = await detectOrchestrationTools(workspacePath);
1487
1661
  const detection = tools.find((t) => t.id === body.id);
1488
1662
  if (!detection) {
@@ -1641,91 +1815,448 @@ app.get('/api/workspace/:id/changes/diff', async (c) => {
1641
1815
  return errorResponse(c, error);
1642
1816
  }
1643
1817
  });
1644
- // 13a. Get workspace knowledge (nexusflow-knowledge.md)
1645
- // Routed through the active storage adapter so the GUI edits the same file the
1646
- // generators write, whichever backend is active.
1647
- app.get('/api/workspace/:id/knowledge', async (c) => {
1648
- try {
1649
- const id = c.req.param('id');
1650
- const config = await loadConfig();
1651
- const workspacePath = resolveWorkspacePath(config.workspacesDir, id);
1652
- const content = (await readWorkspaceKnowledge(workspacePath)) ?? '# Workspace Knowledge\n\nNo knowledge file yet.';
1653
- return c.json({ content });
1654
- }
1655
- catch (error) {
1656
- return errorResponse(c, error);
1818
+ // 13b. Revert changes to specific files in workspace sub-repositories
1819
+ app.post('/api/workspace/:id/changes/revert', async (c) => {
1820
+ if (!hasTrustedLocalOrigin(c.req.header('origin'))) {
1821
+ return c.json({ error: 'A local browser origin is required.' }, 403);
1657
1822
  }
1658
- });
1659
- // 13b. Update workspace knowledge (nexusflow-knowledge.md)
1660
- app.put('/api/workspace/:id/knowledge', async (c) => {
1661
1823
  try {
1662
1824
  const id = c.req.param('id');
1663
- const { content } = await c.req.json();
1825
+ const body = await c.req.json().catch(() => null);
1826
+ const files = Array.isArray(body?.files) ? body.files.filter((f) => typeof f === 'string' && f.trim().length > 0) : [];
1827
+ if (files.length === 0) {
1828
+ return c.json({ error: 'Missing files array in request body.' }, 400);
1829
+ }
1664
1830
  const config = await loadConfig();
1665
1831
  const workspacePath = resolveWorkspacePath(config.workspacesDir, id);
1666
1832
  const feature = await loadFeatureConfig(workspacePath);
1667
- const featureId = feature?.id ?? path.basename(workspacePath);
1668
- await writeWorkspaceFile(workspacePath, featureId, 'nexusflow-knowledge.md', content);
1669
- return c.json({ success: true });
1670
- }
1671
- catch (error) {
1672
- return errorResponse(c, error);
1673
- }
1674
- });
1675
- // 13b-2. Append a single structured learning (used for a GUI quick-add).
1676
- app.post('/api/workspace/:id/knowledge/entry', async (c) => {
1677
- try {
1678
- const id = c.req.param('id');
1679
- const body = (await c.req.json());
1680
- if (!body.title?.trim()) {
1681
- return c.json({ error: 'A short knowledge title is required.' }, 400);
1833
+ const repoName = typeof body?.repo === 'string' && body.repo ? body.repo : (feature?.repos[0] ? path.basename(feature.repos[0]) : null);
1834
+ let worktreePath;
1835
+ if (feature && isInPlace(feature)) {
1836
+ const matches = repoName ? feature.repos.filter((r) => path.basename(r) === repoName) : feature.repos;
1837
+ if (matches.length === 0) {
1838
+ return c.json({ error: `Unknown repo in this workspace.` }, 404);
1839
+ }
1840
+ if (matches.length > 1) {
1841
+ return c.json({ error: `Ambiguous repository '${repoName}' in this workspace. Multiple repositories match this name.` }, 400);
1842
+ }
1843
+ worktreePath = matches[0];
1682
1844
  }
1683
- if (!body.message?.trim()) {
1684
- return c.json({ error: 'A knowledge message is required.' }, 400);
1845
+ else {
1846
+ worktreePath = repoName ? resolveRepoPath(workspacePath, repoName) : workspacePath;
1685
1847
  }
1686
- const config = await loadConfig();
1687
- const workspacePath = resolveWorkspacePath(config.workspacesDir, id);
1688
- const result = body.repo
1689
- ? await addBaseKnowledge(workspacePath, body.repo, {
1690
- type: body.type,
1691
- message: body.message,
1692
- title: body.title,
1693
- scope: body.scope,
1694
- evidence: body.evidence,
1695
- })
1696
- : await addWorkspaceKnowledge(workspacePath, {
1697
- type: body.type,
1698
- message: body.message,
1699
- title: body.title,
1700
- scope: body.scope,
1701
- evidence: body.evidence,
1702
- });
1703
- return c.json({ success: true, ...result });
1704
- }
1705
- catch (error) {
1706
- return errorResponse(c, error);
1707
- }
1708
- });
1709
- // 13c. Get workspace plan (nexusflow-plan.md)
1710
- app.get('/api/workspace/:id/plan', async (c) => {
1711
- try {
1712
- const id = c.req.param('id');
1713
- const config = await loadConfig();
1714
- const workspacePath = resolveWorkspacePath(config.workspacesDir, id);
1715
- const planFile = path.join(workspacePath, 'nexusflow-plan.md');
1716
- let content = '';
1848
+ // Snapshot git index so staged changes can be restored with 100% fidelity on rollback
1849
+ let indexPath = null;
1850
+ let originalIndexBuf = null;
1717
1851
  try {
1718
- content = await fs.readFile(planFile, 'utf-8');
1852
+ const { stdout: gitPathOut } = await execa('git', ['rev-parse', '--git-path', 'index'], { cwd: worktreePath });
1853
+ const relIndexPath = gitPathOut.trim();
1854
+ if (relIndexPath) {
1855
+ indexPath = path.resolve(worktreePath, relIndexPath);
1856
+ try {
1857
+ originalIndexBuf = await fs.readFile(indexPath);
1858
+ }
1859
+ catch (readErr) {
1860
+ if (readErr?.code !== 'ENOENT') {
1861
+ return c.json({
1862
+ error: `Failed to snapshot Git index before revert: ${readErr?.message ?? String(readErr)}`,
1863
+ }, 500);
1864
+ }
1865
+ originalIndexBuf = null;
1866
+ }
1867
+ }
1719
1868
  }
1720
- catch {
1721
- content = '# Workspace Plan\n\nNo implementation plan file yet.';
1869
+ catch (gitErr) {
1870
+ return c.json({
1871
+ error: `Failed to resolve Git index path before revert: ${gitErr?.message ?? String(gitErr)}`,
1872
+ }, 500);
1722
1873
  }
1874
+ const preflightItems = [];
1875
+ for (const file of files) {
1876
+ const resolvedFile = assertWithin(worktreePath, path.join(worktreePath, file));
1877
+ const relFile = path.relative(worktreePath, resolvedFile);
1878
+ const { stdout: statusOut } = await execa('git', ['status', '--porcelain', '--', relFile], { cwd: worktreePath });
1879
+ const statusLine = statusOut.trim();
1880
+ if (!statusLine) {
1881
+ continue;
1882
+ }
1883
+ // Preflight unsupported statuses: conflicts (U, AA, DD), submodules (S), typechanges (T)
1884
+ const code = statusLine.slice(0, 2);
1885
+ if (code.includes('U') || code === 'AA' || code === 'DD') {
1886
+ return c.json({
1887
+ error: `Cannot revert '${relFile}': file has unmerged git conflicts ('${statusLine}'). Resolve conflicts first.`,
1888
+ }, 400);
1889
+ }
1890
+ if (code.includes('S') || code.includes('T')) {
1891
+ return c.json({
1892
+ error: `Cannot revert '${relFile}': file has unsupported git status ('${statusLine}').`,
1893
+ }, 400);
1894
+ }
1895
+ const isUntracked = statusLine.startsWith('??');
1896
+ let exists = false;
1897
+ let originalContent;
1898
+ let originalMode;
1899
+ try {
1900
+ let handle = null;
1901
+ try {
1902
+ if (typeof fs.open === 'function') {
1903
+ handle = await fs.open(resolvedFile, 'r');
1904
+ }
1905
+ if (handle && typeof handle.readFile === 'function') {
1906
+ const [content, stat] = await Promise.all([
1907
+ handle.readFile(),
1908
+ typeof handle.stat === 'function' ? handle.stat() : Promise.resolve(undefined),
1909
+ ]);
1910
+ originalContent = content;
1911
+ originalMode = stat?.mode;
1912
+ exists = true;
1913
+ }
1914
+ else {
1915
+ originalContent = await fs.readFile(resolvedFile);
1916
+ exists = true;
1917
+ }
1918
+ }
1919
+ finally {
1920
+ if (handle && typeof handle.close === 'function') {
1921
+ await handle.close();
1922
+ }
1923
+ }
1924
+ }
1925
+ catch (err) {
1926
+ if (err?.code !== 'ENOENT') {
1927
+ throw err;
1928
+ }
1929
+ exists = false;
1930
+ }
1931
+ preflightItems.push({
1932
+ resolvedFile,
1933
+ relFile,
1934
+ isUntracked,
1935
+ exists,
1936
+ originalContent,
1937
+ originalMode,
1938
+ });
1939
+ }
1940
+ // Execution with rollback protection: if any operation fails, restore previous state
1941
+ const executedReverts = [];
1942
+ try {
1943
+ for (const item of preflightItems) {
1944
+ if (item.isUntracked) {
1945
+ if (item.exists) {
1946
+ await fs.unlink(item.resolvedFile);
1947
+ }
1948
+ }
1949
+ else {
1950
+ await execa('git', ['checkout', 'HEAD', '--', item.relFile], { cwd: worktreePath });
1951
+ }
1952
+ executedReverts.push(item);
1953
+ }
1954
+ }
1955
+ catch (revertErr) {
1956
+ const rollbackErrors = [];
1957
+ // 1. Restore the Git index first if it was snapshotted
1958
+ if (indexPath && originalIndexBuf) {
1959
+ try {
1960
+ await fs.writeFile(indexPath, originalIndexBuf);
1961
+ }
1962
+ catch (indexErr) {
1963
+ rollbackErrors.push(`Failed to restore git index: ${indexErr?.message ?? String(indexErr)}`);
1964
+ }
1965
+ }
1966
+ // 2. Restore modified working tree files and metadata
1967
+ for (const item of executedReverts) {
1968
+ try {
1969
+ if (item.isUntracked) {
1970
+ if (item.exists && item.originalContent) {
1971
+ await fs.writeFile(item.resolvedFile, item.originalContent);
1972
+ if (item.originalMode !== undefined && typeof fs.chmod === 'function') {
1973
+ try {
1974
+ await fs.chmod(item.resolvedFile, item.originalMode);
1975
+ }
1976
+ catch (chmodErr) {
1977
+ rollbackErrors.push(`Failed to restore mode on ${item.relFile}: ${chmodErr?.message ?? String(chmodErr)}`);
1978
+ }
1979
+ }
1980
+ }
1981
+ }
1982
+ else {
1983
+ if (item.exists && item.originalContent) {
1984
+ await fs.writeFile(item.resolvedFile, item.originalContent);
1985
+ if (item.originalMode !== undefined && typeof fs.chmod === 'function') {
1986
+ try {
1987
+ await fs.chmod(item.resolvedFile, item.originalMode);
1988
+ }
1989
+ catch (chmodErr) {
1990
+ rollbackErrors.push(`Failed to restore mode on ${item.relFile}: ${chmodErr?.message ?? String(chmodErr)}`);
1991
+ }
1992
+ }
1993
+ }
1994
+ else if (!item.exists) {
1995
+ try {
1996
+ await fs.unlink(item.resolvedFile);
1997
+ }
1998
+ catch (unlinkErr) {
1999
+ if (unlinkErr?.code !== 'ENOENT') {
2000
+ rollbackErrors.push(`Failed to remove checked-out file ${item.relFile}: ${unlinkErr?.message ?? String(unlinkErr)}`);
2001
+ }
2002
+ }
2003
+ }
2004
+ }
2005
+ }
2006
+ catch (restoreErr) {
2007
+ rollbackErrors.push(`Failed to restore ${item.relFile}: ${restoreErr?.message ?? String(restoreErr)}`);
2008
+ }
2009
+ }
2010
+ if (rollbackErrors.length > 0) {
2011
+ return c.json({
2012
+ error: `Failed to revert files: ${revertErr?.message ?? String(revertErr)}. Warning: Rollback could not fully restore state: ${rollbackErrors.join('; ')}`,
2013
+ }, 500);
2014
+ }
2015
+ return c.json({
2016
+ error: `Failed to revert files: ${revertErr?.message ?? String(revertErr)}. Any modified files were rolled back.`,
2017
+ }, 500);
2018
+ }
2019
+ const reverted = preflightItems.map((item) => item.relFile);
2020
+ return c.json({ success: true, reverted });
2021
+ }
2022
+ catch (error) {
2023
+ return errorResponse(c, error);
2024
+ }
2025
+ });
2026
+ // 13a. Get workspace knowledge (nexusflow-knowledge.md)
2027
+ // Routed through the active storage adapter so the GUI edits the same file the
2028
+ // generators write, whichever backend is active.
2029
+ app.get('/api/workspace/:id/knowledge', async (c) => {
2030
+ try {
2031
+ const id = c.req.param('id');
2032
+ const config = await loadConfig();
2033
+ const workspacePath = resolveWorkspacePath(config.workspacesDir, id);
2034
+ const content = (await readWorkspaceKnowledge(workspacePath)) ?? '# Workspace Knowledge\n\nNo knowledge file yet.';
1723
2035
  return c.json({ content });
1724
2036
  }
1725
2037
  catch (error) {
1726
2038
  return errorResponse(c, error);
1727
2039
  }
1728
2040
  });
2041
+ // 13b. Update workspace knowledge (nexusflow-knowledge.md)
2042
+ app.put('/api/workspace/:id/knowledge', async (c) => {
2043
+ try {
2044
+ const id = c.req.param('id');
2045
+ const { content } = await c.req.json();
2046
+ const config = await loadConfig();
2047
+ const workspacePath = resolveWorkspacePath(config.workspacesDir, id);
2048
+ const feature = await loadFeatureConfig(workspacePath);
2049
+ const featureId = feature?.id ?? path.basename(workspacePath);
2050
+ const { PRIMARY_KNOWLEDGE_FILE, LEGACY_KNOWLEDGE_FILE } = await import('./core/constants.js');
2051
+ const { workspaceFileExists } = await import('./core/storage.js');
2052
+ const filename = (await workspaceFileExists(workspacePath, featureId, PRIMARY_KNOWLEDGE_FILE))
2053
+ ? PRIMARY_KNOWLEDGE_FILE
2054
+ : (await workspaceFileExists(workspacePath, featureId, LEGACY_KNOWLEDGE_FILE))
2055
+ ? LEGACY_KNOWLEDGE_FILE
2056
+ : PRIMARY_KNOWLEDGE_FILE;
2057
+ await writeWorkspaceFile(workspacePath, featureId, filename, content);
2058
+ return c.json({ success: true });
2059
+ }
2060
+ catch (error) {
2061
+ return errorResponse(c, error);
2062
+ }
2063
+ });
2064
+ // 13b-2. Append a single structured learning (used for a GUI quick-add).
2065
+ app.post('/api/workspace/:id/knowledge/entry', async (c) => {
2066
+ try {
2067
+ const id = c.req.param('id');
2068
+ const body = (await c.req.json());
2069
+ if (!body.title?.trim()) {
2070
+ return c.json({ error: 'A short knowledge title is required.' }, 400);
2071
+ }
2072
+ if (!body.message?.trim()) {
2073
+ return c.json({ error: 'A knowledge message is required.' }, 400);
2074
+ }
2075
+ const config = await loadConfig();
2076
+ const workspacePath = resolveWorkspacePath(config.workspacesDir, id);
2077
+ const result = body.repo
2078
+ ? await addBaseKnowledge(workspacePath, body.repo, {
2079
+ type: body.type,
2080
+ message: body.message,
2081
+ title: body.title,
2082
+ scope: body.scope,
2083
+ evidence: body.evidence,
2084
+ })
2085
+ : await addWorkspaceKnowledge(workspacePath, {
2086
+ type: body.type,
2087
+ message: body.message,
2088
+ title: body.title,
2089
+ scope: body.scope,
2090
+ evidence: body.evidence,
2091
+ });
2092
+ return c.json({ success: true, ...result });
2093
+ }
2094
+ catch (error) {
2095
+ return errorResponse(c, error);
2096
+ }
2097
+ });
2098
+ // 13c. Get workspace plan (contextspace-plan.md / nexusflow-plan.md)
2099
+ app.get('/api/workspace/:id/plan', async (c) => {
2100
+ try {
2101
+ const id = c.req.param('id');
2102
+ const config = await loadConfig();
2103
+ const workspacePath = resolveWorkspacePath(config.workspacesDir, id);
2104
+ const { resolveWorkspaceFilePath } = await import('./core/brand-config.js');
2105
+ const resolved = await resolveWorkspaceFilePath(workspacePath, 'plan');
2106
+ let content = '';
2107
+ try {
2108
+ content = await fs.readFile(resolved.path, 'utf-8');
2109
+ }
2110
+ catch (err) {
2111
+ if (err && (err.code === 'ENOENT' || err.code === 'ENOTDIR')) {
2112
+ content = '# Workspace Plan\n\nNo implementation plan file yet.';
2113
+ }
2114
+ else {
2115
+ throw err;
2116
+ }
2117
+ }
2118
+ return c.json({ content });
2119
+ }
2120
+ catch (error) {
2121
+ return errorResponse(c, error);
2122
+ }
2123
+ });
2124
+ // 13c-2. Check workspace migration status (legacy NexusFlow files detected)
2125
+ app.get('/api/workspace/:id/migration-status', async (c) => {
2126
+ try {
2127
+ const id = c.req.param('id');
2128
+ const config = await loadConfig();
2129
+ const workspacePath = resolveWorkspacePath(config.workspacesDir, id);
2130
+ const { isLegacyWorkspace } = await import('./core/migrate.js');
2131
+ const isLegacy = await isLegacyWorkspace(workspacePath);
2132
+ return c.json({ isLegacy });
2133
+ }
2134
+ catch (error) {
2135
+ return errorResponse(c, error);
2136
+ }
2137
+ });
2138
+ // 13c-3. Migrate legacy workspace to native ContextSpace
2139
+ app.post('/api/workspace/:id/migrate', async (c) => {
2140
+ try {
2141
+ const id = c.req.param('id');
2142
+ const config = await loadConfig();
2143
+ const workspacePath = resolveWorkspacePath(config.workspacesDir, id);
2144
+ const { migrateWorkspace } = await import('./core/migrate.js');
2145
+ const report = await migrateWorkspace(workspacePath, { dryRun: false, refresh: true });
2146
+ return c.json(report);
2147
+ }
2148
+ catch (error) {
2149
+ return errorResponse(c, error);
2150
+ }
2151
+ });
2152
+ // 13c-4. Get workspace collaboration stream (.nexusflow/chat.jsonl or .contextspace/chat.jsonl)
2153
+ app.get('/api/workspace/:id/stream', async (c) => {
2154
+ try {
2155
+ const id = c.req.param('id');
2156
+ const config = await loadConfig();
2157
+ const workspacePath = resolveWorkspacePath(config.workspacesDir, id);
2158
+ const { messages, ledgerInfo } = await readWorkspaceChatMessages(workspacePath);
2159
+ let isRemoteActive = false;
2160
+ let remoteStatus = null;
2161
+ let workflowProgress = null;
2162
+ try {
2163
+ const client = await workroomManagerModule.loadPinnedWorkroomClientForWorkspace(id);
2164
+ const snapshot = await client.snapshot();
2165
+ isRemoteActive = true;
2166
+ workflowProgress = snapshot.workflowProgress ?? null;
2167
+ remoteStatus = {
2168
+ roomId: snapshot.roomId,
2169
+ name: snapshot.name,
2170
+ };
2171
+ }
2172
+ catch {
2173
+ // Fallback to in-process workroomManager
2174
+ try {
2175
+ if (workroomManagerModule.workroomManager.hasActiveRoom()) {
2176
+ const status = await workroomManagerModule.workroomManager.status();
2177
+ if ((status.mode === 'host' || status.mode === 'guest') && status.localWorkspaceId === id) {
2178
+ isRemoteActive = true;
2179
+ workflowProgress = status.snapshot?.workflowProgress ?? null;
2180
+ remoteStatus = {
2181
+ roomId: status.roomId,
2182
+ url: status.url,
2183
+ name: status.name,
2184
+ };
2185
+ }
2186
+ }
2187
+ }
2188
+ catch {
2189
+ // ignore
2190
+ }
2191
+ }
2192
+ // Reconcile messages with live authoritative workflow progress by stepId
2193
+ const stepMap = new Map();
2194
+ if (workflowProgress?.steps) {
2195
+ for (const step of workflowProgress.steps) {
2196
+ stepMap.set(step.stepId, step);
2197
+ }
2198
+ }
2199
+ const reconciledMessages = messages.map((msg) => {
2200
+ if (!msg.stepId)
2201
+ return msg;
2202
+ const liveStep = stepMap.get(msg.stepId);
2203
+ if (!liveStep)
2204
+ return msg;
2205
+ return {
2206
+ ...msg,
2207
+ stepProposal: liveStep,
2208
+ };
2209
+ });
2210
+ const relLedgerPath = ledgerInfo.isLegacy ? '.nexusflow/chat.jsonl' : '.contextspace/chat.jsonl';
2211
+ return c.json({
2212
+ workspaceId: id,
2213
+ messages: reconciledMessages,
2214
+ workflowProgress,
2215
+ isRemoteActive,
2216
+ remoteStatus,
2217
+ isLegacy: ledgerInfo.isLegacy,
2218
+ ledgerPath: relLedgerPath,
2219
+ });
2220
+ }
2221
+ catch (error) {
2222
+ return errorResponse(c, error);
2223
+ }
2224
+ });
2225
+ // 13c-5. Post a message to workspace collaboration stream
2226
+ app.post('/api/workspace/:id/stream', async (c) => {
2227
+ try {
2228
+ const id = c.req.param('id');
2229
+ const body = (await c.req.json());
2230
+ const message = String(body.message ?? '').trim();
2231
+ if (!message) {
2232
+ return c.json({ error: 'Message cannot be empty.' }, 400);
2233
+ }
2234
+ const config = await loadConfig();
2235
+ const workspacePath = resolveWorkspacePath(config.workspacesDir, id);
2236
+ const ledger = await resolveWorkspaceChatLedger(workspacePath);
2237
+ await fs.mkdir(ledger.chatDir, { recursive: true });
2238
+ const chatFile = ledger.chatPath;
2239
+ const isHuman = body.author === 'human' || body.harness === 'developer' || body.harness === 'human';
2240
+ const status = body.status ? String(body.status).trim() : (body.stepId ? 'proposed' : undefined);
2241
+ const effectiveStatus = (!isHuman && status === 'completed') ? 'proposed' : status;
2242
+ const entry = {
2243
+ id: createHash('sha256').update(Date.now() + message).digest('hex').slice(0, 12),
2244
+ timestamp: new Date().toISOString(),
2245
+ harness: body.harness || (isHuman ? 'developer' : 'agent'),
2246
+ author: isHuman ? 'human' : 'agent',
2247
+ status: effectiveStatus,
2248
+ message,
2249
+ ...(body.stepId ? { stepId: body.stepId } : {}),
2250
+ ...(body.evidence ? { evidence: body.evidence } : {}),
2251
+ ...(isHuman && effectiveStatus === 'completed' ? { confirmedBy: 'human', confirmedAt: new Date().toISOString() } : {}),
2252
+ };
2253
+ await fs.appendFile(chatFile, JSON.stringify(entry) + '\n', 'utf-8');
2254
+ return c.json({ success: true, entry });
2255
+ }
2256
+ catch (error) {
2257
+ return errorResponse(c, error);
2258
+ }
2259
+ });
1729
2260
  // 13d. Sync all repositories in workspace
1730
2261
  app.post('/api/workspace/:id/sync', async (c) => {
1731
2262
  try {
@@ -1981,7 +2512,8 @@ app.post('/api/updates/install', async (c) => {
1981
2512
  try {
1982
2513
  const { toolId } = await c.req.json();
1983
2514
  const tools = [
1984
- { id: 'nexusflow', cmd: 'npm', args: ['install', '-g', '@mrpatronz/nexusflow'] },
2515
+ { id: ENGINE_ID, cmd: 'npm', args: ['install', '-g', ENGINE_NPM_PACKAGE] },
2516
+ { id: LEGACY_ENGINE_ID, cmd: 'npm', args: ['install', '-g', LEGACY_ENGINE_NPM_PACKAGE] },
1985
2517
  { id: 'antigravity', cmd: 'agy', args: ['update'] },
1986
2518
  { id: 'claude', cmd: 'npm', args: ['install', '-g', '@anthropic-ai/claude-code'] },
1987
2519
  ];
@@ -2004,96 +2536,6 @@ app.post('/api/updates/install', async (c) => {
2004
2536
  return errorResponse(c, error);
2005
2537
  }
2006
2538
  });
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
2539
  // 17.9. Get available workflow templates
2098
2540
  app.get('/api/workflows/templates', async (c) => {
2099
2541
  try {
@@ -2399,6 +2841,505 @@ app.post('/api/skills/workspace/:id/assign', async (c) => {
2399
2841
  return errorResponse(c, error);
2400
2842
  }
2401
2843
  });
2844
+ // ─── Workrooms: opt-in LAN/VPN collaboration ─────────────────────────────
2845
+ const WORKROOM_HUMAN_SESSION_COOKIES = [
2846
+ 'contextspace_workroom_human',
2847
+ 'nexusflow_workroom_human',
2848
+ ];
2849
+ const WORKROOM_BOOTSTRAP_TOKEN = randomToken();
2850
+ function establishWorkroomBootstrap(c) {
2851
+ for (const cookieName of WORKROOM_BOOTSTRAP_COOKIES) {
2852
+ setCookie(c, cookieName, WORKROOM_BOOTSTRAP_TOKEN, {
2853
+ httpOnly: true,
2854
+ sameSite: 'Strict',
2855
+ path: '/api/workrooms',
2856
+ });
2857
+ }
2858
+ }
2859
+ function hasValidWorkroomBootstrap(c) {
2860
+ const cookieToken = WORKROOM_BOOTSTRAP_COOKIES
2861
+ .map((name) => getCookie(c, name))
2862
+ .find((val) => typeof val === 'string' && val);
2863
+ const headerToken = WORKROOM_BOOTSTRAP_HEADERS
2864
+ .map((header) => c.req.header(header))
2865
+ .find((val) => typeof val === 'string' && val);
2866
+ return Boolean(cookieToken && headerToken
2867
+ && tokenDigest(cookieToken) === tokenDigest(WORKROOM_BOOTSTRAP_TOKEN)
2868
+ && tokenDigest(headerToken) === tokenDigest(WORKROOM_BOOTSTRAP_TOKEN));
2869
+ }
2870
+ function establishWorkroomHumanSession(c, token = workroomManager.beginHumanSession()) {
2871
+ for (const cookieName of WORKROOM_HUMAN_SESSION_COOKIES) {
2872
+ setCookie(c, cookieName, token, {
2873
+ httpOnly: true,
2874
+ sameSite: 'Strict',
2875
+ path: '/api/workrooms',
2876
+ });
2877
+ }
2878
+ }
2879
+ function getWorkroomHumanSessionToken(c) {
2880
+ return WORKROOM_HUMAN_SESSION_COOKIES
2881
+ .map((name) => getCookie(c, name))
2882
+ .find((val) => typeof val === 'string' && val);
2883
+ }
2884
+ function clearWorkroomHumanSession(c) {
2885
+ for (const cookieName of WORKROOM_HUMAN_SESSION_COOKIES) {
2886
+ deleteCookie(c, cookieName, { path: '/api/workrooms' });
2887
+ }
2888
+ }
2889
+ function hasExactDashboardOrigin(origin, requestUrl) {
2890
+ if (!origin)
2891
+ return false;
2892
+ try {
2893
+ const normalizedOrigin = new URL(origin).origin;
2894
+ const requestOrigin = new URL(requestUrl).origin;
2895
+ const devOrigin = process.env.CONTEXTSPACE_DASHBOARD_ORIGIN || process.env.NEXUSFLOW_DASHBOARD_ORIGIN;
2896
+ const configuredDevelopmentOrigin = devOrigin ? new URL(devOrigin).origin : undefined;
2897
+ return normalizedOrigin === requestOrigin || normalizedOrigin === configuredDevelopmentOrigin;
2898
+ }
2899
+ catch {
2900
+ return false;
2901
+ }
2902
+ }
2903
+ function hasExactWorkroomBrowserBoundary(c) {
2904
+ const origin = c.req.header('origin');
2905
+ if (origin)
2906
+ return hasExactDashboardOrigin(origin, c.req.url);
2907
+ const referer = c.req.header('referer');
2908
+ if (referer)
2909
+ return hasExactDashboardOrigin(referer, c.req.url);
2910
+ return c.req.header('sec-fetch-site') === 'same-origin';
2911
+ }
2912
+ app.use('/api/workrooms/*', async (c, next) => {
2913
+ const pathname = new URL(c.req.url).pathname;
2914
+ c.header('Cache-Control', 'no-store');
2915
+ if (!hasExactWorkroomBrowserBoundary(c)) {
2916
+ return c.json({ error: 'Workroom access requires the local browser dashboard.' }, 403);
2917
+ }
2918
+ if (pathname === '/api/workrooms/bootstrap')
2919
+ return next();
2920
+ if (!hasValidWorkroomBootstrap(c)) {
2921
+ return c.json({ error: 'Workroom access requires a same-origin dashboard bootstrap.' }, 403);
2922
+ }
2923
+ if (pathname === '/api/workrooms/session' || pathname === '/api/workrooms/session/reclaim'
2924
+ || pathname === '/api/workrooms/session/abandon')
2925
+ return next();
2926
+ const isMutation = ['POST', 'PUT', 'PATCH', 'DELETE'].includes(c.req.method);
2927
+ const needsHumanReadSession = c.req.method === 'GET' && (pathname === '/api/workrooms/snapshot'
2928
+ || pathname === '/api/workrooms/local-resources'
2929
+ || (pathname === '/api/workrooms/status' && workroomManager.hasActiveRoom()));
2930
+ if (!isMutation && !needsHumanReadSession)
2931
+ return next();
2932
+ const doesNotRequireExistingHumanSession = pathname === '/api/workrooms/start'
2933
+ || pathname === '/api/workrooms/join'
2934
+ || pathname === '/api/workrooms/import'
2935
+ || /^\/api\/workrooms\/quarantined\/room-[a-f0-9]{32}\/discard$/.test(pathname)
2936
+ || /^\/api\/workrooms\/room-[a-f0-9]{32}\/resume$/.test(pathname);
2937
+ if (needsHumanReadSession || (isMutation && !doesNotRequireExistingHumanSession)) {
2938
+ try {
2939
+ const authority = workroomManager.assertHumanSession(getWorkroomHumanSessionToken(c));
2940
+ if (pathname === '/api/workrooms/stop')
2941
+ return next();
2942
+ await workroomManager.runWithHumanAuthority(authority, () => next());
2943
+ return;
2944
+ }
2945
+ catch (error) {
2946
+ return errorResponse(c, error);
2947
+ }
2948
+ }
2949
+ await next();
2950
+ });
2951
+ app.post('/api/workrooms/bootstrap', (c) => {
2952
+ establishWorkroomBootstrap(c);
2953
+ return c.json({ token: WORKROOM_BOOTSTRAP_TOKEN });
2954
+ });
2955
+ app.get('/api/workrooms/session', (c) => {
2956
+ const active = workroomManager.hasActiveRoom();
2957
+ if (!active)
2958
+ return c.json({ active: false, locked: false });
2959
+ const roomType = workroomManager.activeRoomType();
2960
+ if (!roomType)
2961
+ return c.json({ active: false, locked: false });
2962
+ try {
2963
+ workroomManager.assertHumanSession(getWorkroomHumanSessionToken(c));
2964
+ return c.json({ active: true, locked: false, roomType });
2965
+ }
2966
+ catch {
2967
+ return c.json({ active: true, locked: true, roomType });
2968
+ }
2969
+ });
2970
+ app.post('/api/workrooms/session/reclaim', async (c) => {
2971
+ try {
2972
+ const body = await c.req.json();
2973
+ if (!workroomManager.hasActiveRoom())
2974
+ return c.json({ error: 'No active Workroom can be reconnected.' }, 409);
2975
+ const token = await workroomManager.reclaimHostHumanSession(String(body.password ?? ''));
2976
+ establishWorkroomHumanSession(c, token);
2977
+ return c.json({ success: true });
2978
+ }
2979
+ catch (error) {
2980
+ return errorResponse(c, error);
2981
+ }
2982
+ });
2983
+ app.post('/api/workrooms/session/abandon', async (c) => {
2984
+ try {
2985
+ const body = await c.req.json();
2986
+ if (body.confirm !== true)
2987
+ return c.json({ error: 'Confirm leaving this locked guest connection.' }, 400);
2988
+ await workroomManager.abandonLockedGuest();
2989
+ clearWorkroomHumanSession(c);
2990
+ return c.json({ success: true });
2991
+ }
2992
+ catch (error) {
2993
+ return errorResponse(c, error);
2994
+ }
2995
+ });
2996
+ app.get('/api/workrooms/interfaces', (c) => {
2997
+ return c.json({ interfaces: listWorkroomNetworkInterfaces() });
2998
+ });
2999
+ app.get('/api/workrooms/status', async (c) => {
3000
+ try {
3001
+ const status = await workroomManager.status();
3002
+ if (status.mode !== 'idle') {
3003
+ workroomManager.assertHumanSession(getWorkroomHumanSessionToken(c));
3004
+ }
3005
+ return c.json({ status });
3006
+ }
3007
+ catch (error) {
3008
+ return errorResponse(c, error);
3009
+ }
3010
+ });
3011
+ app.get('/api/workrooms/paused', async (c) => {
3012
+ try {
3013
+ return c.json({ rooms: await workroomManager.listPaused() });
3014
+ }
3015
+ catch (error) {
3016
+ return errorResponse(c, error);
3017
+ }
3018
+ });
3019
+ app.get('/api/workrooms/quarantined', async (c) => {
3020
+ try {
3021
+ return c.json({ rooms: await workroomManager.listQuarantined() });
3022
+ }
3023
+ catch (error) {
3024
+ return errorResponse(c, error);
3025
+ }
3026
+ });
3027
+ app.post('/api/workrooms/quarantined/:roomId/discard', async (c) => {
3028
+ try {
3029
+ const roomId = c.req.param('roomId');
3030
+ const body = await c.req.json();
3031
+ if (body.confirmRoomId !== roomId) {
3032
+ return c.json({ error: 'Confirm the exact quarantined Workroom ID before discarding it.' }, 400);
3033
+ }
3034
+ await workroomManager.discardQuarantined(roomId);
3035
+ return c.json({ success: true });
3036
+ }
3037
+ catch (error) {
3038
+ return errorResponse(c, error);
3039
+ }
3040
+ });
3041
+ app.post('/api/workrooms/:roomId/resume', async (c) => {
3042
+ try {
3043
+ const body = await c.req.json();
3044
+ const status = await workroomManager.resumeHost(c.req.param('roomId'), String(body.password ?? ''));
3045
+ establishWorkroomHumanSession(c);
3046
+ return c.json({ status });
3047
+ }
3048
+ catch (error) {
3049
+ return errorResponse(c, error);
3050
+ }
3051
+ });
3052
+ app.get('/api/workrooms/preview/:workspaceId', async (c) => {
3053
+ try {
3054
+ const workspaceId = decodeURIComponent(c.req.param('workspaceId'));
3055
+ const config = await loadConfig();
3056
+ const workspacePath = await resolveExactWorkspaceById(config.workspacesDir, workspaceId);
3057
+ if (!workspacePath)
3058
+ return c.json({ error: 'Workspace not found.' }, 404);
3059
+ return c.json({ preview: await buildPortableWorkroomPreview(workspacePath) });
3060
+ }
3061
+ catch (error) {
3062
+ return errorResponse(c, error);
3063
+ }
3064
+ });
3065
+ app.post('/api/workrooms/start', 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: 'Workspace not found.' }, 404);
3073
+ const preview = await buildPortableWorkroomPreview(workspacePath);
3074
+ if (body.contextConfirmed !== true) {
3075
+ return c.json({ error: 'Review the exact shared context and confirm it before starting the Workroom.' }, 400);
3076
+ }
3077
+ const documentOverrides = typeof body.documents === 'object' && body.documents !== null
3078
+ ? body.documents
3079
+ : {};
3080
+ const documents = { plan: '', decisions: '', handoff: '' };
3081
+ for (const name of ['plan', 'decisions', 'handoff']) {
3082
+ if (typeof documentOverrides[name] === 'string')
3083
+ documents[name] = documentOverrides[name];
3084
+ }
3085
+ if (body.contextDigest !== digestPortableWorkroomContext(preview.bundle, documents)) {
3086
+ return c.json({ error: 'The exact selected context changed after review. Refresh the sharing preview and confirm it again.' }, 409);
3087
+ }
3088
+ const status = await workroomManager.startHost({
3089
+ ...preview,
3090
+ documents,
3091
+ name: String(body.name ?? '').trim() || `${preview.bundle.feature.id} workroom`,
3092
+ address: String(body.address ?? ''),
3093
+ port: Number(body.port ?? 4242),
3094
+ password: String(body.password ?? ''),
3095
+ hostDisplayName: String(body.hostDisplayName ?? '').trim() || os.userInfo().username || 'Host',
3096
+ });
3097
+ establishWorkroomHumanSession(c);
3098
+ return c.json({ status }, 201);
3099
+ }
3100
+ catch (error) {
3101
+ return errorResponse(c, error);
3102
+ }
3103
+ });
3104
+ app.post('/api/workrooms/stop', async (c) => {
3105
+ try {
3106
+ const authority = workroomManager.assertHumanSession(getWorkroomHumanSessionToken(c));
3107
+ await workroomManager.stopOrLeave(authority);
3108
+ clearWorkroomHumanSession(c);
3109
+ return c.json({ success: true });
3110
+ }
3111
+ catch (error) {
3112
+ return errorResponse(c, error);
3113
+ }
3114
+ });
3115
+ app.post('/api/workrooms/invites', async (c) => {
3116
+ try {
3117
+ return c.json(await workroomManager.createInvite(), 201);
3118
+ }
3119
+ catch (error) {
3120
+ return errorResponse(c, error);
3121
+ }
3122
+ });
3123
+ app.post('/api/workrooms/join', async (c) => {
3124
+ try {
3125
+ const body = await c.req.json();
3126
+ let localWorkspaceId;
3127
+ if (typeof body.workspaceId === 'string' && body.workspaceId) {
3128
+ const config = await loadConfig();
3129
+ const workspacePath = await resolveExactWorkspaceById(config.workspacesDir, body.workspaceId);
3130
+ if (!workspacePath)
3131
+ return c.json({ error: 'Select an existing local workspace for the Workroom mapping.' }, 404);
3132
+ localWorkspaceId = body.workspaceId;
3133
+ }
3134
+ const status = await workroomManager.join(String(body.invite ?? ''), String(body.password ?? ''), String(body.displayName ?? '').trim() || os.userInfo().username || 'Developer', localWorkspaceId);
3135
+ establishWorkroomHumanSession(c);
3136
+ return c.json({ status }, 202);
3137
+ }
3138
+ catch (error) {
3139
+ return errorResponse(c, error);
3140
+ }
3141
+ });
3142
+ app.post('/api/workrooms/join/poll', async (c) => {
3143
+ try {
3144
+ return c.json({ status: await workroomManager.pollJoin() });
3145
+ }
3146
+ catch (error) {
3147
+ return errorResponse(c, error);
3148
+ }
3149
+ });
3150
+ app.get('/api/workrooms/snapshot', async (c) => {
3151
+ try {
3152
+ return c.json({ snapshot: await workroomManager.snapshot() });
3153
+ }
3154
+ catch (error) {
3155
+ return errorResponse(c, error);
3156
+ }
3157
+ });
3158
+ app.put('/api/workrooms/documents/:name', async (c) => {
3159
+ try {
3160
+ const name = documentNameSchema.parse(c.req.param('name'));
3161
+ const body = await c.req.json();
3162
+ if (typeof body.content !== 'string' || !Number.isInteger(body.expectedRevision)) {
3163
+ return c.json({ error: 'content and an integer expectedRevision are required.' }, 400);
3164
+ }
3165
+ return c.json({ document: await workroomManager.updateDocument(name, body.content, body.expectedRevision) });
3166
+ }
3167
+ catch (error) {
3168
+ return errorResponse(c, error);
3169
+ }
3170
+ });
3171
+ app.post('/api/workrooms/handoff', async (c) => {
3172
+ try {
3173
+ const body = await c.req.json();
3174
+ const workspaceId = typeof body.workspaceId === 'string' ? body.workspaceId : '';
3175
+ const config = await loadConfig();
3176
+ const workspacePath = await resolveExactWorkspaceById(config.workspacesDir, workspaceId);
3177
+ if (!workspacePath)
3178
+ return c.json({ error: 'Select a local workspace before publishing a handoff.' }, 404);
3179
+ return c.json({ document: await workroomManager.publishHandoff(workspacePath) });
3180
+ }
3181
+ catch (error) {
3182
+ return errorResponse(c, error);
3183
+ }
3184
+ });
3185
+ app.post('/api/workrooms/members/:requestId/decision', async (c) => {
3186
+ try {
3187
+ const body = await c.req.json();
3188
+ if (typeof body.accept !== 'boolean')
3189
+ return c.json({ error: 'accept must be a boolean.' }, 400);
3190
+ await workroomManager.decideJoin(c.req.param('requestId'), body.accept);
3191
+ return c.json({ success: true });
3192
+ }
3193
+ catch (error) {
3194
+ return errorResponse(c, error);
3195
+ }
3196
+ });
3197
+ app.put('/api/workrooms/members/:memberId/role', async (c) => {
3198
+ try {
3199
+ const body = await c.req.json();
3200
+ if (body.role !== 'publisher' && body.role !== 'member')
3201
+ return c.json({ error: 'role must be publisher or member.' }, 400);
3202
+ await workroomManager.setRole(c.req.param('memberId'), body.role);
3203
+ return c.json({ success: true });
3204
+ }
3205
+ catch (error) {
3206
+ return errorResponse(c, error);
3207
+ }
3208
+ });
3209
+ app.delete('/api/workrooms/members/:memberId', async (c) => {
3210
+ try {
3211
+ await workroomManager.revokeMember(c.req.param('memberId'));
3212
+ return c.json({ success: true });
3213
+ }
3214
+ catch (error) {
3215
+ return errorResponse(c, error);
3216
+ }
3217
+ });
3218
+ app.post('/api/workrooms/password/rotate', async (c) => {
3219
+ try {
3220
+ const body = await c.req.json();
3221
+ await workroomManager.rotatePassword(String(body.password ?? ''), body.revokeDevices !== false);
3222
+ return c.json({ success: true });
3223
+ }
3224
+ catch (error) {
3225
+ return errorResponse(c, error);
3226
+ }
3227
+ });
3228
+ app.get('/api/workrooms/local-resources', async (c) => {
3229
+ try {
3230
+ return c.json({ resources: await workroomManager.listLocalResources() });
3231
+ }
3232
+ catch (error) {
3233
+ return errorResponse(c, error);
3234
+ }
3235
+ });
3236
+ app.post('/api/workrooms/resources/publish', async (c) => {
3237
+ try {
3238
+ const body = await c.req.json();
3239
+ if (!['skill', 'agent', 'workflow'].includes(body.kind) || typeof body.id !== 'string' || typeof body.version !== 'string') {
3240
+ return c.json({ error: 'kind, id, and semantic version are required.' }, 400);
3241
+ }
3242
+ return c.json({ package: await workroomManager.publishLocalResource(body.kind, body.id, body.version) }, 201);
3243
+ }
3244
+ catch (error) {
3245
+ return errorResponse(c, error);
3246
+ }
3247
+ });
3248
+ app.post('/api/workrooms/resources/:digest/download', async (c) => {
3249
+ try {
3250
+ return c.json(await workroomManager.downloadResource(c.req.param('digest')));
3251
+ }
3252
+ catch (error) {
3253
+ return errorResponse(c, error);
3254
+ }
3255
+ });
3256
+ app.post('/api/workrooms/resources/:digest/apply', async (c) => {
3257
+ try {
3258
+ const body = await c.req.json();
3259
+ return c.json({ applied: await workroomManager.applyResource(c.req.param('digest'), String(body.approvedDigest ?? ''), String(body.approvedLocalDigest ?? '')) });
3260
+ }
3261
+ catch (error) {
3262
+ return errorResponse(c, error);
3263
+ }
3264
+ });
3265
+ app.post('/api/workrooms/resources/:digest/quarantine', async (c) => {
3266
+ try {
3267
+ await workroomManager.quarantineResource(c.req.param('digest'));
3268
+ return c.json({ success: true });
3269
+ }
3270
+ catch (error) {
3271
+ return errorResponse(c, error);
3272
+ }
3273
+ });
3274
+ app.post('/api/workrooms/resources/:digest/purge', async (c) => {
3275
+ try {
3276
+ const body = await c.req.json();
3277
+ if (body.confirmDigest !== c.req.param('digest')) {
3278
+ return c.json({ error: 'Confirm the exact quarantined resource digest before purging it.' }, 400);
3279
+ }
3280
+ await workroomManager.purgeResource(c.req.param('digest'));
3281
+ return c.json({ success: true });
3282
+ }
3283
+ catch (error) {
3284
+ return errorResponse(c, error);
3285
+ }
3286
+ });
3287
+ app.post('/api/workrooms/workflow/select', async (c) => {
3288
+ try {
3289
+ const body = await c.req.json();
3290
+ const steps = Array.isArray(body.steps)
3291
+ ? body.steps.map((step) => workflowStepSchema.parse(step))
3292
+ : undefined;
3293
+ if (typeof body.workflowId !== 'string' || typeof body.version !== 'string' || !steps
3294
+ || !Number.isInteger(body.expectedRevision) || body.expectedRevision < 0) {
3295
+ return c.json({ error: 'workflowId, version, steps, and a nonnegative integer expectedRevision are required.' }, 400);
3296
+ }
3297
+ await workroomManager.selectLocalWorkflow(body.workflowId, body.version, steps, body.expectedRevision);
3298
+ return c.json({ success: true });
3299
+ }
3300
+ catch (error) {
3301
+ return errorResponse(c, error);
3302
+ }
3303
+ });
3304
+ app.post('/api/workrooms/workflow/steps/:stepId/transition', async (c) => {
3305
+ try {
3306
+ const body = await c.req.json();
3307
+ const allowed = ['pending', 'in_progress', 'completion_proposed', 'completed', 'skipped'];
3308
+ if (!allowed.includes(body.status) || !Number.isInteger(body.expectedRevision)) {
3309
+ return c.json({ error: 'A valid status and integer expectedRevision are required.' }, 400);
3310
+ }
3311
+ return c.json({ step: await workroomManager.transitionWorkflowStep(c.req.param('stepId'), body.status, body.expectedRevision, typeof body.evidence === 'string' ? body.evidence : undefined) });
3312
+ }
3313
+ catch (error) {
3314
+ return errorResponse(c, error);
3315
+ }
3316
+ });
3317
+ app.post('/api/workrooms/export', async (c) => {
3318
+ try {
3319
+ const body = await c.req.json();
3320
+ return c.json({ export: await workroomManager.exportRoom(String(body.passphrase ?? '')) });
3321
+ }
3322
+ catch (error) {
3323
+ return errorResponse(c, error);
3324
+ }
3325
+ });
3326
+ app.post('/api/workrooms/import', async (c) => {
3327
+ try {
3328
+ const body = await c.req.json();
3329
+ const status = await workroomManager.importRoom(body.export, String(body.exportPassphrase ?? ''), {
3330
+ name: typeof body.name === 'string' ? body.name : undefined,
3331
+ address: String(body.address ?? ''),
3332
+ port: Number(body.port ?? 4242),
3333
+ password: String(body.password ?? ''),
3334
+ hostDisplayName: String(body.hostDisplayName ?? '').trim() || os.userInfo().username || 'Host',
3335
+ });
3336
+ establishWorkroomHumanSession(c);
3337
+ return c.json({ status }, 201);
3338
+ }
3339
+ catch (error) {
3340
+ return errorResponse(c, error);
3341
+ }
3342
+ });
2402
3343
  // ─── Schedules: recurring workspace jobs (sync/refresh) ─────────────────
2403
3344
  // List all scheduled jobs (with computed next-due time)
2404
3345
  app.get('/api/schedules', async (c) => {
@@ -2500,6 +3441,11 @@ export function startServer(port = 3000, opts = {}) {
2500
3441
  resolve({ port: info.port, server });
2501
3442
  });
2502
3443
  injectWebSocket(server);
3444
+ server.on('close', () => {
3445
+ void workroomManager.stopOrLeave().catch((error) => {
3446
+ console.error('[workroom] failed to stop with the dashboard:', error instanceof Error ? error.message : String(error));
3447
+ });
3448
+ });
2503
3449
  server.on('error', (e) => {
2504
3450
  if (e.code === 'EADDRINUSE') {
2505
3451
  // Callers that own their backend (desktop/extension) pass strictPort so