circuschief 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (319) hide show
  1. package/package.json +33 -0
  2. package/packages/server/bin/cli.js +4 -0
  3. package/packages/server/src/agents/AgentGateway.js +64 -0
  4. package/packages/server/src/agents/BaseAgent.js +41 -0
  5. package/packages/server/src/agents/LoggingAgentWrapper.js +73 -0
  6. package/packages/server/src/agents/adapters/ClaudeCodeAdapter.js +33 -0
  7. package/packages/server/src/agents/adapters/CodexAdapter.js +26 -0
  8. package/packages/server/src/agents/types.js +43 -0
  9. package/packages/server/src/agents/vcr/CassetteStore.js +111 -0
  10. package/packages/server/src/agents/vcr/VCRAgentAdapter.js +126 -0
  11. package/packages/server/src/agents/vcr/VCRSummaryWrapper.js +71 -0
  12. package/packages/server/src/api/canvas-helpers.js +249 -0
  13. package/packages/server/src/api/canvas-trash-routes.js +205 -0
  14. package/packages/server/src/api/canvas.js +331 -0
  15. package/packages/server/src/api/commandButtons.js +312 -0
  16. package/packages/server/src/api/commands.js +169 -0
  17. package/packages/server/src/api/filesystem.js +62 -0
  18. package/packages/server/src/api/git.js +85 -0
  19. package/packages/server/src/api/index.js +44 -0
  20. package/packages/server/src/api/kanban.js +342 -0
  21. package/packages/server/src/api/metrics.js +194 -0
  22. package/packages/server/src/api/projects-helpers.js +43 -0
  23. package/packages/server/src/api/projects-session-helpers.js +295 -0
  24. package/packages/server/src/api/projects.js +384 -0
  25. package/packages/server/src/api/providers.js +249 -0
  26. package/packages/server/src/api/quickResponses.js +129 -0
  27. package/packages/server/src/api/sessions-archive.js +69 -0
  28. package/packages/server/src/api/sessions-commands.js +220 -0
  29. package/packages/server/src/api/sessions-conversations.js +168 -0
  30. package/packages/server/src/api/sessions-draft.js +72 -0
  31. package/packages/server/src/api/sessions-lifecycle.js +190 -0
  32. package/packages/server/src/api/sessions-messages.js +141 -0
  33. package/packages/server/src/api/sessions-notes.js +51 -0
  34. package/packages/server/src/api/sessions-patch.js +252 -0
  35. package/packages/server/src/api/sessions-streaming.js +86 -0
  36. package/packages/server/src/api/sessions.js +269 -0
  37. package/packages/server/src/api/settings.js +194 -0
  38. package/packages/server/src/api/templates.js +63 -0
  39. package/packages/server/src/app.js +51 -0
  40. package/packages/server/src/database.js +58 -0
  41. package/packages/server/src/db/AgentCallLogRepository.js +322 -0
  42. package/packages/server/src/db/AttachmentRepository.js +191 -0
  43. package/packages/server/src/db/BaseRepository.js +39 -0
  44. package/packages/server/src/db/CanvasItemRepository.js +315 -0
  45. package/packages/server/src/db/CommandButtonRepository.js +75 -0
  46. package/packages/server/src/db/CommandRunRepository.js +219 -0
  47. package/packages/server/src/db/ConversationRepository.js +379 -0
  48. package/packages/server/src/db/DatabaseManager.js +91 -0
  49. package/packages/server/src/db/KanbanBoardRepository.js +92 -0
  50. package/packages/server/src/db/KanbanCardRepository.js +286 -0
  51. package/packages/server/src/db/KanbanLaneRepository.js +279 -0
  52. package/packages/server/src/db/MessageRepository.js +156 -0
  53. package/packages/server/src/db/ProjectDefaultsRepository.js +173 -0
  54. package/packages/server/src/db/ProjectRepository.js +110 -0
  55. package/packages/server/src/db/ProviderRepository.js +307 -0
  56. package/packages/server/src/db/QuickResponseRepository.js +186 -0
  57. package/packages/server/src/db/SessionNoteRepository.js +60 -0
  58. package/packages/server/src/db/SessionRepository.js +314 -0
  59. package/packages/server/src/db/SessionSummaryRepository.js +200 -0
  60. package/packages/server/src/db/SessionTemplateRepository.js +171 -0
  61. package/packages/server/src/db/SettingsRepository.js +211 -0
  62. package/packages/server/src/db/TodoRepository.js +132 -0
  63. package/packages/server/src/db/WorkLogRepository.js +122 -0
  64. package/packages/server/src/db/conversation-helpers.js +119 -0
  65. package/packages/server/src/db/index.js +100 -0
  66. package/packages/server/src/db/migrations/canvasItemsMigrations.js +109 -0
  67. package/packages/server/src/db/migrations/conversationsMigrations.js +183 -0
  68. package/packages/server/src/db/migrations/index.js +199 -0
  69. package/packages/server/src/db/migrations/kanbanMigrations.js +99 -0
  70. package/packages/server/src/db/migrations/migrationUtils.js +55 -0
  71. package/packages/server/src/db/migrations/miscMigrations.js +242 -0
  72. package/packages/server/src/db/migrations/projectsMigrations.js +95 -0
  73. package/packages/server/src/db/migrations/sessionsMigrations.js +282 -0
  74. package/packages/server/src/db/session-helpers.js +150 -0
  75. package/packages/server/src/index.js +106 -0
  76. package/packages/server/src/logger.js +22 -0
  77. package/packages/server/src/middleware/sessionLookup.js +57 -0
  78. package/packages/server/src/middleware/upload.js +94 -0
  79. package/packages/server/src/schema.sql +363 -0
  80. package/packages/server/src/services/agentCallLogger.js +116 -0
  81. package/packages/server/src/services/canvasStore.js +56 -0
  82. package/packages/server/src/services/childSessionContext.js +61 -0
  83. package/packages/server/src/services/commandRunner.js +422 -0
  84. package/packages/server/src/services/conversationContext.js +72 -0
  85. package/packages/server/src/services/diffService.js +172 -0
  86. package/packages/server/src/services/draftSessionService.js +181 -0
  87. package/packages/server/src/services/encryption.js +134 -0
  88. package/packages/server/src/services/ghService.js +169 -0
  89. package/packages/server/src/services/gitService.js +520 -0
  90. package/packages/server/src/services/gitSessionSetup.js +48 -0
  91. package/packages/server/src/services/hookService.js +60 -0
  92. package/packages/server/src/services/kanbanService.js +262 -0
  93. package/packages/server/src/services/kanbanTriggers.js +273 -0
  94. package/packages/server/src/services/nodeSpawnHelper.js +63 -0
  95. package/packages/server/src/services/prStatusService.js +204 -0
  96. package/packages/server/src/services/prUrlService.js +224 -0
  97. package/packages/server/src/services/providerTestService.js +81 -0
  98. package/packages/server/src/services/scheduleService.js +110 -0
  99. package/packages/server/src/services/schedulerService.js +281 -0
  100. package/packages/server/src/services/sessionDuplicator.js +63 -0
  101. package/packages/server/src/services/sessionErrors.js +173 -0
  102. package/packages/server/src/services/sessionExecution.js +378 -0
  103. package/packages/server/src/services/sessionManager.js +356 -0
  104. package/packages/server/src/services/sessionPrompts.js +427 -0
  105. package/packages/server/src/services/sessionProvider.js +107 -0
  106. package/packages/server/src/services/slashCommandDiscovery.js +258 -0
  107. package/packages/server/src/services/slashCommandPluginDiscovery.js +216 -0
  108. package/packages/server/src/services/slashCommandService.js +306 -0
  109. package/packages/server/src/services/streamEventCallbacks.js +170 -0
  110. package/packages/server/src/services/streamEventHandler.js +488 -0
  111. package/packages/server/src/services/streamUsageHandler.js +228 -0
  112. package/packages/server/src/services/summaryBroadcast.js +61 -0
  113. package/packages/server/src/services/summaryClaudeClient.js +180 -0
  114. package/packages/server/src/services/summaryPrompts.js +169 -0
  115. package/packages/server/src/services/summaryService.js +552 -0
  116. package/packages/server/src/services/summaryStaleCheck.js +35 -0
  117. package/packages/server/src/services/systemMonitor.js +281 -0
  118. package/packages/server/src/services/templateTriggerService.js +197 -0
  119. package/packages/server/src/services/terminalOutput.js +160 -0
  120. package/packages/server/src/services/todoStore.js +58 -0
  121. package/packages/server/src/services/usageTracker.js +69 -0
  122. package/packages/server/src/services/withConcurrencyGuard.js +110 -0
  123. package/packages/server/src/websocket.js +10 -0
  124. package/packages/server/src/ws/WebSocketManager.js +240 -0
  125. package/packages/server/src/ws/index.js +50 -0
  126. package/packages/shared/package.json +27 -0
  127. package/packages/shared/src/constants.js +44 -0
  128. package/packages/shared/src/contracts/canvas.js +25 -0
  129. package/packages/shared/src/contracts/commandButtons.js +36 -0
  130. package/packages/shared/src/contracts/kanban.js +142 -0
  131. package/packages/shared/src/contracts/projects.js +63 -0
  132. package/packages/shared/src/contracts/providers.js +81 -0
  133. package/packages/shared/src/contracts/quickResponses.js +44 -0
  134. package/packages/shared/src/contracts/sessions.js +112 -0
  135. package/packages/shared/src/contracts/templates.js +51 -0
  136. package/packages/shared/src/index.js +5 -0
  137. package/packages/shared/src/protocol.js +76 -0
  138. package/packages/shared/src/routeParams.js +36 -0
  139. package/packages/shared/src/types.js +167 -0
  140. package/packages/shared/src/utils.js +101 -0
  141. package/packages/web/dist/assets/ActiveSessionsView-BQc76Jc8.js +1 -0
  142. package/packages/web/dist/assets/ActiveSessionsView-ofSvx-K1.css +1 -0
  143. package/packages/web/dist/assets/AgentLogsView-CTCjHjsu.js +2 -0
  144. package/packages/web/dist/assets/AgentLogsView-D90PnQVk.css +1 -0
  145. package/packages/web/dist/assets/ApiClient-Dbs1H78V.js +1 -0
  146. package/packages/web/dist/assets/ArchiveConfirmModal-CCxSZ52u.js +1 -0
  147. package/packages/web/dist/assets/ArchiveConfirmModal-CQZeuYBz.css +1 -0
  148. package/packages/web/dist/assets/CommandButtonDetailView-CF_-LXpU.js +1 -0
  149. package/packages/web/dist/assets/CommandButtonDetailView-DBm3rzhw.css +1 -0
  150. package/packages/web/dist/assets/EffortLevelSelector-BQaQmU2d.css +1 -0
  151. package/packages/web/dist/assets/EffortLevelSelector-DPofLvm-.js +1 -0
  152. package/packages/web/dist/assets/GeneralSettingsView-BCf53fpC.css +1 -0
  153. package/packages/web/dist/assets/GeneralSettingsView-BY1G-Kv8.js +1 -0
  154. package/packages/web/dist/assets/InterpolationHelp-CgdbNcJB.js +1 -0
  155. package/packages/web/dist/assets/InterpolationHelp-iNxTxmhs.css +1 -0
  156. package/packages/web/dist/assets/MarkdownEditor-CqT1U8lo.js +2 -0
  157. package/packages/web/dist/assets/MarkdownEditor-enuH2yvP.css +1 -0
  158. package/packages/web/dist/assets/ModelSelector-BBn_Ve0D.js +1 -0
  159. package/packages/web/dist/assets/ModelSelector-DPPD-92R.css +1 -0
  160. package/packages/web/dist/assets/NewSessionView-Bo5l49nu.js +3 -0
  161. package/packages/web/dist/assets/NewSessionView-Byoi1XdQ.css +1 -0
  162. package/packages/web/dist/assets/PathChooser-BoMGzeg2.css +1 -0
  163. package/packages/web/dist/assets/PathChooser-Cx9gQ-Qt.js +1 -0
  164. package/packages/web/dist/assets/ProjectEditView-BFuscj-V.js +1 -0
  165. package/packages/web/dist/assets/ProjectEditView-DNwBUNRk.css +1 -0
  166. package/packages/web/dist/assets/ProjectListView-C55H1JHQ.css +1 -0
  167. package/packages/web/dist/assets/ProjectListView-Dj0jBZ46.js +1 -0
  168. package/packages/web/dist/assets/ProjectNewView-Brdp-xUu.js +1 -0
  169. package/packages/web/dist/assets/ProjectNewView-CpgE4R-l.css +1 -0
  170. package/packages/web/dist/assets/ProvidersView-B_QQF3RM.css +1 -0
  171. package/packages/web/dist/assets/ProvidersView-Cxc-1skq.js +1 -0
  172. package/packages/web/dist/assets/QuickResponseSettings-B2eVAtHW.js +1 -0
  173. package/packages/web/dist/assets/QuickResponseSettings-B8188A1D.css +1 -0
  174. package/packages/web/dist/assets/QuickResponsesPanel-DIBQFj0W.css +1 -0
  175. package/packages/web/dist/assets/QuickResponsesPanel-lU8pW2B0.js +1 -0
  176. package/packages/web/dist/assets/ResizableTextarea-B5nAA0RV.css +1 -0
  177. package/packages/web/dist/assets/ResizableTextarea-DSy1mWGY.js +1 -0
  178. package/packages/web/dist/assets/SessionCard-BvjLwVYg.js +1 -0
  179. package/packages/web/dist/assets/SessionCard-D20G3bX8.css +1 -0
  180. package/packages/web/dist/assets/SessionDetailView-BQbPg-RJ.js +36 -0
  181. package/packages/web/dist/assets/SessionDetailView-BrMG4p2-.css +1 -0
  182. package/packages/web/dist/assets/SessionFormOptions-BgqFR-5f.js +1 -0
  183. package/packages/web/dist/assets/SessionFormOptions-BuLlDF-7.css +1 -0
  184. package/packages/web/dist/assets/SessionListView-BAIBtJF7.css +1 -0
  185. package/packages/web/dist/assets/SessionListView-CYIHI8qF.js +1 -0
  186. package/packages/web/dist/assets/SessionLogStream-B-FwUMJQ.js +18 -0
  187. package/packages/web/dist/assets/SessionLogStream-zPUTiGbe.css +1 -0
  188. package/packages/web/dist/assets/SettingsView-DC8-hTQ-.css +1 -0
  189. package/packages/web/dist/assets/SettingsView-fZxpiGp7.js +1 -0
  190. package/packages/web/dist/assets/SlashCommandWizard-BB30cSvo.css +1 -0
  191. package/packages/web/dist/assets/SlashCommandWizard-BgaOw9W3.js +1 -0
  192. package/packages/web/dist/assets/SummarySettingsView-DcsmSVJI.css +1 -0
  193. package/packages/web/dist/assets/SummarySettingsView-eeu1Xq86.js +1 -0
  194. package/packages/web/dist/assets/TemplateDetailView-DEPKSwDo.js +1 -0
  195. package/packages/web/dist/assets/TemplateDetailView-DT2m06W7.css +1 -0
  196. package/packages/web/dist/assets/apl-B4CMkyY2.js +1 -0
  197. package/packages/web/dist/assets/asciiarmor-Df11BRmG.js +1 -0
  198. package/packages/web/dist/assets/asn1-EdZsLKOL.js +1 -0
  199. package/packages/web/dist/assets/asterisk-B-8jnY81.js +1 -0
  200. package/packages/web/dist/assets/brainfuck-C4LP7Hcl.js +1 -0
  201. package/packages/web/dist/assets/clike-B9uivgTg.js +1 -0
  202. package/packages/web/dist/assets/clojure-BMjYHr_A.js +1 -0
  203. package/packages/web/dist/assets/cmake-BQqOBYOt.js +1 -0
  204. package/packages/web/dist/assets/cobol-CWcv1MsR.js +1 -0
  205. package/packages/web/dist/assets/coffeescript-S37ZYGWr.js +1 -0
  206. package/packages/web/dist/assets/commandButtons-DNSHH8IA.js +4 -0
  207. package/packages/web/dist/assets/commonlisp-DBKNyK5s.js +1 -0
  208. package/packages/web/dist/assets/crystal-SjHAIU92.js +1 -0
  209. package/packages/web/dist/assets/css-BnMrqG3P.js +1 -0
  210. package/packages/web/dist/assets/cypher-C_CwsFkJ.js +1 -0
  211. package/packages/web/dist/assets/d-pRatUO7H.js +1 -0
  212. package/packages/web/dist/assets/diff-DbItnlRl.js +1 -0
  213. package/packages/web/dist/assets/dockerfile-BKs6k2Af.js +1 -0
  214. package/packages/web/dist/assets/dtd-DF_7sFjM.js +1 -0
  215. package/packages/web/dist/assets/dylan-DwRh75JA.js +1 -0
  216. package/packages/web/dist/assets/ebnf-CDyGwa7X.js +1 -0
  217. package/packages/web/dist/assets/ecl-Cabwm37j.js +1 -0
  218. package/packages/web/dist/assets/eiffel-CnydiIhH.js +1 -0
  219. package/packages/web/dist/assets/elm-vLlmbW-K.js +1 -0
  220. package/packages/web/dist/assets/erlang-BNw1qcRV.js +1 -0
  221. package/packages/web/dist/assets/factor-kuTfRLto.js +1 -0
  222. package/packages/web/dist/assets/fcl-Kvtd6kyn.js +1 -0
  223. package/packages/web/dist/assets/forth-Ffai-XNe.js +1 -0
  224. package/packages/web/dist/assets/fortran-DYz_wnZ1.js +1 -0
  225. package/packages/web/dist/assets/gas-Bneqetm1.js +1 -0
  226. package/packages/web/dist/assets/gherkin-heZmZLOM.js +1 -0
  227. package/packages/web/dist/assets/groovy-D9Dt4D0W.js +1 -0
  228. package/packages/web/dist/assets/haskell-BWDZoCOh.js +1 -0
  229. package/packages/web/dist/assets/haxe-H-WmDvRZ.js +1 -0
  230. package/packages/web/dist/assets/http-DBlCnlav.js +1 -0
  231. package/packages/web/dist/assets/idl-BEugSyMb.js +1 -0
  232. package/packages/web/dist/assets/index-BZlHgDSz.js +1 -0
  233. package/packages/web/dist/assets/index-BhWX8AfE.js +2 -0
  234. package/packages/web/dist/assets/index-Bi3XvF_f.js +1 -0
  235. package/packages/web/dist/assets/index-BqXoPf_D.js +1 -0
  236. package/packages/web/dist/assets/index-CAuTOZSD.js +1 -0
  237. package/packages/web/dist/assets/index-CKYk-fkb.js +1 -0
  238. package/packages/web/dist/assets/index-CTumW_tV.js +318 -0
  239. package/packages/web/dist/assets/index-CVOJVSsC.js +82 -0
  240. package/packages/web/dist/assets/index-CXK2Z3_z.js +1 -0
  241. package/packages/web/dist/assets/index-CYllQ3Vd.js +1 -0
  242. package/packages/web/dist/assets/index-CpsfI08O.js +1 -0
  243. package/packages/web/dist/assets/index-DQkhDeTA.js +3 -0
  244. package/packages/web/dist/assets/index-DWP8iCBp.js +1 -0
  245. package/packages/web/dist/assets/index-DkVb9W_J.js +1 -0
  246. package/packages/web/dist/assets/index-DmKHPbIa.js +1 -0
  247. package/packages/web/dist/assets/index-DrlQi03X.js +1 -0
  248. package/packages/web/dist/assets/index-gmCCsCQ1.css +1 -0
  249. package/packages/web/dist/assets/index-prTEzzgO.js +1 -0
  250. package/packages/web/dist/assets/index-wqgejMCM.js +1 -0
  251. package/packages/web/dist/assets/index-yh0ZHIWw.js +7 -0
  252. package/packages/web/dist/assets/javascript-qCveANmP.js +1 -0
  253. package/packages/web/dist/assets/julia-DuME0IfC.js +1 -0
  254. package/packages/web/dist/assets/livescript-BwQOo05w.js +1 -0
  255. package/packages/web/dist/assets/lua-BgMRiT3U.js +1 -0
  256. package/packages/web/dist/assets/mathematica-DTrFuWx2.js +1 -0
  257. package/packages/web/dist/assets/mbox-CNhZ1qSd.js +1 -0
  258. package/packages/web/dist/assets/mirc-CjQqDB4T.js +1 -0
  259. package/packages/web/dist/assets/mllike-CXdrOF99.js +1 -0
  260. package/packages/web/dist/assets/modelica-Dc1JOy9r.js +1 -0
  261. package/packages/web/dist/assets/mscgen-BA5vi2Kp.js +1 -0
  262. package/packages/web/dist/assets/mumps-BT43cFF4.js +1 -0
  263. package/packages/web/dist/assets/nginx-DdIZxoE0.js +1 -0
  264. package/packages/web/dist/assets/nsis-LdVXkNf5.js +1 -0
  265. package/packages/web/dist/assets/ntriples-BfvgReVJ.js +1 -0
  266. package/packages/web/dist/assets/octave-Ck1zUtKM.js +1 -0
  267. package/packages/web/dist/assets/oz-BzwKVEFT.js +1 -0
  268. package/packages/web/dist/assets/pascal--L3eBynH.js +1 -0
  269. package/packages/web/dist/assets/perl-CdXCOZ3F.js +1 -0
  270. package/packages/web/dist/assets/pig-CevX1Tat.js +1 -0
  271. package/packages/web/dist/assets/powershell-CFHJl5sT.js +1 -0
  272. package/packages/web/dist/assets/projects-DbBQQH-V.js +1 -0
  273. package/packages/web/dist/assets/properties-C78fOPTZ.js +1 -0
  274. package/packages/web/dist/assets/protobuf-ChK-085T.js +1 -0
  275. package/packages/web/dist/assets/providers-ceCc4xRU.js +1 -0
  276. package/packages/web/dist/assets/pug-DukmZTjD.js +1 -0
  277. package/packages/web/dist/assets/puppet-DMA9R1ak.js +1 -0
  278. package/packages/web/dist/assets/python-BuPzkPfP.js +1 -0
  279. package/packages/web/dist/assets/q-pXgVlZs6.js +1 -0
  280. package/packages/web/dist/assets/r-DUYO_cvP.js +1 -0
  281. package/packages/web/dist/assets/rpm-CTu-6PCP.js +1 -0
  282. package/packages/web/dist/assets/ruby-B2Rjki9n.js +1 -0
  283. package/packages/web/dist/assets/sas-B4kiWyti.js +1 -0
  284. package/packages/web/dist/assets/scheme-C41bIUwD.js +1 -0
  285. package/packages/web/dist/assets/sessions-D681M81k.js +1 -0
  286. package/packages/web/dist/assets/settings-D0evez2V.js +1 -0
  287. package/packages/web/dist/assets/shell-CjFT_Tl9.js +1 -0
  288. package/packages/web/dist/assets/sieve-C3Gn_uJK.js +1 -0
  289. package/packages/web/dist/assets/simple-mode-GW_nhZxv.js +1 -0
  290. package/packages/web/dist/assets/smalltalk-CnHTOXQT.js +1 -0
  291. package/packages/web/dist/assets/solr-DehyRSwq.js +1 -0
  292. package/packages/web/dist/assets/sparql-DkYu6x3z.js +1 -0
  293. package/packages/web/dist/assets/spreadsheet-BCZA_wO0.js +1 -0
  294. package/packages/web/dist/assets/sql-D0XecflT.js +1 -0
  295. package/packages/web/dist/assets/stex-C3f8Ysf7.js +1 -0
  296. package/packages/web/dist/assets/style-BTin-zR_.css +1 -0
  297. package/packages/web/dist/assets/stylus-B533Al4x.js +1 -0
  298. package/packages/web/dist/assets/swift-BzpIVaGY.js +1 -0
  299. package/packages/web/dist/assets/tcl-DVfN8rqt.js +1 -0
  300. package/packages/web/dist/assets/textile-CnDTJFAw.js +1 -0
  301. package/packages/web/dist/assets/tiddlywiki-DO-Gjzrf.js +1 -0
  302. package/packages/web/dist/assets/tiki-DGYXhP31.js +1 -0
  303. package/packages/web/dist/assets/toml-Bm5Em-hy.js +1 -0
  304. package/packages/web/dist/assets/troff-wAsdV37c.js +1 -0
  305. package/packages/web/dist/assets/ttcn-CfJYG6tj.js +1 -0
  306. package/packages/web/dist/assets/ttcn-cfg-B9xdYoR4.js +1 -0
  307. package/packages/web/dist/assets/turtle-B1tBg_DP.js +1 -0
  308. package/packages/web/dist/assets/vb-CmGdzxic.js +1 -0
  309. package/packages/web/dist/assets/vbscript-BuJXcnF6.js +1 -0
  310. package/packages/web/dist/assets/velocity-D8B20fx6.js +1 -0
  311. package/packages/web/dist/assets/verilog-C6RDOZhf.js +1 -0
  312. package/packages/web/dist/assets/vhdl-lSbBsy5d.js +1 -0
  313. package/packages/web/dist/assets/webidl-ZXfAyPTL.js +1 -0
  314. package/packages/web/dist/assets/xquery-CQfU5ijd.js +1 -0
  315. package/packages/web/dist/assets/yacas-BJ4BC0dw.js +1 -0
  316. package/packages/web/dist/assets/z80-Hz9HOZM7.js +1 -0
  317. package/packages/web/dist/favicon.png +0 -0
  318. package/packages/web/dist/index.html +17 -0
  319. package/packages/web/dist/logo.png +0 -0
@@ -0,0 +1,331 @@
1
+ import { Router } from 'express';
2
+ import { readFileSync, existsSync, statSync } from 'fs';
3
+ import { mkdir } from 'fs/promises';
4
+ import { extname, join, basename } from 'path';
5
+ import { sessions, canvasItems } from '../database.js';
6
+ import { broadcastToSession } from '../websocket.js';
7
+ import { WS_MESSAGE_TYPES, UpdateCanvasItemRequest } from '../../../shared/src/index.js';
8
+ import { upload, handleUploadError } from '../middleware/upload.js';
9
+ import {
10
+ isBinaryContent,
11
+ getTypeFromExtension,
12
+ processFileBuffer,
13
+ VALID_INLINE_TYPES,
14
+ validateInlineType,
15
+ buildInlineItemData,
16
+ broadcastCanvasUpdate,
17
+ writeCanvasItemToFile,
18
+ } from './canvas-helpers.js';
19
+ import trashRoutes from './canvas-trash-routes.js';
20
+
21
+ // Error message constants
22
+ const ERR_SESSION_NOT_FOUND = 'Session not found';
23
+
24
+ /**
25
+ * Process multipart file upload (Mode 1)
26
+ * @param {Express.Multer.File} file - The uploaded file from multer
27
+ * @returns {{ itemData: Object } | { error: string }}
28
+ */
29
+ function handleMultipartUpload(file) {
30
+ const result = processFileBuffer(file.buffer, file.originalname);
31
+ if (result.error) {
32
+ return { error: result.error };
33
+ }
34
+ return { itemData: result.itemData };
35
+ }
36
+
37
+ /**
38
+ * Process file path upload (Mode 2) - reads file from disk
39
+ * @param {string} filePath - Path to the file on disk
40
+ * @returns {{ itemData: Object } | { error: string }}
41
+ */
42
+ function handleFilePathUpload(filePath) {
43
+ if (!existsSync(filePath)) {
44
+ return { error: `File not found: ${filePath}` };
45
+ }
46
+
47
+ try {
48
+ const fileBuffer = readFileSync(filePath);
49
+ const result = processFileBuffer(fileBuffer, basename(filePath));
50
+ if (result.error) {
51
+ return { error: result.error };
52
+ }
53
+ return { itemData: result.itemData };
54
+ } catch (err) {
55
+ return { error: `Failed to read file: ${err.message}` };
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Process inline content upload (Mode 3)
61
+ * @param {string} type - The content type
62
+ * @param {string} content - The content data
63
+ * @param {string} filename - The filename
64
+ * @returns {{ itemData: Object } | { error: string }}
65
+ */
66
+ function handleInlineUpload(type, content, filename) {
67
+ const validationError = validateInlineType(type);
68
+ if (validationError) {
69
+ return { error: validationError };
70
+ }
71
+ return { itemData: buildInlineItemData(type, content, filename) };
72
+ }
73
+
74
+ // Re-export for tests
75
+ export { isBinaryContent, getTypeFromExtension };
76
+
77
+ const router = Router();
78
+
79
+ // Mount trash and bulk operation routes
80
+ router.use('/', trashRoutes);
81
+
82
+ // POST /api/sessions/:id/canvas - Add canvas item
83
+ // Supports three modes:
84
+ // 1. Multipart mode: FormData with 'file' field - from browser file uploads
85
+ // 2. File mode: { filePath } - reads file from disk
86
+ // 3. Inline mode: { type, content, filename } - uses provided content directly
87
+ router.post('/:id/canvas', upload.single('file'), handleUploadError, (req, res) => {
88
+ const session = sessions.getById(req.params.id);
89
+ if (!session) {
90
+ return res.status(404).json({ error: ERR_SESSION_NOT_FOUND });
91
+ }
92
+
93
+ const { filePath, type, content, filename } = req.body;
94
+ let result;
95
+
96
+ // Determine upload mode and process accordingly
97
+ if (req.file) {
98
+ result = handleMultipartUpload(req.file);
99
+ } else if (filePath) {
100
+ result = handleFilePathUpload(filePath);
101
+ } else if (content !== undefined && type && filename) {
102
+ result = handleInlineUpload(type, content, filename);
103
+ } else {
104
+ return res.status(400).json({
105
+ error: 'Either file upload, filePath, or (type + content + filename) is required'
106
+ });
107
+ }
108
+
109
+ if (result.error) {
110
+ return res.status(400).json({ error: result.error });
111
+ }
112
+
113
+ const item = canvasItems.create(req.params.id, result.itemData);
114
+
115
+ // Broadcast to session subscribers
116
+ broadcastCanvasUpdate(req.params.id, item);
117
+
118
+ res.status(201).json(item);
119
+ });
120
+
121
+ // PUT /api/sessions/:id/canvas/:itemId - Update canvas item content in-place
122
+ router.put('/:id/canvas/:itemId', (req, res) => {
123
+ const session = sessions.getById(req.params.id);
124
+ if (!session) {
125
+ return res.status(404).json({ error: ERR_SESSION_NOT_FOUND });
126
+ }
127
+
128
+ const parsed = UpdateCanvasItemRequest.safeParse(req.body);
129
+ if (!parsed.success) {
130
+ return res.status(400).json({ error: 'Invalid request body', details: parsed.error.issues });
131
+ }
132
+
133
+ const item = canvasItems.getById(req.params.itemId);
134
+ if (!item) {
135
+ return res.status(404).json({ error: 'Canvas item not found' });
136
+ }
137
+
138
+ if (item.sessionId !== req.params.id) {
139
+ return res.status(400).json({ error: 'Canvas item does not belong to this session' });
140
+ }
141
+
142
+ if (!VALID_INLINE_TYPES.includes(item.type)) {
143
+ return res.status(400).json({ error: `Cannot update content for type: ${item.type}. Editable types: ${VALID_INLINE_TYPES.join(', ')}` });
144
+ }
145
+
146
+ const updatedItem = canvasItems.updateContent(req.params.itemId, parsed.data.content);
147
+ broadcastToSession(req.params.id, WS_MESSAGE_TYPES.CANVAS_UPDATE, { item: updatedItem });
148
+ res.json(updatedItem);
149
+ });
150
+
151
+ // GET /api/sessions/:id/canvas - List canvas items
152
+ // Returns only latest version of each file (no version metadata exposed)
153
+ router.get('/:id/canvas', (req, res) => {
154
+ const session = sessions.getById(req.params.id);
155
+ if (!session) {
156
+ return res.status(404).json({ error: ERR_SESSION_NOT_FOUND });
157
+ }
158
+
159
+ // Get only latest versions (one per filename)
160
+ const items = canvasItems.getLatestVersionsBySessionId(req.params.id);
161
+
162
+ // Strip content/data from list responses to reduce payload size.
163
+ // Clients should use GET /canvas/file/:filename/content for inline content.
164
+ res.json(items.map(({ content: _content, data: _data, ...meta }) => meta));
165
+ });
166
+
167
+ // GET /api/sessions/:id/canvas/all - List ALL canvas items (including all versions)
168
+ // Returns all versions of each file for the frontend UI
169
+ router.get('/:id/canvas/all', (req, res) => {
170
+ const session = sessions.getById(req.params.id);
171
+ if (!session) {
172
+ return res.status(404).json({ error: ERR_SESSION_NOT_FOUND });
173
+ }
174
+
175
+ // Get ALL versions (not just latest)
176
+ const items = canvasItems.getBySessionId(req.params.id);
177
+
178
+ // Strip content/data from list responses to reduce payload size.
179
+ res.json(items.map(({ content: _content, data: _data, ...meta }) => meta));
180
+ });
181
+
182
+ // GET /api/sessions/:id/canvas/file/:filename/history/:version - Get historical version of canvas file
183
+ // Uses standard versioning: version 1 = oldest, version N = latest (where N = totalVersions)
184
+ // NOTE: This route must be defined BEFORE the main file route to match correctly
185
+ router.get('/:id/canvas/file/:filename/history/:version', async (req, res) => {
186
+ const { id: sessionId, filename, version } = req.params;
187
+ const versionNum = parseInt(version, 10);
188
+
189
+ const session = sessions.getById(sessionId);
190
+ if (!session) {
191
+ return res.status(404).json({ error: ERR_SESSION_NOT_FOUND });
192
+ }
193
+
194
+ // Get all versions of this file (newest first)
195
+ const allVersions = canvasItems.getAllVersionsByFilename(sessionId, filename);
196
+ if (allVersions.length === 0) {
197
+ return res.status(404).json({ error: 'File not found on canvas' });
198
+ }
199
+
200
+ // Validate version number
201
+ if (isNaN(versionNum) || versionNum < 1 || versionNum > allVersions.length) {
202
+ return res.status(404).json({
203
+ error: `Version ${versionNum} not found. Available versions: 1-${allVersions.length} (1 = oldest, ${allVersions.length} = latest)`
204
+ });
205
+ }
206
+
207
+ // Convert version number to array index
208
+ // allVersions is sorted DESC (newest first): [0] = newest, [length-1] = oldest
209
+ // Standard versioning: version 1 = oldest, version N = newest
210
+ // Mapping: version 1 → index (length-1), version N → index 0
211
+ // Formula: index = length - version
212
+ const index = allVersions.length - versionNum;
213
+ const item = allVersions[index];
214
+
215
+ try {
216
+ // Create temp directory for this session
217
+ const tempDir = `/tmp/canvas-${sessionId}`;
218
+ await mkdir(tempDir, { recursive: true });
219
+
220
+ // Include version in filename to avoid collisions when viewing multiple versions
221
+ const ext = extname(filename);
222
+ const base = basename(filename, ext);
223
+ const tempFilename = `${base}.v${versionNum}${ext}`;
224
+ const filePath = join(tempDir, tempFilename);
225
+
226
+ // Write content based on type
227
+ await writeCanvasItemToFile(item, filePath);
228
+
229
+ // Get file size
230
+ const stats = statSync(filePath);
231
+ const fileSize = stats.size;
232
+
233
+ res.json({
234
+ filePath,
235
+ type: item.type,
236
+ mimeType: item.mimeType,
237
+ fileSize,
238
+ createdAt: item.createdAt,
239
+ version: versionNum,
240
+ totalVersions: allVersions.length
241
+ });
242
+ } catch (err) {
243
+ res.status(500).json({ error: `Failed to write file: ${err.message}` });
244
+ }
245
+ });
246
+
247
+ // GET /api/sessions/:id/canvas/file/:filename/content - Get canvas file content inline
248
+ // Returns content/data inline in JSON for browser consumption (unlike the temp-file endpoint below)
249
+ // Supports ?version=N query param (1-based, 1 = oldest)
250
+ router.get('/:id/canvas/file/:filename/content', (req, res) => {
251
+ const session = sessions.getById(req.params.id);
252
+ if (!session) return res.status(404).json({ error: ERR_SESSION_NOT_FOUND });
253
+
254
+ const allVersions = canvasItems.getAllVersionsByFilename(req.params.id, req.params.filename);
255
+ if (allVersions.length === 0) return res.status(404).json({ error: 'File not found' });
256
+
257
+ // Support ?version=N (1-based, 1 = oldest)
258
+ const versionParam = parseInt(req.query.version);
259
+ let item;
260
+ if (!isNaN(versionParam) && versionParam >= 1 && versionParam <= allVersions.length) {
261
+ // allVersions is sorted DESC (newest first): [0] = newest, [length-1] = oldest
262
+ // Standard versioning: version 1 = oldest, version N = newest
263
+ const index = allVersions.length - versionParam;
264
+ item = allVersions[index];
265
+ } else if (!isNaN(versionParam)) {
266
+ return res.status(404).json({
267
+ error: `Version ${versionParam} not found. Available versions: 1-${allVersions.length}`
268
+ });
269
+ } else {
270
+ item = allVersions[0]; // latest
271
+ }
272
+
273
+ res.json({
274
+ content: item.content ?? null,
275
+ data: item.data ?? null,
276
+ type: item.type,
277
+ mimeType: item.mimeType,
278
+ filename: item.filename,
279
+ });
280
+ });
281
+
282
+ // GET /api/sessions/:id/canvas/file/:filename - Get canvas file by filename
283
+ // Writes the file to /tmp and returns the file path for Claude's Read tool
284
+ // Always returns the latest version
285
+ router.get('/:id/canvas/file/:filename', async (req, res) => {
286
+ const { id: sessionId, filename } = req.params;
287
+
288
+ const session = sessions.getById(sessionId);
289
+ if (!session) {
290
+ return res.status(404).json({ error: ERR_SESSION_NOT_FOUND });
291
+ }
292
+
293
+ // Get all versions of this file (newest first)
294
+ const allVersions = canvasItems.getAllVersionsByFilename(sessionId, filename);
295
+ if (allVersions.length === 0) {
296
+ return res.status(404).json({ error: 'File not found on canvas' });
297
+ }
298
+
299
+ // Always use latest version (index 0)
300
+ const item = allVersions[0];
301
+
302
+ try {
303
+ // Create temp directory for this session
304
+ const tempDir = `/tmp/canvas-${sessionId}`;
305
+ await mkdir(tempDir, { recursive: true });
306
+
307
+ // Use original filename without version suffix
308
+ const filePath = join(tempDir, filename);
309
+
310
+ // Write content based on type
311
+ await writeCanvasItemToFile(item, filePath);
312
+
313
+ // Get file size
314
+ const stats = statSync(filePath);
315
+ const fileSize = stats.size;
316
+
317
+ res.json({
318
+ filePath,
319
+ type: item.type,
320
+ mimeType: item.mimeType,
321
+ fileSize,
322
+ createdAt: item.createdAt,
323
+ version: allVersions.length, // Latest version number in standard versioning
324
+ totalVersions: allVersions.length
325
+ });
326
+ } catch (err) {
327
+ res.status(500).json({ error: `Failed to write file: ${err.message}` });
328
+ }
329
+ });
330
+
331
+ export default router;
@@ -0,0 +1,312 @@
1
+ import { Router } from 'express';
2
+ import { commandButtons, sessions, commandRuns, projects } from '../database.js';
3
+ import { CreateCommandButtonRequest, UpdateCommandButtonRequest } from '../../../shared/src/contracts/commandButtons.js';
4
+ import { commandRunner } from '../services/commandRunner.js';
5
+ import { broadcastToSession, broadcastToProject } from '../websocket.js';
6
+ import { WS_MESSAGE_TYPES } from '../../../shared/src/index.js';
7
+ import { databaseManager } from '../db/DatabaseManager.js';
8
+
9
+ // Error message constants
10
+ const ERR_SESSION_NOT_FOUND = 'Session not found';
11
+
12
+ const router = Router({ mergeParams: true });
13
+
14
+ /**
15
+ * Create WebSocket broadcast callbacks for a command run
16
+ * @param {string} sessionId
17
+ * @param {string} projectId
18
+ * @param {string} runId
19
+ * @param {string} buttonId
20
+ * @returns {Object} Callbacks for onOutput, onComplete, onError
21
+ */
22
+ function createCommandRunCallbacks(sessionId, projectId, runId, buttonId) {
23
+ return {
24
+ onOutput: (text) => {
25
+ broadcastToSession(sessionId, WS_MESSAGE_TYPES.COMMAND_RUN_OUTPUT, {
26
+ sessionId, runId, buttonId, output: text,
27
+ });
28
+ broadcastToProject(projectId, WS_MESSAGE_TYPES.COMMAND_RUN_OUTPUT, {
29
+ projectId, sessionId, runId, buttonId, output: text,
30
+ });
31
+ },
32
+ onComplete: (exitCode, output) => {
33
+ const status = exitCode === 0 ? 'success' : 'error';
34
+ console.log(`[CommandButtons] Command completed: runId=${runId}, buttonId=${buttonId}, exitCode=${exitCode}, status=${status}`);
35
+ console.log(`[CommandButtons] Broadcasting to session ${sessionId}`);
36
+ broadcastToSession(sessionId, WS_MESSAGE_TYPES.COMMAND_RUN_COMPLETE, {
37
+ sessionId, runId, buttonId, status, exitCode, output,
38
+ });
39
+ console.log(`[CommandButtons] Broadcasting to project ${projectId}`);
40
+ broadcastToProject(projectId, WS_MESSAGE_TYPES.COMMAND_RUN_COMPLETE, {
41
+ projectId, sessionId, runId, buttonId, status, exitCode, output,
42
+ });
43
+ },
44
+ onError: (message) => {
45
+ broadcastToSession(sessionId, WS_MESSAGE_TYPES.COMMAND_RUN_ERROR, {
46
+ sessionId, runId, buttonId, error: message,
47
+ });
48
+ broadcastToProject(projectId, WS_MESSAGE_TYPES.COMMAND_RUN_ERROR, {
49
+ projectId, sessionId, runId, buttonId, error: message,
50
+ });
51
+ },
52
+ };
53
+ }
54
+
55
+ /**
56
+ * Broadcast a command run error to session and project subscribers
57
+ */
58
+ function broadcastCommandRunError({ sessionId, projectId, runId, buttonId, errorMessage }) {
59
+ broadcastToSession(sessionId, WS_MESSAGE_TYPES.COMMAND_RUN_ERROR, {
60
+ sessionId, runId, buttonId, error: errorMessage,
61
+ });
62
+ broadcastToProject(projectId, WS_MESSAGE_TYPES.COMMAND_RUN_ERROR, {
63
+ projectId, sessionId, runId, buttonId, error: errorMessage,
64
+ });
65
+ }
66
+
67
+ // GET /api/projects/:projectId/command-buttons - List all command buttons for project
68
+ router.get('/', (req, res) => {
69
+ const { projectId } = req.params;
70
+ const buttons = commandButtons.getByProjectId(projectId);
71
+ res.json(buttons);
72
+ });
73
+
74
+ // GET /api/projects/:projectId/command-buttons/latest-runs - Get latest run for each button per session in project
75
+ router.get('/latest-runs', (req, res) => {
76
+ const { projectId } = req.params;
77
+
78
+ // Verify project exists
79
+ const project = projects.getById(projectId);
80
+ if (!project) {
81
+ return res.status(404).json({ error: 'Project not found' });
82
+ }
83
+
84
+ const latestRuns = commandRuns.getLatestRunsForProject(projectId);
85
+ res.json(latestRuns);
86
+ });
87
+
88
+ // POST /api/projects/:projectId/command-buttons - Create new command button
89
+ router.post('/', (req, res) => {
90
+ const result = CreateCommandButtonRequest.safeParse(req.body);
91
+ if (!result.success) {
92
+ return res.status(400).json({ error: result.error.issues[0].message });
93
+ }
94
+
95
+ const button = commandButtons.create({
96
+ projectId: req.params.projectId,
97
+ label: result.data.label,
98
+ command: result.data.command,
99
+ sortOrder: result.data.sortOrder,
100
+ showOnList: result.data.showOnList,
101
+ });
102
+
103
+ res.status(201).json(button);
104
+ });
105
+
106
+ // GET /api/projects/:projectId/command-buttons/:id - Get single button
107
+ router.get('/:id', (req, res) => {
108
+ const button = commandButtons.getById(req.params.id);
109
+ if (!button) {
110
+ return res.status(404).json({ error: 'Command button not found' });
111
+ }
112
+ res.json(button);
113
+ });
114
+
115
+ // PATCH /api/projects/:projectId/command-buttons/:id - Update button
116
+ router.patch('/:id', (req, res) => {
117
+ const button = commandButtons.getById(req.params.id);
118
+ if (!button) {
119
+ return res.status(404).json({ error: 'Command button not found' });
120
+ }
121
+
122
+ const result = UpdateCommandButtonRequest.safeParse(req.body);
123
+ if (!result.success) {
124
+ return res.status(400).json({ error: result.error.issues[0].message });
125
+ }
126
+
127
+ // Map validated data and only include fields that were provided
128
+ const updateData = {};
129
+ if (result.data.label !== undefined) updateData.label = result.data.label;
130
+ if (result.data.command !== undefined) updateData.command = result.data.command;
131
+ if (result.data.sortOrder !== undefined) updateData.sortOrder = result.data.sortOrder;
132
+ if (result.data.showOnList !== undefined) updateData.showOnList = result.data.showOnList;
133
+
134
+ const updated = commandButtons.update(req.params.id, updateData);
135
+ res.json(updated);
136
+ });
137
+
138
+ // DELETE /api/projects/:projectId/command-buttons/:id - Delete button
139
+ router.delete('/:id', (req, res) => {
140
+ const button = commandButtons.getById(req.params.id);
141
+ if (!button) {
142
+ return res.status(404).json({ error: 'Command button not found' });
143
+ }
144
+
145
+ commandButtons.delete(req.params.id);
146
+ res.status(204).send();
147
+ });
148
+
149
+ // POST /api/sessions/:sessionId/command-buttons/:buttonId/run - Execute button command
150
+ router.post('/run/:buttonId', (req, res) => {
151
+ const { sessionId, buttonId } = req.params;
152
+
153
+ const session = sessions.getById(sessionId);
154
+ if (!session) {
155
+ return res.status(404).json({ error: ERR_SESSION_NOT_FOUND });
156
+ }
157
+
158
+ const button = commandButtons.getById(buttonId);
159
+ if (!button) {
160
+ return res.status(404).json({ error: 'Command button not found' });
161
+ }
162
+
163
+ const workingDirectory = session.gitWorktree || session.project?.workingDirectory || process.cwd();
164
+ const runId = databaseManager.generateId();
165
+
166
+ res.json({ runId, buttonId, status: 'running', output: '' });
167
+
168
+ const callbacks = createCommandRunCallbacks(sessionId, session.projectId, runId, buttonId);
169
+
170
+ (async () => {
171
+ try {
172
+ await commandRunner.run(
173
+ { runId, command: button.command, workingDirectory },
174
+ callbacks,
175
+ { sessionId, buttonId }
176
+ );
177
+ } catch (error) {
178
+ console.error(`Error running command button ${buttonId}:`, error);
179
+ broadcastCommandRunError({ sessionId, projectId: session.projectId, runId, buttonId, errorMessage: error.message });
180
+ }
181
+ })();
182
+ });
183
+
184
+ // GET /api/sessions/:sessionId/command-buttons/runs - Get active runs for session
185
+ router.get('/runs', (req, res) => {
186
+ const { sessionId } = req.params;
187
+
188
+ const session = sessions.getById(sessionId);
189
+ if (!session) {
190
+ return res.status(404).json({ error: ERR_SESSION_NOT_FOUND });
191
+ }
192
+
193
+ // Get running + recent (last hour) from commandRunner
194
+ const activeRuns = commandRunner.getRunsBySession(sessionId);
195
+
196
+ // Also get latest run per button (for historical context beyond 1 hour)
197
+ const latestRuns = commandRuns.getLatestRunsForSession(sessionId);
198
+
199
+ // Merge, preferring activeRuns data (more current output)
200
+ // Use buttonId as the key to avoid duplicates
201
+ const runMap = new Map();
202
+
203
+ // First add latest runs from database
204
+ for (const run of latestRuns) {
205
+ runMap.set(run.buttonId, {
206
+ runId: run.id,
207
+ buttonId: run.buttonId,
208
+ status: run.status,
209
+ output: run.output,
210
+ exitCode: run.exitCode,
211
+ startedAt: run.startedAt,
212
+ completedAt: run.completedAt,
213
+ });
214
+ }
215
+
216
+ // Override with active/recent runs (they're more current)
217
+ for (const run of activeRuns) {
218
+ runMap.set(run.buttonId, run);
219
+ }
220
+
221
+ res.json(Array.from(runMap.values()));
222
+ });
223
+
224
+ // GET /api/sessions/:sessionId/command-buttons/runs/:runId - Get single run by ID
225
+ router.get('/runs/:runId', (req, res) => {
226
+ const { sessionId, runId } = req.params;
227
+
228
+ const session = sessions.getById(sessionId);
229
+ if (!session) {
230
+ return res.status(404).json({ error: ERR_SESSION_NOT_FOUND });
231
+ }
232
+
233
+ // Check if run is currently running (in memory)
234
+ if (commandRunner.isRunning(runId)) {
235
+ const activeRuns = commandRunner.getRunsBySession(sessionId);
236
+ const run = activeRuns.find((r) => r.runId === runId);
237
+ if (run) {
238
+ return res.json(run);
239
+ }
240
+ }
241
+
242
+ // Otherwise check database
243
+ const run = commandRuns.getById(runId);
244
+ if (!run || run.sessionId !== sessionId) {
245
+ return res.status(404).json({ error: 'Run not found' });
246
+ }
247
+
248
+ res.json({
249
+ runId: run.id,
250
+ buttonId: run.buttonId,
251
+ status: run.status,
252
+ output: run.output,
253
+ exitCode: run.exitCode,
254
+ startedAt: run.startedAt,
255
+ completedAt: run.completedAt,
256
+ });
257
+ });
258
+
259
+ // DELETE /api/sessions/:sessionId/command-buttons/runs/:runId - Delete a command run record
260
+ router.delete('/runs/:runId', (req, res) => {
261
+ const { sessionId, runId } = req.params;
262
+
263
+ const session = sessions.getById(sessionId);
264
+ if (!session) {
265
+ return res.status(404).json({ error: ERR_SESSION_NOT_FOUND });
266
+ }
267
+
268
+ const run = commandRuns.getById(runId);
269
+ if (!run || run.sessionId !== sessionId) {
270
+ return res.status(404).json({ error: 'Run not found' });
271
+ }
272
+
273
+ if (commandRunner.isRunning(runId)) {
274
+ return res.status(409).json({ error: 'Cannot delete a running command. Kill it first.' });
275
+ }
276
+
277
+ commandRuns.deleteById(runId);
278
+
279
+ // Broadcast deletion to session and project subscribers
280
+ broadcastToSession(sessionId, WS_MESSAGE_TYPES.COMMAND_RUN_DELETED, {
281
+ runId,
282
+ buttonId: run.buttonId,
283
+ sessionId,
284
+ });
285
+ broadcastToProject(session.projectId, WS_MESSAGE_TYPES.COMMAND_RUN_DELETED, {
286
+ runId,
287
+ buttonId: run.buttonId,
288
+ sessionId,
289
+ projectId: session.projectId,
290
+ });
291
+
292
+ res.status(204).send();
293
+ });
294
+
295
+ // POST /api/sessions/:sessionId/command-buttons/runs/:runId/kill - Kill running command
296
+ router.post('/runs/:runId/kill', (req, res) => {
297
+ const { sessionId, runId } = req.params;
298
+
299
+ const session = sessions.getById(sessionId);
300
+ if (!session) {
301
+ return res.status(404).json({ error: ERR_SESSION_NOT_FOUND });
302
+ }
303
+
304
+ const killed = commandRunner.kill(runId);
305
+ if (!killed) {
306
+ return res.status(404).json({ error: 'Run not found or already completed' });
307
+ }
308
+
309
+ res.json({ success: true, runId });
310
+ });
311
+
312
+ export default router;