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.
- package/package.json +33 -0
- package/packages/server/bin/cli.js +4 -0
- package/packages/server/src/agents/AgentGateway.js +64 -0
- package/packages/server/src/agents/BaseAgent.js +41 -0
- package/packages/server/src/agents/LoggingAgentWrapper.js +73 -0
- package/packages/server/src/agents/adapters/ClaudeCodeAdapter.js +33 -0
- package/packages/server/src/agents/adapters/CodexAdapter.js +26 -0
- package/packages/server/src/agents/types.js +43 -0
- package/packages/server/src/agents/vcr/CassetteStore.js +111 -0
- package/packages/server/src/agents/vcr/VCRAgentAdapter.js +126 -0
- package/packages/server/src/agents/vcr/VCRSummaryWrapper.js +71 -0
- package/packages/server/src/api/canvas-helpers.js +249 -0
- package/packages/server/src/api/canvas-trash-routes.js +205 -0
- package/packages/server/src/api/canvas.js +331 -0
- package/packages/server/src/api/commandButtons.js +312 -0
- package/packages/server/src/api/commands.js +169 -0
- package/packages/server/src/api/filesystem.js +62 -0
- package/packages/server/src/api/git.js +85 -0
- package/packages/server/src/api/index.js +44 -0
- package/packages/server/src/api/kanban.js +342 -0
- package/packages/server/src/api/metrics.js +194 -0
- package/packages/server/src/api/projects-helpers.js +43 -0
- package/packages/server/src/api/projects-session-helpers.js +295 -0
- package/packages/server/src/api/projects.js +384 -0
- package/packages/server/src/api/providers.js +249 -0
- package/packages/server/src/api/quickResponses.js +129 -0
- package/packages/server/src/api/sessions-archive.js +69 -0
- package/packages/server/src/api/sessions-commands.js +220 -0
- package/packages/server/src/api/sessions-conversations.js +168 -0
- package/packages/server/src/api/sessions-draft.js +72 -0
- package/packages/server/src/api/sessions-lifecycle.js +190 -0
- package/packages/server/src/api/sessions-messages.js +141 -0
- package/packages/server/src/api/sessions-notes.js +51 -0
- package/packages/server/src/api/sessions-patch.js +252 -0
- package/packages/server/src/api/sessions-streaming.js +86 -0
- package/packages/server/src/api/sessions.js +269 -0
- package/packages/server/src/api/settings.js +194 -0
- package/packages/server/src/api/templates.js +63 -0
- package/packages/server/src/app.js +51 -0
- package/packages/server/src/database.js +58 -0
- package/packages/server/src/db/AgentCallLogRepository.js +322 -0
- package/packages/server/src/db/AttachmentRepository.js +191 -0
- package/packages/server/src/db/BaseRepository.js +39 -0
- package/packages/server/src/db/CanvasItemRepository.js +315 -0
- package/packages/server/src/db/CommandButtonRepository.js +75 -0
- package/packages/server/src/db/CommandRunRepository.js +219 -0
- package/packages/server/src/db/ConversationRepository.js +379 -0
- package/packages/server/src/db/DatabaseManager.js +91 -0
- package/packages/server/src/db/KanbanBoardRepository.js +92 -0
- package/packages/server/src/db/KanbanCardRepository.js +286 -0
- package/packages/server/src/db/KanbanLaneRepository.js +279 -0
- package/packages/server/src/db/MessageRepository.js +156 -0
- package/packages/server/src/db/ProjectDefaultsRepository.js +173 -0
- package/packages/server/src/db/ProjectRepository.js +110 -0
- package/packages/server/src/db/ProviderRepository.js +307 -0
- package/packages/server/src/db/QuickResponseRepository.js +186 -0
- package/packages/server/src/db/SessionNoteRepository.js +60 -0
- package/packages/server/src/db/SessionRepository.js +314 -0
- package/packages/server/src/db/SessionSummaryRepository.js +200 -0
- package/packages/server/src/db/SessionTemplateRepository.js +171 -0
- package/packages/server/src/db/SettingsRepository.js +211 -0
- package/packages/server/src/db/TodoRepository.js +132 -0
- package/packages/server/src/db/WorkLogRepository.js +122 -0
- package/packages/server/src/db/conversation-helpers.js +119 -0
- package/packages/server/src/db/index.js +100 -0
- package/packages/server/src/db/migrations/canvasItemsMigrations.js +109 -0
- package/packages/server/src/db/migrations/conversationsMigrations.js +183 -0
- package/packages/server/src/db/migrations/index.js +199 -0
- package/packages/server/src/db/migrations/kanbanMigrations.js +99 -0
- package/packages/server/src/db/migrations/migrationUtils.js +55 -0
- package/packages/server/src/db/migrations/miscMigrations.js +242 -0
- package/packages/server/src/db/migrations/projectsMigrations.js +95 -0
- package/packages/server/src/db/migrations/sessionsMigrations.js +282 -0
- package/packages/server/src/db/session-helpers.js +150 -0
- package/packages/server/src/index.js +106 -0
- package/packages/server/src/logger.js +22 -0
- package/packages/server/src/middleware/sessionLookup.js +57 -0
- package/packages/server/src/middleware/upload.js +94 -0
- package/packages/server/src/schema.sql +363 -0
- package/packages/server/src/services/agentCallLogger.js +116 -0
- package/packages/server/src/services/canvasStore.js +56 -0
- package/packages/server/src/services/childSessionContext.js +61 -0
- package/packages/server/src/services/commandRunner.js +422 -0
- package/packages/server/src/services/conversationContext.js +72 -0
- package/packages/server/src/services/diffService.js +172 -0
- package/packages/server/src/services/draftSessionService.js +181 -0
- package/packages/server/src/services/encryption.js +134 -0
- package/packages/server/src/services/ghService.js +169 -0
- package/packages/server/src/services/gitService.js +520 -0
- package/packages/server/src/services/gitSessionSetup.js +48 -0
- package/packages/server/src/services/hookService.js +60 -0
- package/packages/server/src/services/kanbanService.js +262 -0
- package/packages/server/src/services/kanbanTriggers.js +273 -0
- package/packages/server/src/services/nodeSpawnHelper.js +63 -0
- package/packages/server/src/services/prStatusService.js +204 -0
- package/packages/server/src/services/prUrlService.js +224 -0
- package/packages/server/src/services/providerTestService.js +81 -0
- package/packages/server/src/services/scheduleService.js +110 -0
- package/packages/server/src/services/schedulerService.js +281 -0
- package/packages/server/src/services/sessionDuplicator.js +63 -0
- package/packages/server/src/services/sessionErrors.js +173 -0
- package/packages/server/src/services/sessionExecution.js +378 -0
- package/packages/server/src/services/sessionManager.js +356 -0
- package/packages/server/src/services/sessionPrompts.js +427 -0
- package/packages/server/src/services/sessionProvider.js +107 -0
- package/packages/server/src/services/slashCommandDiscovery.js +258 -0
- package/packages/server/src/services/slashCommandPluginDiscovery.js +216 -0
- package/packages/server/src/services/slashCommandService.js +306 -0
- package/packages/server/src/services/streamEventCallbacks.js +170 -0
- package/packages/server/src/services/streamEventHandler.js +488 -0
- package/packages/server/src/services/streamUsageHandler.js +228 -0
- package/packages/server/src/services/summaryBroadcast.js +61 -0
- package/packages/server/src/services/summaryClaudeClient.js +180 -0
- package/packages/server/src/services/summaryPrompts.js +169 -0
- package/packages/server/src/services/summaryService.js +552 -0
- package/packages/server/src/services/summaryStaleCheck.js +35 -0
- package/packages/server/src/services/systemMonitor.js +281 -0
- package/packages/server/src/services/templateTriggerService.js +197 -0
- package/packages/server/src/services/terminalOutput.js +160 -0
- package/packages/server/src/services/todoStore.js +58 -0
- package/packages/server/src/services/usageTracker.js +69 -0
- package/packages/server/src/services/withConcurrencyGuard.js +110 -0
- package/packages/server/src/websocket.js +10 -0
- package/packages/server/src/ws/WebSocketManager.js +240 -0
- package/packages/server/src/ws/index.js +50 -0
- package/packages/shared/package.json +27 -0
- package/packages/shared/src/constants.js +44 -0
- package/packages/shared/src/contracts/canvas.js +25 -0
- package/packages/shared/src/contracts/commandButtons.js +36 -0
- package/packages/shared/src/contracts/kanban.js +142 -0
- package/packages/shared/src/contracts/projects.js +63 -0
- package/packages/shared/src/contracts/providers.js +81 -0
- package/packages/shared/src/contracts/quickResponses.js +44 -0
- package/packages/shared/src/contracts/sessions.js +112 -0
- package/packages/shared/src/contracts/templates.js +51 -0
- package/packages/shared/src/index.js +5 -0
- package/packages/shared/src/protocol.js +76 -0
- package/packages/shared/src/routeParams.js +36 -0
- package/packages/shared/src/types.js +167 -0
- package/packages/shared/src/utils.js +101 -0
- package/packages/web/dist/assets/ActiveSessionsView-BQc76Jc8.js +1 -0
- package/packages/web/dist/assets/ActiveSessionsView-ofSvx-K1.css +1 -0
- package/packages/web/dist/assets/AgentLogsView-CTCjHjsu.js +2 -0
- package/packages/web/dist/assets/AgentLogsView-D90PnQVk.css +1 -0
- package/packages/web/dist/assets/ApiClient-Dbs1H78V.js +1 -0
- package/packages/web/dist/assets/ArchiveConfirmModal-CCxSZ52u.js +1 -0
- package/packages/web/dist/assets/ArchiveConfirmModal-CQZeuYBz.css +1 -0
- package/packages/web/dist/assets/CommandButtonDetailView-CF_-LXpU.js +1 -0
- package/packages/web/dist/assets/CommandButtonDetailView-DBm3rzhw.css +1 -0
- package/packages/web/dist/assets/EffortLevelSelector-BQaQmU2d.css +1 -0
- package/packages/web/dist/assets/EffortLevelSelector-DPofLvm-.js +1 -0
- package/packages/web/dist/assets/GeneralSettingsView-BCf53fpC.css +1 -0
- package/packages/web/dist/assets/GeneralSettingsView-BY1G-Kv8.js +1 -0
- package/packages/web/dist/assets/InterpolationHelp-CgdbNcJB.js +1 -0
- package/packages/web/dist/assets/InterpolationHelp-iNxTxmhs.css +1 -0
- package/packages/web/dist/assets/MarkdownEditor-CqT1U8lo.js +2 -0
- package/packages/web/dist/assets/MarkdownEditor-enuH2yvP.css +1 -0
- package/packages/web/dist/assets/ModelSelector-BBn_Ve0D.js +1 -0
- package/packages/web/dist/assets/ModelSelector-DPPD-92R.css +1 -0
- package/packages/web/dist/assets/NewSessionView-Bo5l49nu.js +3 -0
- package/packages/web/dist/assets/NewSessionView-Byoi1XdQ.css +1 -0
- package/packages/web/dist/assets/PathChooser-BoMGzeg2.css +1 -0
- package/packages/web/dist/assets/PathChooser-Cx9gQ-Qt.js +1 -0
- package/packages/web/dist/assets/ProjectEditView-BFuscj-V.js +1 -0
- package/packages/web/dist/assets/ProjectEditView-DNwBUNRk.css +1 -0
- package/packages/web/dist/assets/ProjectListView-C55H1JHQ.css +1 -0
- package/packages/web/dist/assets/ProjectListView-Dj0jBZ46.js +1 -0
- package/packages/web/dist/assets/ProjectNewView-Brdp-xUu.js +1 -0
- package/packages/web/dist/assets/ProjectNewView-CpgE4R-l.css +1 -0
- package/packages/web/dist/assets/ProvidersView-B_QQF3RM.css +1 -0
- package/packages/web/dist/assets/ProvidersView-Cxc-1skq.js +1 -0
- package/packages/web/dist/assets/QuickResponseSettings-B2eVAtHW.js +1 -0
- package/packages/web/dist/assets/QuickResponseSettings-B8188A1D.css +1 -0
- package/packages/web/dist/assets/QuickResponsesPanel-DIBQFj0W.css +1 -0
- package/packages/web/dist/assets/QuickResponsesPanel-lU8pW2B0.js +1 -0
- package/packages/web/dist/assets/ResizableTextarea-B5nAA0RV.css +1 -0
- package/packages/web/dist/assets/ResizableTextarea-DSy1mWGY.js +1 -0
- package/packages/web/dist/assets/SessionCard-BvjLwVYg.js +1 -0
- package/packages/web/dist/assets/SessionCard-D20G3bX8.css +1 -0
- package/packages/web/dist/assets/SessionDetailView-BQbPg-RJ.js +36 -0
- package/packages/web/dist/assets/SessionDetailView-BrMG4p2-.css +1 -0
- package/packages/web/dist/assets/SessionFormOptions-BgqFR-5f.js +1 -0
- package/packages/web/dist/assets/SessionFormOptions-BuLlDF-7.css +1 -0
- package/packages/web/dist/assets/SessionListView-BAIBtJF7.css +1 -0
- package/packages/web/dist/assets/SessionListView-CYIHI8qF.js +1 -0
- package/packages/web/dist/assets/SessionLogStream-B-FwUMJQ.js +18 -0
- package/packages/web/dist/assets/SessionLogStream-zPUTiGbe.css +1 -0
- package/packages/web/dist/assets/SettingsView-DC8-hTQ-.css +1 -0
- package/packages/web/dist/assets/SettingsView-fZxpiGp7.js +1 -0
- package/packages/web/dist/assets/SlashCommandWizard-BB30cSvo.css +1 -0
- package/packages/web/dist/assets/SlashCommandWizard-BgaOw9W3.js +1 -0
- package/packages/web/dist/assets/SummarySettingsView-DcsmSVJI.css +1 -0
- package/packages/web/dist/assets/SummarySettingsView-eeu1Xq86.js +1 -0
- package/packages/web/dist/assets/TemplateDetailView-DEPKSwDo.js +1 -0
- package/packages/web/dist/assets/TemplateDetailView-DT2m06W7.css +1 -0
- package/packages/web/dist/assets/apl-B4CMkyY2.js +1 -0
- package/packages/web/dist/assets/asciiarmor-Df11BRmG.js +1 -0
- package/packages/web/dist/assets/asn1-EdZsLKOL.js +1 -0
- package/packages/web/dist/assets/asterisk-B-8jnY81.js +1 -0
- package/packages/web/dist/assets/brainfuck-C4LP7Hcl.js +1 -0
- package/packages/web/dist/assets/clike-B9uivgTg.js +1 -0
- package/packages/web/dist/assets/clojure-BMjYHr_A.js +1 -0
- package/packages/web/dist/assets/cmake-BQqOBYOt.js +1 -0
- package/packages/web/dist/assets/cobol-CWcv1MsR.js +1 -0
- package/packages/web/dist/assets/coffeescript-S37ZYGWr.js +1 -0
- package/packages/web/dist/assets/commandButtons-DNSHH8IA.js +4 -0
- package/packages/web/dist/assets/commonlisp-DBKNyK5s.js +1 -0
- package/packages/web/dist/assets/crystal-SjHAIU92.js +1 -0
- package/packages/web/dist/assets/css-BnMrqG3P.js +1 -0
- package/packages/web/dist/assets/cypher-C_CwsFkJ.js +1 -0
- package/packages/web/dist/assets/d-pRatUO7H.js +1 -0
- package/packages/web/dist/assets/diff-DbItnlRl.js +1 -0
- package/packages/web/dist/assets/dockerfile-BKs6k2Af.js +1 -0
- package/packages/web/dist/assets/dtd-DF_7sFjM.js +1 -0
- package/packages/web/dist/assets/dylan-DwRh75JA.js +1 -0
- package/packages/web/dist/assets/ebnf-CDyGwa7X.js +1 -0
- package/packages/web/dist/assets/ecl-Cabwm37j.js +1 -0
- package/packages/web/dist/assets/eiffel-CnydiIhH.js +1 -0
- package/packages/web/dist/assets/elm-vLlmbW-K.js +1 -0
- package/packages/web/dist/assets/erlang-BNw1qcRV.js +1 -0
- package/packages/web/dist/assets/factor-kuTfRLto.js +1 -0
- package/packages/web/dist/assets/fcl-Kvtd6kyn.js +1 -0
- package/packages/web/dist/assets/forth-Ffai-XNe.js +1 -0
- package/packages/web/dist/assets/fortran-DYz_wnZ1.js +1 -0
- package/packages/web/dist/assets/gas-Bneqetm1.js +1 -0
- package/packages/web/dist/assets/gherkin-heZmZLOM.js +1 -0
- package/packages/web/dist/assets/groovy-D9Dt4D0W.js +1 -0
- package/packages/web/dist/assets/haskell-BWDZoCOh.js +1 -0
- package/packages/web/dist/assets/haxe-H-WmDvRZ.js +1 -0
- package/packages/web/dist/assets/http-DBlCnlav.js +1 -0
- package/packages/web/dist/assets/idl-BEugSyMb.js +1 -0
- package/packages/web/dist/assets/index-BZlHgDSz.js +1 -0
- package/packages/web/dist/assets/index-BhWX8AfE.js +2 -0
- package/packages/web/dist/assets/index-Bi3XvF_f.js +1 -0
- package/packages/web/dist/assets/index-BqXoPf_D.js +1 -0
- package/packages/web/dist/assets/index-CAuTOZSD.js +1 -0
- package/packages/web/dist/assets/index-CKYk-fkb.js +1 -0
- package/packages/web/dist/assets/index-CTumW_tV.js +318 -0
- package/packages/web/dist/assets/index-CVOJVSsC.js +82 -0
- package/packages/web/dist/assets/index-CXK2Z3_z.js +1 -0
- package/packages/web/dist/assets/index-CYllQ3Vd.js +1 -0
- package/packages/web/dist/assets/index-CpsfI08O.js +1 -0
- package/packages/web/dist/assets/index-DQkhDeTA.js +3 -0
- package/packages/web/dist/assets/index-DWP8iCBp.js +1 -0
- package/packages/web/dist/assets/index-DkVb9W_J.js +1 -0
- package/packages/web/dist/assets/index-DmKHPbIa.js +1 -0
- package/packages/web/dist/assets/index-DrlQi03X.js +1 -0
- package/packages/web/dist/assets/index-gmCCsCQ1.css +1 -0
- package/packages/web/dist/assets/index-prTEzzgO.js +1 -0
- package/packages/web/dist/assets/index-wqgejMCM.js +1 -0
- package/packages/web/dist/assets/index-yh0ZHIWw.js +7 -0
- package/packages/web/dist/assets/javascript-qCveANmP.js +1 -0
- package/packages/web/dist/assets/julia-DuME0IfC.js +1 -0
- package/packages/web/dist/assets/livescript-BwQOo05w.js +1 -0
- package/packages/web/dist/assets/lua-BgMRiT3U.js +1 -0
- package/packages/web/dist/assets/mathematica-DTrFuWx2.js +1 -0
- package/packages/web/dist/assets/mbox-CNhZ1qSd.js +1 -0
- package/packages/web/dist/assets/mirc-CjQqDB4T.js +1 -0
- package/packages/web/dist/assets/mllike-CXdrOF99.js +1 -0
- package/packages/web/dist/assets/modelica-Dc1JOy9r.js +1 -0
- package/packages/web/dist/assets/mscgen-BA5vi2Kp.js +1 -0
- package/packages/web/dist/assets/mumps-BT43cFF4.js +1 -0
- package/packages/web/dist/assets/nginx-DdIZxoE0.js +1 -0
- package/packages/web/dist/assets/nsis-LdVXkNf5.js +1 -0
- package/packages/web/dist/assets/ntriples-BfvgReVJ.js +1 -0
- package/packages/web/dist/assets/octave-Ck1zUtKM.js +1 -0
- package/packages/web/dist/assets/oz-BzwKVEFT.js +1 -0
- package/packages/web/dist/assets/pascal--L3eBynH.js +1 -0
- package/packages/web/dist/assets/perl-CdXCOZ3F.js +1 -0
- package/packages/web/dist/assets/pig-CevX1Tat.js +1 -0
- package/packages/web/dist/assets/powershell-CFHJl5sT.js +1 -0
- package/packages/web/dist/assets/projects-DbBQQH-V.js +1 -0
- package/packages/web/dist/assets/properties-C78fOPTZ.js +1 -0
- package/packages/web/dist/assets/protobuf-ChK-085T.js +1 -0
- package/packages/web/dist/assets/providers-ceCc4xRU.js +1 -0
- package/packages/web/dist/assets/pug-DukmZTjD.js +1 -0
- package/packages/web/dist/assets/puppet-DMA9R1ak.js +1 -0
- package/packages/web/dist/assets/python-BuPzkPfP.js +1 -0
- package/packages/web/dist/assets/q-pXgVlZs6.js +1 -0
- package/packages/web/dist/assets/r-DUYO_cvP.js +1 -0
- package/packages/web/dist/assets/rpm-CTu-6PCP.js +1 -0
- package/packages/web/dist/assets/ruby-B2Rjki9n.js +1 -0
- package/packages/web/dist/assets/sas-B4kiWyti.js +1 -0
- package/packages/web/dist/assets/scheme-C41bIUwD.js +1 -0
- package/packages/web/dist/assets/sessions-D681M81k.js +1 -0
- package/packages/web/dist/assets/settings-D0evez2V.js +1 -0
- package/packages/web/dist/assets/shell-CjFT_Tl9.js +1 -0
- package/packages/web/dist/assets/sieve-C3Gn_uJK.js +1 -0
- package/packages/web/dist/assets/simple-mode-GW_nhZxv.js +1 -0
- package/packages/web/dist/assets/smalltalk-CnHTOXQT.js +1 -0
- package/packages/web/dist/assets/solr-DehyRSwq.js +1 -0
- package/packages/web/dist/assets/sparql-DkYu6x3z.js +1 -0
- package/packages/web/dist/assets/spreadsheet-BCZA_wO0.js +1 -0
- package/packages/web/dist/assets/sql-D0XecflT.js +1 -0
- package/packages/web/dist/assets/stex-C3f8Ysf7.js +1 -0
- package/packages/web/dist/assets/style-BTin-zR_.css +1 -0
- package/packages/web/dist/assets/stylus-B533Al4x.js +1 -0
- package/packages/web/dist/assets/swift-BzpIVaGY.js +1 -0
- package/packages/web/dist/assets/tcl-DVfN8rqt.js +1 -0
- package/packages/web/dist/assets/textile-CnDTJFAw.js +1 -0
- package/packages/web/dist/assets/tiddlywiki-DO-Gjzrf.js +1 -0
- package/packages/web/dist/assets/tiki-DGYXhP31.js +1 -0
- package/packages/web/dist/assets/toml-Bm5Em-hy.js +1 -0
- package/packages/web/dist/assets/troff-wAsdV37c.js +1 -0
- package/packages/web/dist/assets/ttcn-CfJYG6tj.js +1 -0
- package/packages/web/dist/assets/ttcn-cfg-B9xdYoR4.js +1 -0
- package/packages/web/dist/assets/turtle-B1tBg_DP.js +1 -0
- package/packages/web/dist/assets/vb-CmGdzxic.js +1 -0
- package/packages/web/dist/assets/vbscript-BuJXcnF6.js +1 -0
- package/packages/web/dist/assets/velocity-D8B20fx6.js +1 -0
- package/packages/web/dist/assets/verilog-C6RDOZhf.js +1 -0
- package/packages/web/dist/assets/vhdl-lSbBsy5d.js +1 -0
- package/packages/web/dist/assets/webidl-ZXfAyPTL.js +1 -0
- package/packages/web/dist/assets/xquery-CQfU5ijd.js +1 -0
- package/packages/web/dist/assets/yacas-BJ4BC0dw.js +1 -0
- package/packages/web/dist/assets/z80-Hz9HOZM7.js +1 -0
- package/packages/web/dist/favicon.png +0 -0
- package/packages/web/dist/index.html +17 -0
- package/packages/web/dist/logo.png +0 -0
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { Router } from 'express';
|
|
2
|
+
import { sessions, attachments, sessionSummaries } from '../database.js';
|
|
3
|
+
import { cleanupActiveSession, stopSession, restartSession } from '../services/sessionManager.js';
|
|
4
|
+
import { broadcastToSession, broadcastToProject } from '../websocket.js';
|
|
5
|
+
import { WS_MESSAGE_TYPES } from '../../../shared/src/index.js';
|
|
6
|
+
import * as gitService from '../services/gitService.js';
|
|
7
|
+
import * as summaryService from '../services/summaryService.js';
|
|
8
|
+
import { executeHookAsync } from '../services/hookService.js';
|
|
9
|
+
import { requireSession, requireSessionAndProject } from '../middleware/sessionLookup.js';
|
|
10
|
+
import { duplicateSession } from '../services/sessionDuplicator.js';
|
|
11
|
+
import { configureSchedule, ScheduleError } from '../services/scheduleService.js';
|
|
12
|
+
|
|
13
|
+
const router = Router();
|
|
14
|
+
|
|
15
|
+
// GET /api/sessions/:id/summary - Get session summary
|
|
16
|
+
router.get('/:id/summary', requireSession, async (req, res) => {
|
|
17
|
+
// Check if generateIfMissing query param is set
|
|
18
|
+
const generateIfMissing = req.query.generate === 'true';
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
const summary = await summaryService.getSummary(req.params.id, generateIfMissing);
|
|
22
|
+
if (!summary) {
|
|
23
|
+
return res.status(404).json({ error: 'Summary not found' });
|
|
24
|
+
}
|
|
25
|
+
res.json(summary);
|
|
26
|
+
} catch (error) {
|
|
27
|
+
res.status(500).json({ error: error.message });
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// POST /api/sessions/:id/summary - Generate/regenerate session summary
|
|
32
|
+
router.post('/:id/summary', requireSession, async (req, res) => {
|
|
33
|
+
try {
|
|
34
|
+
const summary = await summaryService.regenerateSummary(req.params.id);
|
|
35
|
+
if (!summary) {
|
|
36
|
+
return res.status(500).json({ error: 'Failed to generate summary' });
|
|
37
|
+
}
|
|
38
|
+
res.status(201).json(summary);
|
|
39
|
+
} catch (error) {
|
|
40
|
+
res.status(500).json({ error: error.message });
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// PUT /api/sessions/:id/summary - Directly set summary data (for testing/seeding)
|
|
45
|
+
router.put('/:id/summary', requireSession, async (req, res) => {
|
|
46
|
+
try {
|
|
47
|
+
const summary = sessionSummaries.upsert(req.params.id, req.body);
|
|
48
|
+
res.json(summary);
|
|
49
|
+
} catch (error) {
|
|
50
|
+
res.status(500).json({ error: error.message });
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// POST /api/sessions/:id/schedule - Schedule a follow-up message for an existing session
|
|
55
|
+
router.post('/:id/schedule', requireSessionAndProject, async (req, res) => {
|
|
56
|
+
try {
|
|
57
|
+
const updated = configureSchedule(req.session_, req.body);
|
|
58
|
+
res.json(updated);
|
|
59
|
+
} catch (error) {
|
|
60
|
+
if (error instanceof ScheduleError) {
|
|
61
|
+
return res.status(error.statusCode).json({ error: error.message });
|
|
62
|
+
}
|
|
63
|
+
console.error('Schedule session error:', error);
|
|
64
|
+
res.status(500).json({ error: error.message });
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// POST /api/sessions/:id/duplicate - Duplicate a session
|
|
69
|
+
router.post('/:id/duplicate', requireSession, async (req, res) => {
|
|
70
|
+
try {
|
|
71
|
+
const { name } = req.body;
|
|
72
|
+
const newSession = await duplicateSession(req.params.id, { name });
|
|
73
|
+
|
|
74
|
+
// Broadcast new session creation to project subscribers
|
|
75
|
+
broadcastToProject(req.session_.projectId, WS_MESSAGE_TYPES.SESSION_CREATED, {
|
|
76
|
+
projectId: req.session_.projectId,
|
|
77
|
+
session: newSession,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
res.status(201).json(newSession);
|
|
81
|
+
} catch (error) {
|
|
82
|
+
console.error('Error duplicating session:', error);
|
|
83
|
+
res.status(500).json({ error: error.message });
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// POST /api/sessions/:id/stop - Stop running session
|
|
88
|
+
router.post('/:id/stop', requireSession, async (req, res) => {
|
|
89
|
+
// Allow stopping running, waiting, or stuck sessions (crashed sessions may be stuck in 'running')
|
|
90
|
+
// Don't allow stopping already errored or stopped sessions
|
|
91
|
+
if (req.session_.status === 'error' || req.session_.status === 'stopped') {
|
|
92
|
+
return res.status(400).json({ error: 'Session is not active' });
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
await stopSession(req.session_.id);
|
|
97
|
+
res.json({ success: true });
|
|
98
|
+
} catch (error) {
|
|
99
|
+
res.status(500).json({ error: error.message });
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// POST /api/sessions/:id/restart - Restart a completed/error session
|
|
104
|
+
router.post('/:id/restart', requireSession, (req, res) => {
|
|
105
|
+
if (req.session_.status !== 'stopped' && req.session_.status !== 'error') {
|
|
106
|
+
return res.status(400).json({ error: 'Session can only be restarted when stopped or in error state' });
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
restartSession(req.session_.id);
|
|
111
|
+
res.json({ success: true });
|
|
112
|
+
} catch (error) {
|
|
113
|
+
res.status(500).json({ error: error.message });
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// DELETE /api/sessions/:id - Delete session
|
|
118
|
+
router.delete('/:id', requireSessionAndProject, async (req, res) => {
|
|
119
|
+
// Collect all descendant session IDs before any deletions
|
|
120
|
+
// (database ON DELETE SET NULL would orphan them otherwise)
|
|
121
|
+
const descendantIds = sessions.getAllDescendantIds(req.params.id);
|
|
122
|
+
|
|
123
|
+
// Helper: clean up and delete a single session by ID
|
|
124
|
+
const cleanupAndDeleteSession = async (sessionId) => {
|
|
125
|
+
// Clean up active session if running
|
|
126
|
+
cleanupActiveSession(sessionId);
|
|
127
|
+
|
|
128
|
+
// Clean up summary service debounce timers
|
|
129
|
+
summaryService.cleanupSession(sessionId);
|
|
130
|
+
|
|
131
|
+
// Clean up attachment files from disk
|
|
132
|
+
try {
|
|
133
|
+
attachments.deleteSessionAttachmentsFromDisk(req.workingDirectory, sessionId);
|
|
134
|
+
} catch (error) {
|
|
135
|
+
console.warn(`Failed to remove attachment files for session ${sessionId}:`, error.message);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Broadcast deletion to close any open WebSocket subscriptions
|
|
139
|
+
broadcastToSession(sessionId, WS_MESSAGE_TYPES.SESSION_DELETED, { sessionId });
|
|
140
|
+
|
|
141
|
+
// Delete session (cascade will handle messages, canvas items, notes)
|
|
142
|
+
sessions.delete(sessionId);
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
// Delete all descendants first (leaves before branches to avoid orphaning)
|
|
146
|
+
for (const descendantId of descendantIds.reverse()) {
|
|
147
|
+
await cleanupAndDeleteSession(descendantId);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Remove git worktree if parent session has one (skip for child sessions - they may share parent's worktree)
|
|
151
|
+
if (req.session_.gitWorktree && !req.session_.parentSessionId) {
|
|
152
|
+
try {
|
|
153
|
+
await gitService.removeWorktree(req.project.workingDirectory, req.session_.gitWorktree, true);
|
|
154
|
+
} catch (error) {
|
|
155
|
+
// Log but don't fail - worktree may already be removed or have issues
|
|
156
|
+
console.warn(`Failed to remove worktree for session ${req.session_.id}:`, error.message);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Clean up and delete the parent session itself
|
|
161
|
+
await cleanupAndDeleteSession(req.params.id);
|
|
162
|
+
|
|
163
|
+
// Broadcast deletion to project subscribers for real-time list updates
|
|
164
|
+
// (sends one event per deleted session so the frontend can remove them all)
|
|
165
|
+
broadcastToProject(req.session_.projectId, WS_MESSAGE_TYPES.SESSION_DELETED, {
|
|
166
|
+
projectId: req.session_.projectId,
|
|
167
|
+
sessionId: req.params.id,
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
for (const descendantId of descendantIds) {
|
|
171
|
+
broadcastToProject(req.session_.projectId, WS_MESSAGE_TYPES.SESSION_DELETED, {
|
|
172
|
+
projectId: req.session_.projectId,
|
|
173
|
+
sessionId: descendantId,
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Execute on_session_deleted hook if configured (non-blocking)
|
|
178
|
+
// Skip for child sessions - they share parent's resources and shouldn't trigger teardown
|
|
179
|
+
if (req.project?.onSessionDeleted && !req.session_.parentSessionId) {
|
|
180
|
+
executeHookAsync(req.project.onSessionDeleted, req.workingDirectory, {
|
|
181
|
+
sessionId: req.session_.id,
|
|
182
|
+
projectId: req.project.id,
|
|
183
|
+
sessionName: req.session_.name,
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
res.status(204).send();
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
export default router;
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { Router } from 'express';
|
|
2
|
+
import { sessions, messages, todos, conversations, attachments } from '../database.js';
|
|
3
|
+
import { continueSession } from '../services/sessionManager.js';
|
|
4
|
+
import { upload as _upload, handleUploadError } from '../middleware/upload.js';
|
|
5
|
+
import { requireSession, requireSessionAndProject } from '../middleware/sessionLookup.js';
|
|
6
|
+
import * as slashCommandService from '../services/slashCommandService.js';
|
|
7
|
+
|
|
8
|
+
const router = Router();
|
|
9
|
+
|
|
10
|
+
// GET /api/sessions/:id/messages - Get session messages
|
|
11
|
+
// Supports ?conversation_id=xxx to filter by conversation
|
|
12
|
+
router.get('/:id/messages', requireSession, (req, res) => {
|
|
13
|
+
const { conversation_id } = req.query;
|
|
14
|
+
|
|
15
|
+
let sessionMessages;
|
|
16
|
+
let resolvedConvId = null;
|
|
17
|
+
if (conversation_id) {
|
|
18
|
+
// Get messages for specific conversation
|
|
19
|
+
const conv = conversations.getById(conversation_id);
|
|
20
|
+
if (!conv || conv.sessionId !== req.params.id) {
|
|
21
|
+
return res.status(404).json({ error: 'Conversation not found' });
|
|
22
|
+
}
|
|
23
|
+
sessionMessages = messages.getByConversationId(conversation_id);
|
|
24
|
+
resolvedConvId = conversation_id;
|
|
25
|
+
} else {
|
|
26
|
+
// Get messages for active conversation, or all messages if no active conversation
|
|
27
|
+
const activeConv = conversations.getActiveBySessionId(req.params.id);
|
|
28
|
+
if (activeConv) {
|
|
29
|
+
sessionMessages = messages.getByConversationId(activeConv.id);
|
|
30
|
+
resolvedConvId = activeConv.id;
|
|
31
|
+
} else {
|
|
32
|
+
// Fall back to all messages (for legacy/migration)
|
|
33
|
+
sessionMessages = messages.getBySessionId(req.params.id);
|
|
34
|
+
resolvedConvId = 'all (no active conversation)';
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
console.log(`[API] fetchMessages: session ${req.params.id}, conversation ${resolvedConvId}, returned ${sessionMessages.length} messages`);
|
|
39
|
+
|
|
40
|
+
// Attach file attachments to each message (without content for efficiency)
|
|
41
|
+
const messagesWithAttachments = sessionMessages.map((msg) => ({
|
|
42
|
+
...msg,
|
|
43
|
+
attachments: attachments.getByMessageIdWithoutContent(msg.id),
|
|
44
|
+
}));
|
|
45
|
+
|
|
46
|
+
res.json(messagesWithAttachments);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// POST /api/sessions/:id/message - Send follow-up message
|
|
50
|
+
// Supports both JSON and multipart/form-data (for file attachments)
|
|
51
|
+
router.post('/:id/message', _upload.array('files', 10), handleUploadError, requireSessionAndProject, async (req, res) => {
|
|
52
|
+
const content = req.body.content;
|
|
53
|
+
const model = req.body.model || null; // Model to use for this message
|
|
54
|
+
const files = req.files || [];
|
|
55
|
+
|
|
56
|
+
if (!content) {
|
|
57
|
+
return res.status(400).json({ error: 'Content is required' });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (req.session_.status !== 'waiting' && req.session_.status !== 'stopped' && req.session_.status !== 'error') {
|
|
61
|
+
return res.status(400).json({ error: 'Session is not waiting for input' });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
// Store file attachments if any - saves to disk in workingDirectory/.attachments
|
|
66
|
+
const messageAttachments = attachments.createBatch(req.session_.id, null, files, req.workingDirectory);
|
|
67
|
+
|
|
68
|
+
// Check if the message is a slash command/skill invocation (starts with "/")
|
|
69
|
+
const resolved = await slashCommandService.resolvePromptSkillOrCommand(
|
|
70
|
+
req.workingDirectory, content, req.project.systemPrompt || null
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
if (resolved) {
|
|
74
|
+
continueSession(req.session_.id, resolved.userMessage, req.workingDirectory, { systemPrompt: resolved.systemPrompt, fileAttachments: messageAttachments, model }).catch((error) => {
|
|
75
|
+
console.error(`Continue session error (${resolved.type}):`, error);
|
|
76
|
+
});
|
|
77
|
+
return res.json({ success: true });
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Standard plain text message
|
|
81
|
+
continueSession(req.session_.id, content, req.workingDirectory, { systemPrompt: req.project.systemPrompt, fileAttachments: messageAttachments, model }).catch((error) => {
|
|
82
|
+
console.error('Continue session error:', error);
|
|
83
|
+
});
|
|
84
|
+
res.json({ success: true });
|
|
85
|
+
} catch (error) {
|
|
86
|
+
res.status(500).json({ error: error.message });
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// GET /api/sessions/:id/todos - Get session todos
|
|
91
|
+
// Supports ?conversation_id=xxx to fetch todos for a specific conversation
|
|
92
|
+
router.get('/:id/todos', requireSession, (req, res) => {
|
|
93
|
+
const { conversation_id } = req.query;
|
|
94
|
+
|
|
95
|
+
let sessionTodos;
|
|
96
|
+
if (conversation_id) {
|
|
97
|
+
// Get todos for specific conversation
|
|
98
|
+
const conv = conversations.getById(conversation_id);
|
|
99
|
+
if (!conv || conv.sessionId !== req.params.id) {
|
|
100
|
+
return res.status(404).json({ error: 'Conversation not found' });
|
|
101
|
+
}
|
|
102
|
+
sessionTodos = todos.getByConversationId(conversation_id);
|
|
103
|
+
} else {
|
|
104
|
+
// Get todos for active conversation, or empty array if no active conversation
|
|
105
|
+
const activeConv = conversations.getActiveBySessionId(req.params.id);
|
|
106
|
+
sessionTodos = activeConv ? todos.getByConversationId(activeConv.id) : [];
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
res.json(sessionTodos);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// GET /api/sessions/:id/workflow-latest-response - Get the most recent assistant response across the entire workflow
|
|
113
|
+
router.get('/:id/workflow-latest-response', requireSession, (req, res) => {
|
|
114
|
+
try {
|
|
115
|
+
// Find the root of the workflow
|
|
116
|
+
const rootId = sessions.getRootSessionId(req.params.id);
|
|
117
|
+
if (!rootId) {
|
|
118
|
+
return res.status(404).json({ error: 'Session not found' });
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Collect all session IDs in the workflow
|
|
122
|
+
const descendantIds = sessions.getAllDescendantIds(rootId);
|
|
123
|
+
const allSessionIds = [rootId, ...descendantIds];
|
|
124
|
+
|
|
125
|
+
// Find the most recent assistant message across all sessions
|
|
126
|
+
const message = messages.getLatestAssistantMessageForSessions(allSessionIds);
|
|
127
|
+
if (!message) {
|
|
128
|
+
return res.status(404).json({ error: 'No assistant response found' });
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Look up the session name for context
|
|
132
|
+
const messageSession = sessions.getById(message.sessionId);
|
|
133
|
+
const sessionName = messageSession?.name || null;
|
|
134
|
+
|
|
135
|
+
res.json({ message, sessionName });
|
|
136
|
+
} catch (error) {
|
|
137
|
+
res.status(500).json({ error: error.message });
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
export default router;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { Router } from 'express';
|
|
2
|
+
import { sessionNotes } from '../database.js';
|
|
3
|
+
import { requireSession } from '../middleware/sessionLookup.js';
|
|
4
|
+
|
|
5
|
+
const router = Router();
|
|
6
|
+
|
|
7
|
+
// GET /api/sessions/:id/notes - Get session notes
|
|
8
|
+
router.get('/:id/notes', requireSession, (req, res) => {
|
|
9
|
+
const notes = sessionNotes.getBySessionId(req.params.id);
|
|
10
|
+
res.json(notes);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
// POST /api/sessions/:id/notes - Create session note
|
|
14
|
+
router.post('/:id/notes', requireSession, (req, res) => {
|
|
15
|
+
const { content } = req.body;
|
|
16
|
+
if (!content) {
|
|
17
|
+
return res.status(400).json({ error: 'Content is required' });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const note = sessionNotes.create(req.params.id, content);
|
|
21
|
+
res.status(201).json(note);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
// PUT /api/sessions/:id/notes/:noteId - Update session note
|
|
25
|
+
router.put('/:id/notes/:noteId', (req, res) => {
|
|
26
|
+
const note = sessionNotes.getById(req.params.noteId);
|
|
27
|
+
if (!note || note.sessionId !== req.params.id) {
|
|
28
|
+
return res.status(404).json({ error: 'Note not found' });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const { content } = req.body;
|
|
32
|
+
if (!content) {
|
|
33
|
+
return res.status(400).json({ error: 'Content is required' });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const updated = sessionNotes.update(req.params.noteId, content);
|
|
37
|
+
res.json(updated);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// DELETE /api/sessions/:id/notes/:noteId - Delete session note
|
|
41
|
+
router.delete('/:id/notes/:noteId', (req, res) => {
|
|
42
|
+
const note = sessionNotes.getById(req.params.noteId);
|
|
43
|
+
if (!note || note.sessionId !== req.params.id) {
|
|
44
|
+
return res.status(404).json({ error: 'Note not found' });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
sessionNotes.delete(req.params.noteId);
|
|
48
|
+
res.status(204).send();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
export default router;
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import { Router } from 'express';
|
|
2
|
+
import { sessions, sessionTemplates, modelProviders } from '../database.js';
|
|
3
|
+
import { broadcastToSession, broadcastToProject } from '../websocket.js';
|
|
4
|
+
import { WS_MESSAGE_TYPES } from '../../../shared/src/index.js';
|
|
5
|
+
import * as summaryService from '../services/summaryService.js';
|
|
6
|
+
import { setSessionNameFromPr } from '../services/prUrlService.js';
|
|
7
|
+
import { requireSession } from '../middleware/sessionLookup.js';
|
|
8
|
+
|
|
9
|
+
const router = Router();
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Validate effortLevel field
|
|
13
|
+
* @param {*} value
|
|
14
|
+
* @returns {{ error?: string, value: * }}
|
|
15
|
+
*/
|
|
16
|
+
function validateEffortLevel(value) {
|
|
17
|
+
if (value === null) return { value };
|
|
18
|
+
const valid = ['low', 'medium', 'high', 'max', 'auto'];
|
|
19
|
+
if (!valid.includes(value)) {
|
|
20
|
+
return { error: 'Invalid effort level. Must be one of: low, medium, high, max, auto' };
|
|
21
|
+
}
|
|
22
|
+
// Normalize 'auto' to null
|
|
23
|
+
return { value: value === 'auto' ? null : value };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Validate status field
|
|
28
|
+
* @param {*} value
|
|
29
|
+
* @returns {{ error?: string, value: * }}
|
|
30
|
+
*/
|
|
31
|
+
function validateStatus(value) {
|
|
32
|
+
const valid = ['starting', 'running', 'waiting', 'error', 'stopped', 'scheduled'];
|
|
33
|
+
if (!valid.includes(value)) {
|
|
34
|
+
return { error: 'Invalid status' };
|
|
35
|
+
}
|
|
36
|
+
return { value };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Validate mode field
|
|
41
|
+
* @param {*} value
|
|
42
|
+
* @returns {{ error?: string, value: * }}
|
|
43
|
+
*/
|
|
44
|
+
function validateMode(value) {
|
|
45
|
+
const valid = ['plan', 'standard', 'yolo'];
|
|
46
|
+
if (!valid.includes(value)) {
|
|
47
|
+
return { error: 'Invalid mode. Must be one of: plan, standard, yolo' };
|
|
48
|
+
}
|
|
49
|
+
return { value };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Validate nextTemplateId field
|
|
54
|
+
* @param {*} value
|
|
55
|
+
* @returns {{ error?: string, value: * }}
|
|
56
|
+
*/
|
|
57
|
+
function validateNextTemplateId(value) {
|
|
58
|
+
if (value !== null) {
|
|
59
|
+
const template = sessionTemplates.getById(value);
|
|
60
|
+
if (!template) {
|
|
61
|
+
return { error: 'Template not found' };
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return { value };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Validate providerId field
|
|
69
|
+
* @param {*} value
|
|
70
|
+
* @returns {{ error?: string, value: * }}
|
|
71
|
+
*/
|
|
72
|
+
function validateProviderId(value) {
|
|
73
|
+
if (value !== null) {
|
|
74
|
+
const provider = modelProviders.getById(value);
|
|
75
|
+
if (!provider) {
|
|
76
|
+
return { error: 'Provider not found' };
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return { value };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Validate prUrl field
|
|
84
|
+
* @param {*} value
|
|
85
|
+
* @returns {{ error?: string, value: * }}
|
|
86
|
+
*/
|
|
87
|
+
function validatePrUrl(value) {
|
|
88
|
+
if (value === null || value === '') {
|
|
89
|
+
return { value: null };
|
|
90
|
+
}
|
|
91
|
+
if (typeof value !== 'string') {
|
|
92
|
+
return { error: 'prUrl must be a string or null' };
|
|
93
|
+
}
|
|
94
|
+
const prUrlPattern = /^https:\/\/github\.com\/[^/]+\/[^/]+\/pull\/\d+$/;
|
|
95
|
+
if (!prUrlPattern.test(value)) {
|
|
96
|
+
return { error: 'Invalid PR URL format. Must be a valid GitHub PR URL (e.g., https://github.com/owner/repo/pull/123)' };
|
|
97
|
+
}
|
|
98
|
+
return { value };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Field definitions for PATCH /:id with optional validators and transforms.
|
|
103
|
+
* Each entry maps a request body field name to its processing config.
|
|
104
|
+
*/
|
|
105
|
+
const FIELD_DEFINITIONS = [
|
|
106
|
+
{ field: 'name' },
|
|
107
|
+
{ field: 'manuallyNamed', transform: Boolean },
|
|
108
|
+
{ field: 'thinkingEnabled', transform: Boolean },
|
|
109
|
+
{ field: 'effortLevel', validate: validateEffortLevel },
|
|
110
|
+
{ field: 'status', validate: validateStatus },
|
|
111
|
+
{ field: 'mode', validate: validateMode },
|
|
112
|
+
{ field: 'nextTemplateId', validate: validateNextTemplateId },
|
|
113
|
+
{ field: 'model' },
|
|
114
|
+
{ field: 'pendingModel' },
|
|
115
|
+
{ field: 'autoSendPendingPrompt', transform: Boolean },
|
|
116
|
+
{ field: 'providerId', validate: validateProviderId },
|
|
117
|
+
{ field: 'prUrl', validate: validatePrUrl },
|
|
118
|
+
// Scheduling fields
|
|
119
|
+
{ field: 'scheduledAt' },
|
|
120
|
+
{ field: 'autoRescheduleEnabled', transform: Boolean },
|
|
121
|
+
{ field: 'rescheduleDelayMinutes', transform: (v) => parseInt(v, 10) },
|
|
122
|
+
{ field: 'rescheduleOnTokenLimit', transform: Boolean },
|
|
123
|
+
{ field: 'rescheduleOnServiceError', transform: Boolean },
|
|
124
|
+
{ field: 'maxRescheduleCount', transform: (v) => v ? parseInt(v, 10) : null },
|
|
125
|
+
{ field: 'maxTotalTokens', transform: (v) => v ? parseInt(v, 10) : null },
|
|
126
|
+
{ field: 'rescheduleCount', transform: (v) => parseInt(v, 10) },
|
|
127
|
+
{ field: 'rescheduleAtTokenCount', transform: (v) => v ? parseInt(v, 10) : null },
|
|
128
|
+
];
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Build update data object from request body using field definitions.
|
|
132
|
+
* Returns { updateData, error } where error is a string if validation failed.
|
|
133
|
+
* @param {object} body - The request body
|
|
134
|
+
* @returns {{ updateData: object, error?: string }}
|
|
135
|
+
*/
|
|
136
|
+
function buildUpdateData(body) {
|
|
137
|
+
const updateData = {};
|
|
138
|
+
|
|
139
|
+
for (const { field, validate, transform } of FIELD_DEFINITIONS) {
|
|
140
|
+
const value = body[field];
|
|
141
|
+
if (value === undefined) continue;
|
|
142
|
+
|
|
143
|
+
if (validate) {
|
|
144
|
+
const result = validate(value);
|
|
145
|
+
if (result.error) return { updateData: {}, error: result.error };
|
|
146
|
+
updateData[field] = result.value;
|
|
147
|
+
} else if (transform) {
|
|
148
|
+
updateData[field] = transform(value);
|
|
149
|
+
} else {
|
|
150
|
+
updateData[field] = value;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Special case: auto-set manuallyNamed when name is updated (unless explicitly provided)
|
|
155
|
+
if (body.name !== undefined && body.manuallyNamed === undefined) {
|
|
156
|
+
updateData.manuallyNamed = true;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return { updateData };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Broadcast session update to both session and project subscribers.
|
|
164
|
+
* @param {string} sessionId
|
|
165
|
+
* @param {string} projectId
|
|
166
|
+
* @param {object} updated - The updated session object
|
|
167
|
+
* @param {object} updateData - The fields that were updated
|
|
168
|
+
*/
|
|
169
|
+
function broadcastSessionUpdate(sessionId, projectId, updated, updateData) {
|
|
170
|
+
// Broadcast status update if status changed
|
|
171
|
+
if (updateData.status) {
|
|
172
|
+
broadcastToSession(sessionId, WS_MESSAGE_TYPES.SESSION_STATUS, {
|
|
173
|
+
sessionId,
|
|
174
|
+
status: updateData.status,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Broadcast session update to session subscribers (e.g. detail view)
|
|
179
|
+
broadcastToSession(sessionId, WS_MESSAGE_TYPES.SESSION_UPDATED, {
|
|
180
|
+
sessionId,
|
|
181
|
+
session: updated,
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// Broadcast session update to project subscribers for real-time list updates
|
|
185
|
+
broadcastToProject(projectId, WS_MESSAGE_TYPES.SESSION_UPDATED, {
|
|
186
|
+
projectId,
|
|
187
|
+
sessionId,
|
|
188
|
+
session: updated,
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// PATCH /api/sessions/:id - Update session settings
|
|
193
|
+
router.patch('/:id', requireSession, (req, res) => {
|
|
194
|
+
const { updateData, error } = buildUpdateData(req.body);
|
|
195
|
+
|
|
196
|
+
if (error) {
|
|
197
|
+
return res.status(400).json({ error });
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (Object.keys(updateData).length === 0) {
|
|
201
|
+
return res.status(400).json({ error: 'No valid fields to update' });
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const updated = sessions.update(req.params.id, updateData);
|
|
205
|
+
|
|
206
|
+
// Propagate PR URL to parent session if set (not when clearing)
|
|
207
|
+
if (updateData.prUrl) {
|
|
208
|
+
summaryService.propagatePrUrlToParent(req.params.id, updateData.prUrl);
|
|
209
|
+
|
|
210
|
+
// Set session name from PR title (fire-and-forget async)
|
|
211
|
+
// The response returns immediately; client gets name update via WebSocket
|
|
212
|
+
setSessionNameFromPr(req.params.id, updateData.prUrl).catch(err => {
|
|
213
|
+
console.error(`[Sessions API] Failed to set session name from PR:`, err);
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
broadcastSessionUpdate(req.params.id, req.session_.projectId, updated, updateData);
|
|
218
|
+
|
|
219
|
+
res.json(updated);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
// PATCH /api/sessions/:id/pending-prompt - Update pending prompt for auto-save
|
|
223
|
+
router.patch('/:id/pending-prompt', requireSession, (req, res) => {
|
|
224
|
+
const { pendingPrompt } = req.body;
|
|
225
|
+
|
|
226
|
+
// Allow null or string (including empty string for clearing)
|
|
227
|
+
if (pendingPrompt !== null && typeof pendingPrompt !== 'string') {
|
|
228
|
+
return res.status(400).json({ error: 'pendingPrompt must be a string or null' });
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const updated = sessions.update(req.params.id, { pendingPrompt });
|
|
232
|
+
|
|
233
|
+
// Broadcast update to session subscribers
|
|
234
|
+
broadcastToSession(req.params.id, WS_MESSAGE_TYPES.SESSION_UPDATED, {
|
|
235
|
+
sessionId: req.params.id,
|
|
236
|
+
session: updated,
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
// Broadcast to project subscribers for real-time updates
|
|
240
|
+
broadcastToProject(req.session_.projectId, WS_MESSAGE_TYPES.SESSION_UPDATED, {
|
|
241
|
+
projectId: req.session_.projectId,
|
|
242
|
+
sessionId: req.params.id,
|
|
243
|
+
session: updated,
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
res.json(updated);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
export default router;
|
|
250
|
+
|
|
251
|
+
// Export for testing
|
|
252
|
+
export { buildUpdateData, broadcastSessionUpdate, FIELD_DEFINITIONS };
|