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,281 @@
|
|
|
1
|
+
import os from 'os';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import { execFile, execFileSync } from 'child_process';
|
|
4
|
+
import { WS_MESSAGE_TYPES } from '../../../shared/src/index.js';
|
|
5
|
+
import { broadcast } from '../ws/index.js';
|
|
6
|
+
|
|
7
|
+
// Broadcast interval in milliseconds (5 seconds)
|
|
8
|
+
const BROADCAST_INTERVAL_MS = 5000;
|
|
9
|
+
|
|
10
|
+
// Interval handle
|
|
11
|
+
let intervalId = null;
|
|
12
|
+
|
|
13
|
+
// Previous CPU sample for delta computation
|
|
14
|
+
let prevCpus = null;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Compute CPU usage percentage from two cpu samples.
|
|
18
|
+
* @param {os.CpuInfo[]} prevCpus
|
|
19
|
+
* @param {os.CpuInfo[]} currCpus
|
|
20
|
+
* @returns {number} CPU usage percent (0-100)
|
|
21
|
+
*/
|
|
22
|
+
export function computeCpuUsage(prevCpuSamples, currCpuSamples) {
|
|
23
|
+
if (!prevCpuSamples || !currCpuSamples || prevCpuSamples.length !== currCpuSamples.length) {
|
|
24
|
+
return 0;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
let totalIdle = 0;
|
|
28
|
+
let totalTick = 0;
|
|
29
|
+
|
|
30
|
+
for (let i = 0; i < currCpuSamples.length; i++) {
|
|
31
|
+
const prev = prevCpuSamples[i].times;
|
|
32
|
+
const curr = currCpuSamples[i].times;
|
|
33
|
+
|
|
34
|
+
const idleDelta = curr.idle - prev.idle;
|
|
35
|
+
const userDelta = curr.user - prev.user;
|
|
36
|
+
const niceDelta = curr.nice - prev.nice;
|
|
37
|
+
const sysDelta = curr.sys - prev.sys;
|
|
38
|
+
const irqDelta = curr.irq - prev.irq;
|
|
39
|
+
|
|
40
|
+
const tickDelta = idleDelta + userDelta + niceDelta + sysDelta + irqDelta;
|
|
41
|
+
|
|
42
|
+
totalIdle += idleDelta;
|
|
43
|
+
totalTick += tickDelta;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (totalTick === 0) return 0;
|
|
47
|
+
|
|
48
|
+
const usagePercent = ((totalTick - totalIdle) / totalTick) * 100;
|
|
49
|
+
return Math.round(usagePercent * 10) / 10;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Get available memory bytes using platform-specific methods.
|
|
54
|
+
*
|
|
55
|
+
* os.freemem() only reports truly "free" memory, ignoring reclaimable
|
|
56
|
+
* caches and buffers. On macOS especially, this makes memory appear nearly
|
|
57
|
+
* exhausted even when the system is not under pressure.
|
|
58
|
+
*
|
|
59
|
+
* - macOS: uses vm_stat to sum free + purgeable + speculative pages
|
|
60
|
+
* - Linux: reads MemAvailable from /proc/meminfo
|
|
61
|
+
* - Fallback: os.freemem()
|
|
62
|
+
*
|
|
63
|
+
* @returns {number} Available memory in bytes
|
|
64
|
+
*/
|
|
65
|
+
export function getAvailableMemoryBytes() {
|
|
66
|
+
const platform = os.platform();
|
|
67
|
+
|
|
68
|
+
if (platform === 'darwin') {
|
|
69
|
+
try {
|
|
70
|
+
const stdout = execFileSync('vm_stat', { encoding: 'utf8', timeout: 3000 });
|
|
71
|
+
return parseVmStatOutput(stdout);
|
|
72
|
+
} catch {
|
|
73
|
+
return os.freemem();
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (platform === 'linux') {
|
|
78
|
+
try {
|
|
79
|
+
const content = fs.readFileSync('/proc/meminfo', 'utf8');
|
|
80
|
+
return parseMemInfoOutput(content);
|
|
81
|
+
} catch {
|
|
82
|
+
return os.freemem();
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return os.freemem();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Parse macOS vm_stat output to compute available memory bytes.
|
|
91
|
+
*
|
|
92
|
+
* macOS categorises physical pages as:
|
|
93
|
+
* - free: truly unused
|
|
94
|
+
* - inactive: not recently accessed, reclaimable immediately
|
|
95
|
+
* - speculative: speculatively allocated, reclaimable
|
|
96
|
+
* - purgeable: marked purgeable by apps, reclaimable
|
|
97
|
+
* - active: recently accessed by running processes
|
|
98
|
+
* - wired: locked by the kernel, cannot be paged out
|
|
99
|
+
* - compressed: compressed in the compressor
|
|
100
|
+
*
|
|
101
|
+
* "Available" = free + inactive + purgeable + speculative
|
|
102
|
+
* This matches what macOS considers available (not under pressure).
|
|
103
|
+
*
|
|
104
|
+
* @param {string} stdout - Output from vm_stat command
|
|
105
|
+
* @returns {number} Available memory in bytes
|
|
106
|
+
*/
|
|
107
|
+
export function parseVmStatOutput(stdout) {
|
|
108
|
+
if (!stdout || typeof stdout !== 'string') return os.freemem();
|
|
109
|
+
|
|
110
|
+
// First line contains page size, e.g. "Mach Virtual Memory Statistics: (page size of 16384 bytes)"
|
|
111
|
+
const pageSizeMatch = stdout.match(/page size of (\d+) bytes/);
|
|
112
|
+
const pageSize = pageSizeMatch ? parseInt(pageSizeMatch[1], 10) : 16384;
|
|
113
|
+
|
|
114
|
+
// Parse page counts - vm_stat format: "Pages free: 123456."
|
|
115
|
+
const getValue = (label) => {
|
|
116
|
+
const match = stdout.match(new RegExp(`${label}:\\s+(\\d+)`));
|
|
117
|
+
return match ? parseInt(match[1], 10) : 0;
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const free = getValue('Pages free');
|
|
121
|
+
const inactive = getValue('Pages inactive');
|
|
122
|
+
const purgeable = getValue('Pages purgeable');
|
|
123
|
+
const speculative = getValue('Pages speculative');
|
|
124
|
+
|
|
125
|
+
const availableBytes = (free + inactive + purgeable + speculative) * pageSize;
|
|
126
|
+
|
|
127
|
+
// Sanity check: if result is 0 or unreasonably small, fall back
|
|
128
|
+
if (availableBytes <= 0) return os.freemem();
|
|
129
|
+
|
|
130
|
+
return availableBytes;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Parse Linux /proc/meminfo to get MemAvailable.
|
|
135
|
+
*
|
|
136
|
+
* @param {string} content - Contents of /proc/meminfo
|
|
137
|
+
* @returns {number} Available memory in bytes
|
|
138
|
+
*/
|
|
139
|
+
export function parseMemInfoOutput(content) {
|
|
140
|
+
if (!content || typeof content !== 'string') return os.freemem();
|
|
141
|
+
|
|
142
|
+
const match = content.match(/MemAvailable:\s+(\d+)\s+kB/);
|
|
143
|
+
if (!match) return os.freemem();
|
|
144
|
+
|
|
145
|
+
return parseInt(match[1], 10) * 1024;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Compute memory metrics from OS.
|
|
150
|
+
* @param {number} [totalBytes] - Total memory bytes (defaults to os.totalmem()); injectable for testing
|
|
151
|
+
* @param {number} [availableBytes] - Available memory bytes (defaults to platform-aware available memory); injectable for testing
|
|
152
|
+
* @returns {{ usedPercent: number, usedGB: number, totalGB: number }}
|
|
153
|
+
*/
|
|
154
|
+
export function computeMemoryMetrics(totalBytes = os.totalmem(), availableBytes = getAvailableMemoryBytes()) {
|
|
155
|
+
const usedBytes = totalBytes - availableBytes;
|
|
156
|
+
|
|
157
|
+
const totalGB = Math.round((totalBytes / (1024 ** 3)) * 10) / 10;
|
|
158
|
+
const usedGB = Math.round((usedBytes / (1024 ** 3)) * 10) / 10;
|
|
159
|
+
const usedPercent = Math.round((usedBytes / totalBytes) * 1000) / 10;
|
|
160
|
+
|
|
161
|
+
return { usedPercent, usedGB, totalGB };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Parse `df -k /` output to extract disk metrics.
|
|
166
|
+
* @param {string} stdout
|
|
167
|
+
* @returns {{ usedPercent: number, freeGB: number, totalGB: number } | null}
|
|
168
|
+
*/
|
|
169
|
+
export function parseDfOutput(stdout) {
|
|
170
|
+
if (!stdout || typeof stdout !== 'string') return null;
|
|
171
|
+
|
|
172
|
+
const lines = stdout.trim().split('\n');
|
|
173
|
+
// df output: header line + data line(s)
|
|
174
|
+
// We want the second line (index 1)
|
|
175
|
+
if (lines.length < 2) return null;
|
|
176
|
+
|
|
177
|
+
const dataLine = lines[1].trim();
|
|
178
|
+
if (!dataLine) return null;
|
|
179
|
+
|
|
180
|
+
// df -k columns: Filesystem 1K-blocks Used Available Use% Mounted-on
|
|
181
|
+
const parts = dataLine.split(/\s+/);
|
|
182
|
+
if (parts.length < 5) return null;
|
|
183
|
+
|
|
184
|
+
const totalKB = parseInt(parts[1], 10);
|
|
185
|
+
const usedKB = parseInt(parts[2], 10);
|
|
186
|
+
const availKB = parseInt(parts[3], 10);
|
|
187
|
+
|
|
188
|
+
if (isNaN(totalKB) || isNaN(usedKB) || isNaN(availKB) || totalKB === 0) return null;
|
|
189
|
+
|
|
190
|
+
const totalGB = Math.round((totalKB / (1024 ** 2)) * 10) / 10;
|
|
191
|
+
const freeGB = Math.round((availKB / (1024 ** 2)) * 10) / 10;
|
|
192
|
+
// Use (total - available) instead of "used" column for usedPercent.
|
|
193
|
+
// On macOS APFS, the Used column only reports one volume snapshot, not total usage.
|
|
194
|
+
// On Linux, Used + Available ≠ Total due to reserved blocks.
|
|
195
|
+
const effectiveUsedKB = totalKB - availKB;
|
|
196
|
+
const usedPercent = Math.round((effectiveUsedKB / totalKB) * 1000) / 10;
|
|
197
|
+
|
|
198
|
+
return { usedPercent, freeGB, totalGB };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Collect disk metrics using df command.
|
|
203
|
+
* Returns null if df is unavailable or fails.
|
|
204
|
+
* @returns {Promise<{ usedPercent: number, freeGB: number, totalGB: number } | null>}
|
|
205
|
+
*/
|
|
206
|
+
async function collectDiskMetrics() {
|
|
207
|
+
return new Promise((resolve) => {
|
|
208
|
+
const controller = new AbortController();
|
|
209
|
+
const timeout = setTimeout(() => controller.abort(), 3000);
|
|
210
|
+
|
|
211
|
+
execFile('df', ['-k', '/'], { signal: controller.signal }, (error, stdout) => {
|
|
212
|
+
clearTimeout(timeout);
|
|
213
|
+
if (error) {
|
|
214
|
+
resolve(null);
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
resolve(parseDfOutput(stdout));
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Collect and broadcast system metrics.
|
|
224
|
+
*/
|
|
225
|
+
async function collectAndBroadcast() {
|
|
226
|
+
// CPU: sample current, compute delta from previous
|
|
227
|
+
const currCpus = os.cpus();
|
|
228
|
+
const cpuUsagePercent = computeCpuUsage(prevCpus, currCpus);
|
|
229
|
+
prevCpus = currCpus;
|
|
230
|
+
|
|
231
|
+
// Memory
|
|
232
|
+
const memory = computeMemoryMetrics();
|
|
233
|
+
|
|
234
|
+
// Disk (may be null on failure or Windows)
|
|
235
|
+
const disk = await collectDiskMetrics();
|
|
236
|
+
|
|
237
|
+
const payload = {
|
|
238
|
+
cpu: {
|
|
239
|
+
usagePercent: cpuUsagePercent,
|
|
240
|
+
coreCount: currCpus.length,
|
|
241
|
+
model: currCpus[0]?.model || 'Unknown',
|
|
242
|
+
},
|
|
243
|
+
memory,
|
|
244
|
+
disk,
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
broadcast(WS_MESSAGE_TYPES.SYSTEM_METRICS, payload);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Start the system monitor service.
|
|
252
|
+
* Broadcasts system metrics every 5 seconds.
|
|
253
|
+
*/
|
|
254
|
+
export function start() {
|
|
255
|
+
if (intervalId) {
|
|
256
|
+
console.log('[SystemMonitor] Already running');
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
console.log('[SystemMonitor] Starting system metrics broadcast');
|
|
261
|
+
|
|
262
|
+
// Take initial CPU sample before first interval fires
|
|
263
|
+
prevCpus = os.cpus();
|
|
264
|
+
|
|
265
|
+
intervalId = setInterval(collectAndBroadcast, BROADCAST_INTERVAL_MS);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Stop the system monitor service.
|
|
270
|
+
*/
|
|
271
|
+
export function stop() {
|
|
272
|
+
if (intervalId) {
|
|
273
|
+
clearInterval(intervalId);
|
|
274
|
+
intervalId = null;
|
|
275
|
+
prevCpus = null;
|
|
276
|
+
console.log('[SystemMonitor] Stopped system metrics broadcast');
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Export for testing
|
|
281
|
+
export { BROADCAST_INTERVAL_MS, collectAndBroadcast };
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { Liquid } from 'liquidjs';
|
|
2
|
+
import { sessions, sessionTemplates, sessionSummaries, projects } from '../database.js';
|
|
3
|
+
import { setupGitForSession } from './gitSessionSetup.js';
|
|
4
|
+
import { runSession } from './sessionManager.js';
|
|
5
|
+
import { broadcastToProject } from '../websocket.js';
|
|
6
|
+
import { WS_MESSAGE_TYPES } from '../../../shared/src/index.js';
|
|
7
|
+
|
|
8
|
+
const liquid = new Liquid();
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Get the root session (the original session that started a template chain)
|
|
12
|
+
* @param {Object} session - The current session
|
|
13
|
+
* @returns {Object} The root session (or the current session if it has no parent)
|
|
14
|
+
*/
|
|
15
|
+
export function getRootSession(session) {
|
|
16
|
+
let current = session;
|
|
17
|
+
while (current.parentSessionId) {
|
|
18
|
+
const parent = sessions.getById(current.parentSessionId);
|
|
19
|
+
if (!parent) break;
|
|
20
|
+
current = parent;
|
|
21
|
+
}
|
|
22
|
+
return current;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Build a template context object for a session and its summary.
|
|
27
|
+
*/
|
|
28
|
+
function buildSessionContext(session, summary) {
|
|
29
|
+
return {
|
|
30
|
+
id: session.id,
|
|
31
|
+
name: session.name,
|
|
32
|
+
status: session.status,
|
|
33
|
+
summary: summary?.fullSummary || summary?.shortSummary || 'No summary available',
|
|
34
|
+
shortSummary: summary?.shortSummary || 'No summary available',
|
|
35
|
+
fullSummary: summary?.fullSummary || 'No summary available',
|
|
36
|
+
keyActions: summary?.keyActions || [],
|
|
37
|
+
filesModified: summary?.filesModified || [],
|
|
38
|
+
outcome: summary?.outcome || session.status,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Render a template prompt with parent session and root session context
|
|
44
|
+
* @param {string} templatePrompt - The Liquid template string
|
|
45
|
+
* @param {{ parentSession: Object, parentSummary: Object|null, rootSession: Object, rootSummary: Object|null }} sessionContext - Session context objects
|
|
46
|
+
* @returns {Promise<string>} The rendered prompt
|
|
47
|
+
*/
|
|
48
|
+
export async function renderTemplatePrompt(templatePrompt, sessionContext) {
|
|
49
|
+
const { parentSession, parentSummary, rootSession, rootSummary } = sessionContext;
|
|
50
|
+
const context = {
|
|
51
|
+
parentSession: buildSessionContext(parentSession, parentSummary),
|
|
52
|
+
rootSession: buildSessionContext(rootSession, rootSummary),
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
return liquid.parseAndRender(templatePrompt, context);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Derive session settings from template and root session.
|
|
60
|
+
* Template values take precedence if set, otherwise inherit from root.
|
|
61
|
+
*/
|
|
62
|
+
function deriveSessionSettings(template, rootSession) {
|
|
63
|
+
return {
|
|
64
|
+
thinkingEnabled: template.thinkingEnabled !== null ? template.thinkingEnabled : rootSession.thinkingEnabled,
|
|
65
|
+
gitBranch: template.gitBranch || rootSession.gitBranch,
|
|
66
|
+
gitMode: template.gitMode || null,
|
|
67
|
+
model: template.model !== null ? template.model : rootSession.model,
|
|
68
|
+
mode: template.mode !== null ? template.mode : rootSession.mode,
|
|
69
|
+
effortLevel: template.effortLevel !== null ? template.effortLevel : rootSession.effortLevel,
|
|
70
|
+
// Inherit rescheduling settings from root session
|
|
71
|
+
autoRescheduleEnabled: rootSession.autoRescheduleEnabled,
|
|
72
|
+
rescheduleOnTokenLimit: rootSession.rescheduleOnTokenLimit,
|
|
73
|
+
rescheduleOnServiceError: rootSession.rescheduleOnServiceError,
|
|
74
|
+
rescheduleDelayMinutes: rootSession.rescheduleDelayMinutes,
|
|
75
|
+
rescheduleAtTokenCount: rootSession.rescheduleAtTokenCount,
|
|
76
|
+
maxRescheduleCount: rootSession.maxRescheduleCount,
|
|
77
|
+
maxTotalTokens: rootSession.maxTotalTokens,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Resolve the working directory for a new template-triggered session.
|
|
83
|
+
* Inherits the parent worktree if available, otherwise sets up git normally.
|
|
84
|
+
* @param {Object} parentSession - The parent session
|
|
85
|
+
* @param {Object} project - The project
|
|
86
|
+
* @param {Object} settings - Derived session settings
|
|
87
|
+
* @param {string} newSessionId - The new session ID
|
|
88
|
+
* @returns {Promise<{workingDirectory: string, gitWorktree: string|null}>}
|
|
89
|
+
*/
|
|
90
|
+
async function resolveWorkingDirectory(parentSession, project, settings, newSessionId) {
|
|
91
|
+
if (parentSession.gitWorktree) {
|
|
92
|
+
console.log(`Template trigger: Inheriting parent worktree: ${parentSession.gitWorktree}`);
|
|
93
|
+
return { workingDirectory: parentSession.gitWorktree, gitWorktree: parentSession.gitWorktree };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const gitSetup = await setupGitForSession({
|
|
97
|
+
projectDir: project.workingDirectory,
|
|
98
|
+
gitMode: settings.gitMode,
|
|
99
|
+
gitBranch: settings.gitBranch,
|
|
100
|
+
sessionId: newSessionId,
|
|
101
|
+
});
|
|
102
|
+
return { workingDirectory: gitSetup.workingDirectory, gitWorktree: gitSetup.gitWorktree };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Check if a session should trigger its next template and do so
|
|
107
|
+
* Called when a session completes (status changes to completed, error, or stopped)
|
|
108
|
+
* @param {string} sessionId - The session that just completed
|
|
109
|
+
*/
|
|
110
|
+
export async function checkAndTriggerNextTemplate(sessionId) {
|
|
111
|
+
const session = sessions.getById(sessionId);
|
|
112
|
+
if (!session) {
|
|
113
|
+
console.warn(`Template trigger: Session ${sessionId} not found`);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (!session.nextTemplateId) {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const template = sessionTemplates.getById(session.nextTemplateId);
|
|
122
|
+
if (!template) {
|
|
123
|
+
console.warn(`Template trigger: Template ${session.nextTemplateId} not found for session ${sessionId}`);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const project = projects.getById(session.projectId);
|
|
128
|
+
if (!project) {
|
|
129
|
+
console.warn(`Template trigger: Project ${session.projectId} not found for session ${sessionId}`);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
console.log(`Template trigger: Triggering template "${template.name}" after session "${session.name}"`);
|
|
134
|
+
|
|
135
|
+
try {
|
|
136
|
+
const parentSummary = sessionSummaries.getBySessionId(sessionId);
|
|
137
|
+
const rootSession = getRootSession(session);
|
|
138
|
+
const rootSummary = sessionSummaries.getBySessionId(rootSession.id);
|
|
139
|
+
|
|
140
|
+
const renderedPrompt = await renderTemplatePrompt(template.prompt, { parentSession: session, parentSummary, rootSession, rootSummary });
|
|
141
|
+
const settings = deriveSessionSettings(template, rootSession);
|
|
142
|
+
const newSessionName = `${template.name} (from: ${session.name})`;
|
|
143
|
+
|
|
144
|
+
const newSession = sessions.create(
|
|
145
|
+
session.projectId,
|
|
146
|
+
newSessionName,
|
|
147
|
+
renderedPrompt,
|
|
148
|
+
{
|
|
149
|
+
mode: settings.mode,
|
|
150
|
+
thinkingEnabled: settings.thinkingEnabled,
|
|
151
|
+
gitBranch: settings.gitBranch,
|
|
152
|
+
parentSessionId: null,
|
|
153
|
+
status: 'starting',
|
|
154
|
+
model: settings.model,
|
|
155
|
+
effortLevel: settings.effortLevel,
|
|
156
|
+
}
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
sessions.update(newSession.id, {
|
|
160
|
+
parentSessionId: session.id,
|
|
161
|
+
nextTemplateId: template.nextTemplateId || null,
|
|
162
|
+
autoRescheduleEnabled: settings.autoRescheduleEnabled,
|
|
163
|
+
rescheduleOnTokenLimit: settings.rescheduleOnTokenLimit,
|
|
164
|
+
rescheduleOnServiceError: settings.rescheduleOnServiceError,
|
|
165
|
+
rescheduleDelayMinutes: settings.rescheduleDelayMinutes,
|
|
166
|
+
rescheduleAtTokenCount: settings.rescheduleAtTokenCount,
|
|
167
|
+
maxRescheduleCount: settings.maxRescheduleCount,
|
|
168
|
+
maxTotalTokens: settings.maxTotalTokens,
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
const { workingDirectory, gitWorktree } = await resolveWorkingDirectory(session, project, settings, newSession.id);
|
|
172
|
+
|
|
173
|
+
if (gitWorktree) {
|
|
174
|
+
sessions.update(newSession.id, { gitWorktree });
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const updatedSession = sessions.getById(newSession.id);
|
|
178
|
+
broadcastToProject(session.projectId, WS_MESSAGE_TYPES.SESSION_CREATED, {
|
|
179
|
+
projectId: session.projectId,
|
|
180
|
+
session: updatedSession,
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
runSession(newSession.id, renderedPrompt, workingDirectory, { systemPrompt: project.systemPrompt, model: settings.model }).catch((error) => {
|
|
184
|
+
console.error(`Template trigger: Error running session ${newSession.id}:`, error);
|
|
185
|
+
const errorSession = sessions.update(newSession.id, { status: 'error', error: error.message });
|
|
186
|
+
broadcastToProject(session.projectId, WS_MESSAGE_TYPES.SESSION_UPDATED, {
|
|
187
|
+
projectId: session.projectId,
|
|
188
|
+
sessionId: newSession.id,
|
|
189
|
+
session: errorSession,
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
console.log(`Template trigger: Created and started session ${newSession.id}`);
|
|
194
|
+
} catch (error) {
|
|
195
|
+
console.error(`Template trigger: Failed to trigger template for session ${sessionId}:`, error);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Strip ANSI escape codes from text
|
|
3
|
+
* Removes all CSI (Control Sequence Introducer) sequences:
|
|
4
|
+
* - SGR codes: \x1b[...m (colors, bold, italic, etc.)
|
|
5
|
+
* - Cursor movement: \x1b[1A, \x1b[2B, etc.
|
|
6
|
+
* - Line/screen clearing: \x1b[2K, \x1b[0J, etc.
|
|
7
|
+
* - Other CSI sequences: \x1b[...H, \x1b[...J, etc.
|
|
8
|
+
*
|
|
9
|
+
* @param {string} text - Text potentially containing ANSI codes
|
|
10
|
+
* @returns {string} Text with all ANSI codes removed
|
|
11
|
+
*/
|
|
12
|
+
export function stripAnsiCodes(text) {
|
|
13
|
+
if (!text || typeof text !== 'string') {
|
|
14
|
+
return text;
|
|
15
|
+
}
|
|
16
|
+
// Match all CSI sequences: ESC [ <params> <final-char>
|
|
17
|
+
// This covers colors, cursor movement, line clearing, and other terminal control sequences
|
|
18
|
+
// eslint-disable-next-line no-control-regex
|
|
19
|
+
return text.replace(/\x1b\[[0-9;?]*[A-Za-z]/g, '');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Terminal output processor that simulates cursor control behavior
|
|
24
|
+
*
|
|
25
|
+
* When tools like yarn run in TTY mode, they use cursor control sequences to create
|
|
26
|
+
* animated progress displays. For example:
|
|
27
|
+
* \x1b[2K\x1b[1G[] 0/576 <- clear line, go to column 1, print progress
|
|
28
|
+
* \x1b[2K\x1b[1G[] 136/576 <- clear line, go to column 1, print NEW progress
|
|
29
|
+
*
|
|
30
|
+
* On a real terminal, the second line overwrites the first. But when we just strip
|
|
31
|
+
* the codes, we get "[] 0/576[] 136/576" concatenated together.
|
|
32
|
+
*
|
|
33
|
+
* This processor simulates the terminal behavior by:
|
|
34
|
+
* 1. Maintaining a "current line" buffer (content since last newline)
|
|
35
|
+
* 2. When we see \x1b[2K (clear line), we clear the current line buffer
|
|
36
|
+
* 3. When we see \x1b[1G (cursor to column 1), we clear the current line buffer
|
|
37
|
+
* 4. When we see \r (carriage return), we clear the current line buffer
|
|
38
|
+
* 5. When we see \n, we flush the current line and start fresh
|
|
39
|
+
* 6. All ANSI escape codes are stripped from output
|
|
40
|
+
*/
|
|
41
|
+
export class TerminalOutputProcessor {
|
|
42
|
+
constructor() {
|
|
43
|
+
/** @type {string} Content on the current line (since last newline) */
|
|
44
|
+
this.currentLine = '';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Handle a CSI (Control Sequence Introducer) command
|
|
49
|
+
* Returns true if the current line should be cleared
|
|
50
|
+
* @param {string} cmd - The command character
|
|
51
|
+
* @param {string} params - The parameters string
|
|
52
|
+
* @returns {boolean} Whether to clear the current line
|
|
53
|
+
*/
|
|
54
|
+
#shouldClearLineForCSI(cmd, params) {
|
|
55
|
+
// Erase in Line: [0K = to end, [1K = to start, [2K = entire line
|
|
56
|
+
if (cmd === 'K') return true;
|
|
57
|
+
// Cursor Character Absolute: [1G = go to column 1 (start of line)
|
|
58
|
+
if (cmd === 'G' && (params === '' || params === '1')) return true;
|
|
59
|
+
// Cursor Position: [n;mH or [n;mf moves cursor to row n, column m
|
|
60
|
+
if (cmd === 'H' || cmd === 'f') return true;
|
|
61
|
+
// Cursor movement: A=up, B=down, C=right, D=left
|
|
62
|
+
if (cmd === 'A' || cmd === 'B' || cmd === 'C' || cmd === 'D') return true;
|
|
63
|
+
// Erase in Display: [0J = to end, [1J = to start, [2J = entire screen
|
|
64
|
+
if (cmd === 'J') return true;
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Try to parse and handle a CSI escape sequence starting at position i.
|
|
70
|
+
* Returns the new index past the sequence, or -1 if not a complete CSI sequence.
|
|
71
|
+
* @param {string} chunk
|
|
72
|
+
* @param {number} i - current index (pointing at ESC)
|
|
73
|
+
* @returns {number} new index past the sequence, or -1
|
|
74
|
+
*/
|
|
75
|
+
#handleCSISequence(chunk, i) {
|
|
76
|
+
if (chunk[i] !== '\x1b' || chunk[i + 1] !== '[') return -1;
|
|
77
|
+
// Parse the CSI sequence: ESC [ <params> <command>
|
|
78
|
+
let j = i + 2;
|
|
79
|
+
while (j < chunk.length && /[0-9;?]/.test(chunk[j])) {
|
|
80
|
+
j++;
|
|
81
|
+
}
|
|
82
|
+
if (j >= chunk.length) return -1;
|
|
83
|
+
|
|
84
|
+
const cmd = chunk[j];
|
|
85
|
+
const params = chunk.slice(i + 2, j);
|
|
86
|
+
|
|
87
|
+
// Handle cursor control sequences that affect line content
|
|
88
|
+
if (this.#shouldClearLineForCSI(cmd, params)) this.currentLine = '';
|
|
89
|
+
// All other sequences (including color codes 'm') are just stripped
|
|
90
|
+
|
|
91
|
+
return j + 1;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Process a chunk of terminal output, simulating cursor control behavior
|
|
96
|
+
*
|
|
97
|
+
* @param {string} chunk - Raw terminal output chunk
|
|
98
|
+
* @returns {string} Processed output with cursor behavior simulated and ANSI codes stripped
|
|
99
|
+
*/
|
|
100
|
+
process(chunk) {
|
|
101
|
+
if (!chunk || typeof chunk !== 'string') {
|
|
102
|
+
return '';
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
let output = '';
|
|
106
|
+
let i = 0;
|
|
107
|
+
|
|
108
|
+
while (i < chunk.length) {
|
|
109
|
+
// Check for ESC sequence
|
|
110
|
+
const csiEnd = this.#handleCSISequence(chunk, i);
|
|
111
|
+
if (csiEnd >= 0) {
|
|
112
|
+
i = csiEnd;
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Handle carriage return - go to start of line (used for overwriting)
|
|
117
|
+
if (chunk[i] === '\r') {
|
|
118
|
+
// Don't clear if next char is \n (normal line ending)
|
|
119
|
+
if (chunk[i + 1] !== '\n') {
|
|
120
|
+
this.currentLine = '';
|
|
121
|
+
}
|
|
122
|
+
i++;
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Handle newline - flush current line and start fresh
|
|
127
|
+
if (chunk[i] === '\n') {
|
|
128
|
+
output += `${this.currentLine }\n`;
|
|
129
|
+
this.currentLine = '';
|
|
130
|
+
i++;
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Regular character - add to current line
|
|
135
|
+
this.currentLine += chunk[i];
|
|
136
|
+
i++;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return output;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Flush any remaining content in the current line buffer
|
|
144
|
+
* Call this when the stream ends to get the final incomplete line
|
|
145
|
+
*
|
|
146
|
+
* @returns {string} Any remaining content
|
|
147
|
+
*/
|
|
148
|
+
flush() {
|
|
149
|
+
const remaining = this.currentLine;
|
|
150
|
+
this.currentLine = '';
|
|
151
|
+
return remaining;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Reset the processor state
|
|
156
|
+
*/
|
|
157
|
+
reset() {
|
|
158
|
+
this.currentLine = '';
|
|
159
|
+
}
|
|
160
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { todos, conversations } from '../database.js';
|
|
2
|
+
import { broadcastToSession } from '../websocket.js';
|
|
3
|
+
import { WS_MESSAGE_TYPES } from '../../../shared/src/index.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Update todos for a conversation (replaces all existing todos for that conversation)
|
|
7
|
+
* Called when Claude executes TodoWrite
|
|
8
|
+
* @param {string} sessionId
|
|
9
|
+
* @param {string} conversationId
|
|
10
|
+
* @param {Array<{content: string, status: string}>} todoList
|
|
11
|
+
* @returns {Array}
|
|
12
|
+
*/
|
|
13
|
+
export function updateTodos(sessionId, conversationId, todoList) {
|
|
14
|
+
const updatedTodos = todos.replaceAllForConversation(sessionId, conversationId, todoList);
|
|
15
|
+
broadcastToSession(sessionId, WS_MESSAGE_TYPES.TODOS_UPDATE, {
|
|
16
|
+
sessionId,
|
|
17
|
+
conversationId,
|
|
18
|
+
todos: updatedTodos,
|
|
19
|
+
});
|
|
20
|
+
return updatedTodos;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Get all todos for a conversation
|
|
25
|
+
* @param {string} conversationId
|
|
26
|
+
* @returns {Array}
|
|
27
|
+
*/
|
|
28
|
+
export function getTodosByConversation(conversationId) {
|
|
29
|
+
return todos.getByConversationId(conversationId);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Get all todos for the active conversation of a session
|
|
34
|
+
* Falls back to empty array if no active conversation
|
|
35
|
+
* @param {string} sessionId
|
|
36
|
+
* @returns {Array}
|
|
37
|
+
*/
|
|
38
|
+
export function getTodosForSession(sessionId) {
|
|
39
|
+
const activeConv = conversations.getActiveBySessionId(sessionId);
|
|
40
|
+
if (activeConv) {
|
|
41
|
+
return todos.getByConversationId(activeConv.id);
|
|
42
|
+
}
|
|
43
|
+
return [];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Clear all todos for a conversation
|
|
48
|
+
* @param {string} sessionId
|
|
49
|
+
* @param {string} conversationId
|
|
50
|
+
*/
|
|
51
|
+
export function clearTodos(sessionId, conversationId) {
|
|
52
|
+
todos.deleteByConversationId(conversationId);
|
|
53
|
+
broadcastToSession(sessionId, WS_MESSAGE_TYPES.TODOS_UPDATE, {
|
|
54
|
+
sessionId,
|
|
55
|
+
conversationId,
|
|
56
|
+
todos: [],
|
|
57
|
+
});
|
|
58
|
+
}
|