farming-code 2.2.30 → 2.2.32
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/THIRD_PARTY_NOTICES.md +7 -0
- package/backend/acp/{client-services.js → client-services.cjs} +132 -134
- package/backend/acp/packaged-claude-acp.cjs +37 -0
- package/backend/acp/packaged-codex-acp.cjs +37 -0
- package/backend/acp/patch-decisions.cjs +182 -0
- package/backend/acp/permission-security.cjs +131 -0
- package/backend/{acp-checkpoint-store.js → acp-checkpoint-store.cjs} +90 -59
- package/backend/{acp-runtime.js → acp-runtime.cjs} +1220 -1112
- package/backend/acp-session-state.cjs +622 -0
- package/backend/acp-transcript.cjs +525 -0
- package/backend/agent-env.cjs +257 -0
- package/backend/agent-extension-discovery.cjs +452 -0
- package/backend/agent-extension-inventory.cjs +116 -0
- package/backend/agent-json-stream.cjs +283 -0
- package/backend/agent-lifecycle-journal.cjs +185 -0
- package/backend/{agent-manager.js → agent-manager.cjs} +3116 -4275
- package/backend/agent-order-transaction.cjs +85 -0
- package/backend/agent-order.cjs +159 -0
- package/backend/agent-provider-session.cjs +98 -0
- package/backend/agent-runtime-binding.cjs +179 -0
- package/backend/agent-session-history.cjs +1022 -0
- package/backend/agent-session-inventory.cjs +291 -0
- package/backend/async-cache.cjs +104 -0
- package/backend/atomic-json-store.cjs +67 -0
- package/backend/auth.cjs +261 -0
- package/backend/authoritative-inventory-cache.cjs +383 -0
- package/backend/business-health.cjs +68 -0
- package/backend/chat-runtime.cjs +50 -0
- package/backend/claude-settings.cjs +132 -0
- package/backend/cli-agents.cjs +435 -0
- package/backend/codex-context-window.cjs +196 -0
- package/backend/codex-models.cjs +228 -0
- package/backend/codex-session-archive.cjs +82 -0
- package/backend/codex-session-history.cjs +590 -0
- package/backend/{codex-terminal-profile.js → codex-terminal-profile.cjs} +149 -155
- package/backend/codex-transcript-sanitizer.cjs +130 -0
- package/backend/codex-transcript.cjs +2107 -0
- package/backend/command-runner-child.cjs +105 -0
- package/backend/config-manager.cjs +1263 -0
- package/backend/control-api.cjs +439 -0
- package/backend/data/LICENSE.cua +21 -0
- package/backend/executable-discovery.cjs +204 -0
- package/backend/farming-agent-bootstrap.cjs +81 -0
- package/backend/farming-agent-bootstrap.zh_cn.md +6 -2
- package/backend/{farming-app-cli.js → farming-app-cli.cjs} +441 -493
- package/backend/farming-cli.cjs +414 -0
- package/backend/farming-net-pass.cjs +319 -0
- package/backend/farming-net-registry.cjs +123 -0
- package/backend/farming-net-server.cjs +309 -0
- package/backend/farming-session-store.cjs +651 -0
- package/backend/git-worktree-info.cjs +181 -0
- package/backend/haiku-token.cjs +828 -0
- package/backend/index-html.cjs +96 -0
- package/backend/input-parts.cjs +68 -0
- package/backend/input-routing.cjs +40 -0
- package/backend/{local-session-engine.js → local-session-engine.cjs} +276 -282
- package/backend/main-agent-skills.cjs +309 -0
- package/backend/main-page-session.cjs +132 -0
- package/backend/native-pty-controller-generation.cjs +139 -0
- package/backend/{native-pty-host-client.js → native-pty-host-client.cjs} +338 -346
- package/backend/native-pty-host-identity.cjs +125 -0
- package/backend/native-pty-host-path.cjs +68 -0
- package/backend/{native-pty-host.js → native-pty-host.cjs} +480 -569
- package/backend/native-session-engine.cjs +296 -0
- package/backend/network.cjs +57 -0
- package/backend/npm-update-helper.cjs +399 -0
- package/backend/packaged-node-pty.cjs +120 -0
- package/backend/preview-session-manager.cjs +89 -0
- package/backend/provider-adapters.cjs +492 -0
- package/backend/provider-home-configuration.cjs +211 -0
- package/backend/provider-session-id.cjs +79 -0
- package/backend/provider-session-service.cjs +333 -0
- package/backend/qr-share-tickets.cjs +119 -0
- package/backend/resource-broadcast-protocol.cjs +50 -0
- package/backend/review-diff-router.cjs +202 -0
- package/backend/review-diff-service.cjs +1267 -0
- package/backend/review-session-router.cjs +93 -0
- package/backend/review-session-service.cjs +470 -0
- package/backend/review-session-store.cjs +161 -0
- package/backend/review-state-router.cjs +132 -0
- package/backend/review-state-store.cjs +299 -0
- package/backend/run-history-store.cjs +97 -0
- package/backend/runtime-dependency-manager.cjs +893 -0
- package/backend/runtime-executable-invocation.cjs +37 -0
- package/backend/runtime-observation.cjs +96 -0
- package/backend/server-process-identity.cjs +70 -0
- package/backend/server.cjs +3356 -0
- package/backend/session-engine-bridge.cjs +144 -0
- package/backend/session-engine-router.cjs +105 -0
- package/backend/session-engine.cjs +64 -0
- package/backend/session-stream-protocol.cjs +181 -0
- package/backend/shell-busy-integration.cjs +442 -0
- package/backend/slash-command-discovery.cjs +423 -0
- package/backend/storage-layout.cjs +263 -0
- package/backend/system-monitor.cjs +96 -0
- package/backend/terminal-attach-checkpoint.cjs +73 -0
- package/backend/terminal-exit-quiescence.cjs +57 -0
- package/backend/{terminal-reducer-flow-control.js → terminal-reducer-flow-control.cjs} +45 -22
- package/backend/terminal-runtime-cleanup.cjs +99 -0
- package/backend/{terminal-screen-state.js → terminal-screen-state.cjs} +100 -108
- package/backend/{terminal-screen-worker-pool.js → terminal-screen-worker-pool.cjs} +89 -79
- package/backend/{terminal-screen-worker-thread.js → terminal-screen-worker-thread.cjs} +64 -65
- package/backend/terminal-screen-worker.cjs +352 -0
- package/backend/terminal-state-serialization.cjs +147 -0
- package/backend/{terminal-status.js → terminal-status.cjs} +137 -187
- package/backend/theme-manager.cjs +185 -0
- package/backend/{update-service.js → update-service.cjs} +258 -248
- package/backend/usage-forecast.cjs +95 -0
- package/backend/{usage-history-client.js → usage-history-client.cjs} +144 -94
- package/backend/usage-history-smoke.cjs +85 -0
- package/backend/usage-history-worker.cjs +60 -0
- package/backend/{usage-monitor.js → usage-monitor.cjs} +363 -390
- package/backend/workspace-directory.cjs +239 -0
- package/backend/workspace-discovery.cjs +271 -0
- package/backend/{workspace-file-router.js → workspace-file-router.cjs} +213 -282
- package/backend/{workspace-file-service.js → workspace-file-service.cjs} +1139 -1318
- package/backend/workspace-root-registry.cjs +192 -0
- package/bin/farming +1 -1
- package/dist/acp/codex-acp-1.1.4.mjs +18 -1
- package/dist/assets/App-BCkdpWB6.css +1 -0
- package/dist/assets/App-CRjKpaGp.js +697 -0
- package/dist/assets/CodeSelect-Ci8sKZPU.js +1 -0
- package/dist/assets/CodeSelect-T2eNzrOc.css +1 -0
- package/dist/assets/{FileEditorMarkdownPreview-Cs9cLv8a.js → FileEditorMarkdownPreview-V06QHlAg.js} +55 -55
- package/dist/assets/FileEditorPane-BX-Wjg9U.js +2 -0
- package/dist/assets/IconGlyphs-YhV7tqd4.js +1 -0
- package/dist/assets/ProjectFilesSection-BDT2_r0o.js +12 -0
- package/dist/assets/ReviewPage-BqAs5sG1.js +3 -0
- package/dist/assets/code-dark-B7eM_9G9.css +1 -0
- package/dist/assets/{cssMode-Bdg5RpcJ.js → cssMode-ADdAlxTO.js} +1 -1
- package/dist/assets/{editor.api2-D74ufkbr.js → editor.api2-BFLAr0YF.js} +162 -162
- package/dist/assets/file-icons-BpeqWkIB.js +1 -0
- package/dist/assets/{freemarker2-DfMDJfnE.js → freemarker2-C90q5jrp.js} +1 -1
- package/dist/assets/{handlebars-BEdPtzMT.js → handlebars-BwUQcEDq.js} +1 -1
- package/dist/assets/{html-B1fPb1QD.js → html-1s3uPKfh.js} +1 -1
- package/dist/assets/{htmlMode-CY2ZjHxH.js → htmlMode-xtKD99de.js} +1 -1
- package/dist/assets/{index-D-zetSol.js → index-C8MchCem.js} +3 -3
- package/dist/assets/{javascript-BV2OsWA0.js → javascript-Ph7ijyny.js} +1 -1
- package/dist/assets/{jsonMode-zd07aeuX.js → jsonMode-D-jGAnFH.js} +1 -1
- package/dist/assets/linkComputer-CbGyI_qx.js +1 -0
- package/dist/assets/{liquid-BParI24X.js → liquid-0IamE_cI.js} +1 -1
- package/dist/assets/{lspLanguageFeatures-BzredkoV.js → lspLanguageFeatures-BxnQunWF.js} +1 -1
- package/dist/assets/main-CLKXa6hf.css +1 -0
- package/dist/assets/{mdx-vGzUJN98.js → mdx-DdNmfivn.js} +1 -1
- package/dist/assets/{monaco.contribution-BmaeBQEc.js → monaco.contribution-B43jsUs1.js} +2 -2
- package/dist/assets/{python-CUwdLgsI.js → python-IAt4X5TS.js} +1 -1
- package/dist/assets/{razor-BScebUxU.js → razor-DRkFYW0S.js} +1 -1
- package/dist/assets/review-C8-oPKXk.css +1 -0
- package/dist/assets/{tsMode-DzUAibXK.js → tsMode-_d_4U2gf.js} +1 -1
- package/dist/assets/{typescript-D9EOeM7N.js → typescript-C7zcMq9l.js} +1 -1
- package/dist/assets/{workers-DAiomYZM.js → workers-D0nOyJzf.js} +1 -1
- package/dist/assets/workspace-editor-model-DND_Y-Bj.js +1 -0
- package/dist/assets/workspace-editor-monaco-BPWhgcIB.js +4 -0
- package/dist/assets/workspace-editor-monaco-N641qPjX.js +1 -0
- package/dist/assets/workspace-view-state-k1bTlJ_Z.js +13 -0
- package/dist/assets/{xml-wWl1dhkQ.js → xml-BR-HvxT8.js} +1 -1
- package/dist/assets/{yaml-Dc2WDaOZ.js → yaml-YtFQwh4q.js} +1 -1
- package/dist/index.html +1 -1
- package/extensions/browser/backend/agent-browser-runtime.cjs +1040 -0
- package/extensions/browser/backend/agent-capability.cjs +85 -0
- package/extensions/browser/backend/browser-mcp-server.cjs +655 -0
- package/extensions/browser/backend/browser-resource-manager.cjs +1486 -0
- package/extensions/browser/backend/browser-resource-store.cjs +228 -0
- package/extensions/browser/backend/browser-router.cjs +203 -0
- package/extensions/browser/backend/executable-discovery.cjs +248 -0
- package/extensions/browser/backend/farming-browser-client.cjs +116 -0
- package/extensions/browser/backend/index.cjs +32 -0
- package/extensions/browser/backend/managed-chromium-installer.cjs +833 -0
- package/extensions/browser/bin/farming-browser +16 -7
- package/extensions/computer/backend/agent-capability.cjs +72 -0
- package/extensions/computer/backend/computer-constants.cjs +59 -0
- package/extensions/computer/backend/computer-mcp-server.cjs +182 -0
- package/extensions/computer/backend/computer-resource-manager.cjs +1219 -0
- package/extensions/computer/backend/computer-resource-store.cjs +190 -0
- package/extensions/computer/backend/computer-router.cjs +257 -0
- package/extensions/computer/backend/cua-tools.json +3067 -0
- package/extensions/computer/backend/farming-computer-client.cjs +84 -0
- package/extensions/computer/backend/index.cjs +35 -0
- package/extensions/computer/backend/isolated-browser-provider.cjs +157 -0
- package/extensions/computer/bin/farming-computer +131 -0
- package/frontend/farming-net/app.js +255 -262
- package/frontend/ghostty-loader.js +24 -20
- package/frontend/reading-anchor.js +210 -193
- package/frontend/runtime-paths.js +24 -26
- package/frontend/session-bridge.js +90 -111
- package/frontend/session-modal-bridge.js +184 -302
- package/frontend/skin-bridge.js +20 -23
- package/frontend/skins/crt/app.js +7686 -7901
- package/frontend/skins/crt/effects.js +7 -6
- package/frontend/skins/crt/index.html +6 -6
- package/frontend/skins/crt/styles/markdown.css +4 -4
- package/frontend/terminal-bridge.js +174 -181
- package/frontend/terminal-replay.js +320 -345
- package/frontend/theme-loader.js +55 -71
- package/package.json +64 -37
- package/shared/backend-connection-status.js +38 -69
- package/shared/browser-protocol.js +235 -110
- package/shared/websocket-liveness.js +40 -0
- package/backend/acp/packaged-claude-acp.js +0 -13
- package/backend/acp/packaged-codex-acp.js +0 -13
- package/backend/acp/patch-decisions.js +0 -153
- package/backend/acp/permission-security.js +0 -108
- package/backend/acp-session-state.js +0 -634
- package/backend/acp-transcript.js +0 -512
- package/backend/agent-env.js +0 -250
- package/backend/agent-json-stream.js +0 -268
- package/backend/agent-lifecycle-journal.js +0 -133
- package/backend/agent-order.js +0 -166
- package/backend/agent-provider-session.js +0 -76
- package/backend/agent-runtime-binding.js +0 -174
- package/backend/agent-session-history.js +0 -986
- package/backend/async-cache.js +0 -94
- package/backend/atomic-json-store.js +0 -37
- package/backend/auth.js +0 -251
- package/backend/chat-runtime.js +0 -28
- package/backend/claude-settings.js +0 -121
- package/backend/cli-agents.js +0 -486
- package/backend/codex-context-window.js +0 -189
- package/backend/codex-models.js +0 -226
- package/backend/codex-session-archive.js +0 -65
- package/backend/codex-session-history.js +0 -647
- package/backend/codex-transcript-sanitizer.js +0 -115
- package/backend/codex-transcript.js +0 -2362
- package/backend/command-runner-child.js +0 -67
- package/backend/config-manager.js +0 -1117
- package/backend/control-api.js +0 -455
- package/backend/executable-discovery.js +0 -216
- package/backend/farming-agent-bootstrap.js +0 -49
- package/backend/farming-cli.js +0 -403
- package/backend/farming-net-pass.js +0 -285
- package/backend/farming-net-registry.js +0 -112
- package/backend/farming-net-server.js +0 -298
- package/backend/farming-session-store.js +0 -677
- package/backend/git-worktree-info.js +0 -181
- package/backend/haiku-token.js +0 -772
- package/backend/index-html.js +0 -80
- package/backend/input-parts.js +0 -54
- package/backend/input-routing.js +0 -24
- package/backend/json-cli-runtime.js +0 -275
- package/backend/main-agent-skills.js +0 -288
- package/backend/main-page-session.js +0 -122
- package/backend/native-pty-controller-generation.js +0 -106
- package/backend/native-pty-host-identity.js +0 -86
- package/backend/native-pty-host-path.js +0 -42
- package/backend/native-session-engine.js +0 -268
- package/backend/network.js +0 -23
- package/backend/npm-update-helper.js +0 -374
- package/backend/packaged-node-pty.js +0 -99
- package/backend/preview-session-manager.js +0 -68
- package/backend/provider-adapters.js +0 -399
- package/backend/provider-session-id.js +0 -47
- package/backend/provider-session-service.js +0 -345
- package/backend/qr-share-tickets.js +0 -100
- package/backend/review-diff-router.js +0 -180
- package/backend/review-diff-service.js +0 -1270
- package/backend/review-session-router.js +0 -74
- package/backend/review-session-service.js +0 -446
- package/backend/review-session-store.js +0 -151
- package/backend/review-state-router.js +0 -116
- package/backend/review-state-store.js +0 -286
- package/backend/run-history-store.js +0 -67
- package/backend/runtime-dependency-manager.js +0 -759
- package/backend/runtime-executable-invocation.js +0 -20
- package/backend/runtime-observation.js +0 -81
- package/backend/server-process-identity.js +0 -47
- package/backend/server.js +0 -3225
- package/backend/session-engine-bridge.js +0 -129
- package/backend/session-engine-router.js +0 -66
- package/backend/session-engine.js +0 -39
- package/backend/session-stream-protocol.js +0 -185
- package/backend/shell-busy-integration.js +0 -472
- package/backend/slash-command-discovery.js +0 -433
- package/backend/storage-layout.js +0 -205
- package/backend/system-monitor.js +0 -77
- package/backend/terminal-attach-checkpoint.js +0 -74
- package/backend/terminal-exit-quiescence.js +0 -39
- package/backend/terminal-runtime-cleanup.js +0 -80
- package/backend/terminal-screen-worker.js +0 -337
- package/backend/terminal-state-serialization.js +0 -127
- package/backend/theme-manager.js +0 -163
- package/backend/usage-forecast.js +0 -86
- package/backend/usage-history-scanner.ts +0 -2718
- package/backend/usage-history-smoke.js +0 -64
- package/backend/usage-history-worker.js +0 -34
- package/backend/workspace-directory.js +0 -232
- package/backend/workspace-discovery.js +0 -311
- package/backend/workspace-root-registry.js +0 -166
- package/dist/assets/App-BKNUILuL.css +0 -1
- package/dist/assets/App-Z1N7T0eM.js +0 -695
- package/dist/assets/FileEditorPane-CNtuGjQ3.js +0 -2
- package/dist/assets/IconGlyphs-BClgag6E.js +0 -1
- package/dist/assets/ProjectFilesSection-K3GHuQ4U.js +0 -12
- package/dist/assets/ReviewPage-BRuxFaH5.js +0 -3
- package/dist/assets/code-dark-kLsQLvpf.css +0 -1
- package/dist/assets/file-icons-BOlcJ0ZF.js +0 -1
- package/dist/assets/main-DUTz-Rd2.css +0 -1
- package/dist/assets/review-BcnwQtJv.css +0 -1
- package/dist/assets/workspace-editor-model-CSGCl21x.js +0 -1
- package/dist/assets/workspace-editor-monaco-CJNwbpCy.js +0 -4
- package/dist/assets/workspace-editor-monaco-CO9w8YV0.js +0 -1
- package/dist/assets/workspace-view-state-BjKPYp0E.js +0 -7
- package/extensions/browser/backend/agent-browser-runtime.js +0 -1041
- package/extensions/browser/backend/agent-capability.js +0 -55
- package/extensions/browser/backend/browser-mcp-server.js +0 -621
- package/extensions/browser/backend/browser-resource-manager.js +0 -1331
- package/extensions/browser/backend/browser-resource-store.js +0 -190
- package/extensions/browser/backend/browser-router.js +0 -141
- package/extensions/browser/backend/executable-discovery.js +0 -238
- package/extensions/browser/backend/farming-browser-client.js +0 -83
- package/extensions/browser/backend/index.js +0 -7
- package/extensions/browser/backend/managed-chromium-installer.js +0 -593
- package/extensions/browser/frontend/BrowserSidebarPortals.tsx +0 -360
- package/extensions/browser/frontend/BrowserViewer.tsx +0 -500
- package/extensions/browser/frontend/browser-resource-state.ts +0 -60
- package/extensions/browser/frontend/browser.css +0 -567
- package/extensions/browser/frontend/types.ts +0 -38
- package/extensions/browser/frontend/useBrowserResources.ts +0 -166
- package/frontend/vendor/ghostty-web/index.d.ts +0 -1842
- package/shared/backend-connection-status.d.ts +0 -37
- package/shared/browser-protocol.d.ts +0 -5
- /package/dist/assets/{api-w2x5QfxB.js → api-ByCV9MEr.js} +0 -0
- /package/dist/assets/{core-vPkV8P_m.js → core-Czwdx12k.js} +0 -0
- /package/dist/assets/{fsharp-DNZNZyEE.js → fsharp-BjPpkoJe.js} +0 -0
- /package/dist/assets/{go-_kPJjNc_.js → go-BkIDmSUP.js} +0 -0
- /package/dist/assets/{graphql-4HCNVt5g.js → graphql-CCJTpqi1.js} +0 -0
- /package/dist/assets/{hcl-C87ymY0o.js → hcl-s4z8lUFg.js} +0 -0
- /package/dist/assets/{ini-BYrrGNTc.js → ini-CyUW1I7Z.js} +0 -0
- /package/dist/assets/{java-B211rJbk.js → java-CFCcGRiM.js} +0 -0
- /package/dist/assets/{julia-DVPsRtBm.js → julia-DQyH_EYb.js} +0 -0
- /package/dist/assets/{kotlin-BYyNerjp.js → kotlin-B8hA5O51.js} +0 -0
- /package/dist/assets/{less-EC9UblGE.js → less-DaVdaas0.js} +0 -0
- /package/dist/assets/{lexon-B1yuUdqW.js → lexon-DLBhPd3y.js} +0 -0
- /package/dist/assets/{lua-Bo6GFzxT.js → lua-gB1c_cif.js} +0 -0
- /package/dist/assets/{m3-C-Y9cBVI.js → m3-C2T3z3gB.js} +0 -0
- /package/dist/assets/{markdown-BHu-wLbe.js → markdown-CKq-Euj1.js} +0 -0
- /package/dist/assets/{mips-Dkds26rX.js → mips-Bcs5qIDX.js} +0 -0
- /package/dist/assets/{msdax-PU4g-8_g.js → msdax-CBP48FHF.js} +0 -0
- /package/dist/assets/{mysql-fPpuGpS7.js → mysql-c7tiPcFj.js} +0 -0
- /package/dist/assets/{objective-c-Wy1e5JUf.js → objective-c-D4BZkeEV.js} +0 -0
- /package/dist/assets/{pascal-BIXwvrqU.js → pascal-CJDZg0MN.js} +0 -0
- /package/dist/assets/{pascaligo-DwR0AtL_.js → pascaligo-Bygi8Ynl.js} +0 -0
- /package/dist/assets/{perl-CGHAl8my.js → perl-aS8bkf7E.js} +0 -0
- /package/dist/assets/{pgsql-B91M73E2.js → pgsql-CzNAYFYl.js} +0 -0
- /package/dist/assets/{php-C70ttrCx.js → php-DOtoEFTT.js} +0 -0
- /package/dist/assets/{pla-BNrXaRh0.js → pla-DugRnao1.js} +0 -0
- /package/dist/assets/{postiats-Dv2IAZR9.js → postiats-DsXwusKh.js} +0 -0
- /package/dist/assets/{powerquery-CkY5Wq7J.js → powerquery-DL3o72oD.js} +0 -0
- /package/dist/assets/{powershell-uGIYLU3C.js → powershell-C1qHekys.js} +0 -0
- /package/dist/assets/{protobuf-CavfmJu-.js → protobuf-DHgsc-cH.js} +0 -0
- /package/dist/assets/{pug-Drqnyitx.js → pug-FZXlE_Iu.js} +0 -0
- /package/dist/assets/{qrcode-7uGsTEmi.js → qrcode-DJDTdjL7.js} +0 -0
- /package/dist/assets/{qsharp-C7yF-Dti.js → qsharp-bLyhI3DT.js} +0 -0
- /package/dist/assets/{r-HXLGP0_-.js → r-DjFGXbAa.js} +0 -0
- /package/dist/assets/{redis-C4y6mf2p.js → redis-DSoZ_V6u.js} +0 -0
- /package/dist/assets/{redshift-DdQrFPrv.js → redshift-CzkdCsrg.js} +0 -0
- /package/dist/assets/{restructuredtext-CBrf2PNj.js → restructuredtext-B_XPrGCQ.js} +0 -0
- /package/dist/assets/{ruby-CVtMoX2B.js → ruby-Cry2RneJ.js} +0 -0
- /package/dist/assets/{rust-Ckq4sbMP.js → rust-CWzdbFq1.js} +0 -0
- /package/dist/assets/{sb-er66OpE3.js → sb-DmofU59h.js} +0 -0
- /package/dist/assets/{scala-BdWZz-hH.js → scala-BUC2l-vN.js} +0 -0
- /package/dist/assets/{scheme-kTV_s2kP.js → scheme-CNIB_72f.js} +0 -0
- /package/dist/assets/{scss-Bymy2VJQ.js → scss-DcL4-NbX.js} +0 -0
- /package/dist/assets/{shell-bwEP1LQ1.js → shell-CgAGvSSA.js} +0 -0
- /package/dist/assets/{solidity-DCddeBgL.js → solidity-DSkt6O_P.js} +0 -0
- /package/dist/assets/{sophia-BMN3AQig.js → sophia--5ytcjDN.js} +0 -0
- /package/dist/assets/{sparql-CSXOLjzi.js → sparql-C1Srr7cD.js} +0 -0
- /package/dist/assets/{sql-COg8ewmK.js → sql-2k_O5fj7.js} +0 -0
- /package/dist/assets/{st-CIaZtBS_.js → st-DCCCPqfz.js} +0 -0
- /package/dist/assets/{swift-DXiOhUHu.js → swift-Dd8mAfOt.js} +0 -0
- /package/dist/assets/{systemverilog-BTDE6Dr2.js → systemverilog-B0-aZYXu.js} +0 -0
- /package/dist/assets/{tcl-B0zy9NvV.js → tcl-CZAZwPVX.js} +0 -0
- /package/dist/assets/{twig-D594l4Z1.js → twig-D-Ag0tdS.js} +0 -0
- /package/dist/assets/{typespec-XKVjaPqo.js → typespec-B7kTFp0Z.js} +0 -0
- /package/dist/assets/{useWorkspaceMenuKeyboard-Bpy_s69Z.js → useWorkspaceMenuKeyboard-uYZpDxhU.js} +0 -0
- /package/dist/assets/{vb-B2wYoKRV.js → vb-20w1JPum.js} +0 -0
- /package/dist/assets/{wgsl-D0Yf2LbK.js → wgsl-CLJfqFt0.js} +0 -0
- /package/dist/assets/{workspace-working-copy-C3FSR3N6.js → workspace-working-copy-Cfz07cOd.js} +0 -0
package/backend/server.js
DELETED
|
@@ -1,3225 +0,0 @@
|
|
|
1
|
-
const express = require('express');
|
|
2
|
-
const WebSocket = require('ws');
|
|
3
|
-
const http = require('http');
|
|
4
|
-
const path = require('path');
|
|
5
|
-
const fs = require('fs');
|
|
6
|
-
const os = require('os');
|
|
7
|
-
const crypto = require('crypto');
|
|
8
|
-
const { execFile } = require('child_process');
|
|
9
|
-
const { promisify } = require('util');
|
|
10
|
-
const { URLSearchParams, pathToFileURL } = require('url');
|
|
11
|
-
const AgentManager = require('./agent-manager');
|
|
12
|
-
const { runtimeKind } = require('./agent-runtime-binding');
|
|
13
|
-
const ConfigManager = require('./config-manager');
|
|
14
|
-
const ThemeManager = require('./theme-manager');
|
|
15
|
-
const TokenAuth = require('./auth');
|
|
16
|
-
const { getLocalIPs, getPrimaryLocalIP } = require('./network');
|
|
17
|
-
const { listAvailableAgents, resolveCompatibleCodexExecutable } = require('./executable-discovery');
|
|
18
|
-
const { readClaudeSettingsSummary } = require('./claude-settings');
|
|
19
|
-
const { listCodexModelOptions } = require('./codex-models');
|
|
20
|
-
const { listCodexSessions } = require('./codex-session-history');
|
|
21
|
-
const {
|
|
22
|
-
buildAgentSessionResumeCommand,
|
|
23
|
-
findAgentSession,
|
|
24
|
-
isSafeSessionId,
|
|
25
|
-
listAgentSessions,
|
|
26
|
-
normalizeProvider,
|
|
27
|
-
paginateAgentSessions,
|
|
28
|
-
resolveCodexResumeModelProvider,
|
|
29
|
-
searchAgentSessions,
|
|
30
|
-
} = require('./agent-session-history');
|
|
31
|
-
const {
|
|
32
|
-
findActiveAgentClaimingSession,
|
|
33
|
-
mainPageAgentSessionKey,
|
|
34
|
-
mainPageAgentSessionFromKey,
|
|
35
|
-
mainPageAgentSessionsToAutoResume,
|
|
36
|
-
resumedAgentSource,
|
|
37
|
-
} = require('./main-page-session');
|
|
38
|
-
const { discoverAgentWorkspaces } = require('./workspace-discovery');
|
|
39
|
-
const { inspectGitWorktree } = require('./git-worktree-info');
|
|
40
|
-
const { createWorkspaceDirectoryRouter } = require('./workspace-directory');
|
|
41
|
-
const { createControlRouter } = require('./control-api');
|
|
42
|
-
const { WorkspaceFileService, WorkspaceFileError } = require('./workspace-file-service');
|
|
43
|
-
const { createWorkspaceFileRouter, resolveWorkspaceRoot } = require('./workspace-file-router');
|
|
44
|
-
const { WorkspaceRootRegistry, rootIdForPath } = require('./workspace-root-registry');
|
|
45
|
-
const {
|
|
46
|
-
BrowserResourceManager,
|
|
47
|
-
createBrowserRouter,
|
|
48
|
-
} = require('../extensions/browser/backend');
|
|
49
|
-
const { UsageMonitor } = require('./usage-monitor');
|
|
50
|
-
const { CodexContextWindowReader } = require('./codex-context-window');
|
|
51
|
-
const { AsyncCache } = require('./async-cache');
|
|
52
|
-
const { getMainAgentSkillsCatalog } = require('./main-agent-skills');
|
|
53
|
-
const { discoverAgentExtensions, discoverSlashCommands } = require('./slash-command-discovery');
|
|
54
|
-
const { FarmingUpdateService } = require('./update-service');
|
|
55
|
-
const { inputPartsFromMessage } = require('./input-parts');
|
|
56
|
-
const { cleanupTerminalRuntime } = require('./terminal-runtime-cleanup');
|
|
57
|
-
const { QrShareTicketStore, SHARE_TICKET_TTL_MS } = require('./qr-share-tickets');
|
|
58
|
-
const { ReviewStateStore } = require('./review-state-store');
|
|
59
|
-
const { createReviewStateRouter } = require('./review-state-router');
|
|
60
|
-
const { ReviewDiffService } = require('./review-diff-service');
|
|
61
|
-
const { createReviewDiffRouter } = require('./review-diff-router');
|
|
62
|
-
const { ReviewSessionStore } = require('./review-session-store');
|
|
63
|
-
const { ReviewSessionService } = require('./review-session-service');
|
|
64
|
-
const { createReviewSessionRouter } = require('./review-session-router');
|
|
65
|
-
const {
|
|
66
|
-
applyIndexHtmlAppearance,
|
|
67
|
-
normalizeBasePath,
|
|
68
|
-
routePath,
|
|
69
|
-
rewriteIndexHtmlForBasePath,
|
|
70
|
-
appendIndexHtmlAssetToken,
|
|
71
|
-
} = require('./index-html');
|
|
72
|
-
const { decodeAcpTranscriptMedia } = require('./acp-transcript');
|
|
73
|
-
const {
|
|
74
|
-
coalesceSessionStream,
|
|
75
|
-
deliverSessionStreamToClients,
|
|
76
|
-
shouldBroadcastSessionStreamImmediately,
|
|
77
|
-
} = require('./session-stream-protocol');
|
|
78
|
-
const {
|
|
79
|
-
MIN_PROTOCOL_VERSION,
|
|
80
|
-
PROTOCOL_VERSION,
|
|
81
|
-
protocolCompatible,
|
|
82
|
-
sanitizeAgentUpdatePatch,
|
|
83
|
-
validateClientMessage,
|
|
84
|
-
} = require('../shared/browser-protocol');
|
|
85
|
-
|
|
86
|
-
const BASE_PATH = normalizeBasePath(process.env.FARMING_BASE_PATH || '/');
|
|
87
|
-
const PORT = process.env.PORT || 3000;
|
|
88
|
-
const tokenAuth = new TokenAuth({ basePath: BASE_PATH || '/' });
|
|
89
|
-
const authEnabled = tokenAuth.isEnabled();
|
|
90
|
-
const WS_PATH = routePath(BASE_PATH, '/ws');
|
|
91
|
-
const encodeCookieToken = TokenAuth.encodeCookieToken;
|
|
92
|
-
const DEFAULT_TRANSCRIPT_MAX_TURNS = 240;
|
|
93
|
-
const MAX_TRANSCRIPT_TURNS = 1000;
|
|
94
|
-
const INTERACTIVE_REFRESH_CACHE_MAX_AGE_MS = 3_000;
|
|
95
|
-
const execFileAsync = promisify(execFile);
|
|
96
|
-
|
|
97
|
-
const app = express();
|
|
98
|
-
const server = http.createServer(app);
|
|
99
|
-
const wss = new WebSocket.Server({ server });
|
|
100
|
-
|
|
101
|
-
const configManager = new ConfigManager();
|
|
102
|
-
configManager.init();
|
|
103
|
-
|
|
104
|
-
function resolveCliBinDir() {
|
|
105
|
-
if (process.env.FARMING_CLI_BIN_DIR) {
|
|
106
|
-
return process.env.FARMING_CLI_BIN_DIR;
|
|
107
|
-
}
|
|
108
|
-
if (process.pkg || process.env.FARMING_PACKAGED_RUNTIME === '1') {
|
|
109
|
-
return path.dirname(process.execPath);
|
|
110
|
-
}
|
|
111
|
-
return path.join(__dirname, '..', 'bin');
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
const agentManager = new AgentManager(configManager, {
|
|
115
|
-
controlUrl: `http://127.0.0.1:${PORT}${BASE_PATH}`,
|
|
116
|
-
tokenFile: tokenAuth.getTokenFile(),
|
|
117
|
-
authDisabled: !authEnabled,
|
|
118
|
-
cliBinDir: resolveCliBinDir(),
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
async function requireAgentRecoveryForHttp(res) {
|
|
122
|
-
try {
|
|
123
|
-
await agentManager.whenRecovered();
|
|
124
|
-
return true;
|
|
125
|
-
} catch (error) {
|
|
126
|
-
res.status(503).json({
|
|
127
|
-
error: error?.message || 'Agent lifecycle recovery is unavailable',
|
|
128
|
-
retryable: true,
|
|
129
|
-
});
|
|
130
|
-
return false;
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
const themeManager = new ThemeManager({ configDir: configManager.farmingDir });
|
|
135
|
-
const workspaceFileService = new WorkspaceFileService();
|
|
136
|
-
const workspaceRootRegistry = new WorkspaceRootRegistry(agentManager);
|
|
137
|
-
const browserResourceManager = new BrowserResourceManager({
|
|
138
|
-
configDir: configManager.farmingDir,
|
|
139
|
-
isEnabled: () => configManager.getSettings().browserExtensionEnabled === true,
|
|
140
|
-
getBrowserSettings: () => configManager.getSettings(),
|
|
141
|
-
});
|
|
142
|
-
const browserRuntimeRecoveryPromise = browserResourceManager.init().catch(error => {
|
|
143
|
-
console.warn('Failed to recover Browser runtimes:', error?.message || error);
|
|
144
|
-
return null;
|
|
145
|
-
});
|
|
146
|
-
const updateService = new FarmingUpdateService({
|
|
147
|
-
rootDir: path.join(__dirname, '..'),
|
|
148
|
-
configDir: configManager.farmingDir,
|
|
149
|
-
platform: process.platform,
|
|
150
|
-
arch: process.arch,
|
|
151
|
-
packagedRuntime: Boolean(process.pkg || process.env.FARMING_PACKAGED_RUNTIME === '1'),
|
|
152
|
-
});
|
|
153
|
-
const usageMonitor = new UsageMonitor({
|
|
154
|
-
agentManager,
|
|
155
|
-
configDir: configManager.farmingDir,
|
|
156
|
-
getProviderHomes: configuredProviderHomes,
|
|
157
|
-
});
|
|
158
|
-
const codexContextWindowReader = new CodexContextWindowReader();
|
|
159
|
-
const usageSummaryCache = new AsyncCache(() => usageMonitor.getUsageSummary(), {
|
|
160
|
-
ttlMs: 15_000,
|
|
161
|
-
staleMs: 2 * 60_000,
|
|
162
|
-
});
|
|
163
|
-
const codexModelOptionsCache = new AsyncCache(() => listCodexModelOptions(), {
|
|
164
|
-
ttlMs: 5 * 60_000,
|
|
165
|
-
staleMs: 5 * 60_000,
|
|
166
|
-
});
|
|
167
|
-
function configuredProviderHomes() {
|
|
168
|
-
const settings = configManager.getSettings();
|
|
169
|
-
const agentHomes = settings.agentHomes && typeof settings.agentHomes === 'object' ? settings.agentHomes : {};
|
|
170
|
-
const result = {};
|
|
171
|
-
for (const [provider, homes] of Object.entries(agentHomes)) {
|
|
172
|
-
if (!Array.isArray(homes)) continue;
|
|
173
|
-
result[provider] = homes.map(home => ({
|
|
174
|
-
id: String(home.id || 'default'),
|
|
175
|
-
path: configManager.expandWorkspacePath(String(home.path || '')),
|
|
176
|
-
})).filter(home => home.id && home.path);
|
|
177
|
-
}
|
|
178
|
-
return result;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
const agentSessionsCache = new AsyncCache(() => {
|
|
182
|
-
return listAgentSessions({
|
|
183
|
-
limit: 5000,
|
|
184
|
-
providerLimit: 5000,
|
|
185
|
-
scanLimit: 5000,
|
|
186
|
-
providerHomes: configuredProviderHomes(),
|
|
187
|
-
});
|
|
188
|
-
}, {
|
|
189
|
-
ttlMs: 30_000,
|
|
190
|
-
staleMs: 5 * 60_000,
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
function withSearchTimeout(promise, timeoutMs) {
|
|
194
|
-
let timer;
|
|
195
|
-
const timeout = new Promise((_, reject) => {
|
|
196
|
-
timer = setTimeout(() => {
|
|
197
|
-
const error = new Error('Agent search timed out');
|
|
198
|
-
error.code = 'ETIMEDOUT';
|
|
199
|
-
reject(error);
|
|
200
|
-
}, timeoutMs);
|
|
201
|
-
});
|
|
202
|
-
return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
const qrShareTickets = new QrShareTicketStore({ ttlMs: SHARE_TICKET_TTL_MS });
|
|
206
|
-
const reviewStateStore = new ReviewStateStore(configManager.farmingDir, {
|
|
207
|
-
seedReviews: {
|
|
208
|
-
'review-fixture-553987': {
|
|
209
|
-
patchsets: {
|
|
210
|
-
'Patchset 20': { reviewedPaths: ['clis/dataflow.py', 'clis/fetch_instance_log.py'], revision: 0 },
|
|
211
|
-
'Patchset 19': { reviewedPaths: ['clis/fetch_instance_log.py'], revision: 0 },
|
|
212
|
-
},
|
|
213
|
-
},
|
|
214
|
-
},
|
|
215
|
-
});
|
|
216
|
-
const reviewDiffService = new ReviewDiffService(agentManager, workspaceFileService);
|
|
217
|
-
const reviewSessionStore = new ReviewSessionStore(configManager.farmingDir);
|
|
218
|
-
const reviewSessionService = new ReviewSessionService(workspaceFileService, reviewSessionStore, reviewStateStore, {
|
|
219
|
-
resolveAgentRoot: agentId => agentManager.getAgentWorkspaceRoot(agentId),
|
|
220
|
-
resolveAcpReviewChanges: (agentId, itemIds) => agentManager.getAcpReviewChanges(agentId, itemIds),
|
|
221
|
-
});
|
|
222
|
-
const workspaceDiscoveryCache = new AsyncCache((key) => {
|
|
223
|
-
const request = JSON.parse(key);
|
|
224
|
-
return discoverAgentWorkspaces({
|
|
225
|
-
limit: request.limit,
|
|
226
|
-
agent: request.agent,
|
|
227
|
-
});
|
|
228
|
-
}, {
|
|
229
|
-
ttlMs: 30_000,
|
|
230
|
-
staleMs: 2 * 60_000,
|
|
231
|
-
});
|
|
232
|
-
|
|
233
|
-
const frontendDir = path.join(__dirname, '../frontend');
|
|
234
|
-
const crtFrontendDir = path.join(frontendDir, 'skins', 'crt');
|
|
235
|
-
const distDir = path.join(__dirname, '../dist');
|
|
236
|
-
const staticAppDir = fs.existsSync(distDir) ? distDir : frontendDir;
|
|
237
|
-
const xtermBrowserEntryPath = path.join(__dirname, '..', 'node_modules', '@xterm', 'xterm', 'lib', 'xterm.js');
|
|
238
|
-
const xtermFitEntryPath = path.join(__dirname, '..', 'node_modules', '@xterm', 'addon-fit', 'lib', 'addon-fit.js');
|
|
239
|
-
const xtermWebglEntryPath = path.join(__dirname, '..', 'node_modules', '@xterm', 'addon-webgl', 'lib', 'addon-webgl.js');
|
|
240
|
-
const xtermCssPath = path.join(__dirname, '..', 'node_modules', '@xterm', 'xterm', 'css', 'xterm.css');
|
|
241
|
-
const materialIconDir = path.join(__dirname, '..', 'node_modules', 'material-icon-theme', 'icons');
|
|
242
|
-
|
|
243
|
-
function getAvailableAgentsForRequest() {
|
|
244
|
-
if (process.env.FARMING_E2E_FAKE_EXECUTABLES === '1') {
|
|
245
|
-
return [
|
|
246
|
-
{
|
|
247
|
-
name: 'codex',
|
|
248
|
-
command: 'codex',
|
|
249
|
-
description: 'Codex CLI - OpenAI coding assistant',
|
|
250
|
-
category: 'coding',
|
|
251
|
-
supported: true,
|
|
252
|
-
interactive: true,
|
|
253
|
-
},
|
|
254
|
-
{
|
|
255
|
-
name: 'claude',
|
|
256
|
-
command: 'claude',
|
|
257
|
-
description: 'Claude CLI - Anthropic assistant',
|
|
258
|
-
category: 'coding',
|
|
259
|
-
supported: true,
|
|
260
|
-
interactive: true,
|
|
261
|
-
},
|
|
262
|
-
{
|
|
263
|
-
name: 'opencode',
|
|
264
|
-
command: 'opencode',
|
|
265
|
-
description: 'OpenCode - AI coding assistant',
|
|
266
|
-
category: 'coding',
|
|
267
|
-
supported: true,
|
|
268
|
-
interactive: true,
|
|
269
|
-
},
|
|
270
|
-
{
|
|
271
|
-
name: 'qoder',
|
|
272
|
-
command: 'qodercli',
|
|
273
|
-
description: 'Qoder - AI coding assistant',
|
|
274
|
-
category: 'coding',
|
|
275
|
-
supported: true,
|
|
276
|
-
interactive: true,
|
|
277
|
-
},
|
|
278
|
-
{
|
|
279
|
-
name: 'bash',
|
|
280
|
-
command: 'bash',
|
|
281
|
-
description: 'Shell session',
|
|
282
|
-
category: 'shell',
|
|
283
|
-
supported: true,
|
|
284
|
-
interactive: true,
|
|
285
|
-
},
|
|
286
|
-
{
|
|
287
|
-
name: 'zsh',
|
|
288
|
-
command: 'zsh',
|
|
289
|
-
description: 'Z shell',
|
|
290
|
-
category: 'shell',
|
|
291
|
-
supported: true,
|
|
292
|
-
interactive: true,
|
|
293
|
-
},
|
|
294
|
-
];
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
const shellEnv = agentManager.resolveAgentShellEnv('', { maxAgeMs: INTERACTIVE_REFRESH_CACHE_MAX_AGE_MS });
|
|
298
|
-
const pathEnv = typeof shellEnv?.PATH === 'string' && shellEnv.PATH.trim()
|
|
299
|
-
? shellEnv.PATH
|
|
300
|
-
: (process.env.PATH || '');
|
|
301
|
-
return listAvailableAgents(pathEnv);
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
function normalizeWorkspaceCompletionInput(value) {
|
|
305
|
-
const raw = typeof value === 'string' ? value.trim() : '';
|
|
306
|
-
if (!raw) {
|
|
307
|
-
return { raw: '', parent: os.homedir(), prefix: '', displayParent: '~', explicitDirectory: false };
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
const home = os.homedir();
|
|
311
|
-
const expanded = raw === '~' ? home : raw.startsWith('~/') ? path.join(home, raw.slice(2)) : raw;
|
|
312
|
-
const explicitDirectory = raw.endsWith(path.sep) || raw === '~';
|
|
313
|
-
const parent = explicitDirectory ? expanded : path.dirname(expanded);
|
|
314
|
-
const prefix = explicitDirectory ? '' : path.basename(expanded);
|
|
315
|
-
const trimTrailingSeparator = (input) => {
|
|
316
|
-
let next = input;
|
|
317
|
-
while (next.length > 1 && next.endsWith(path.sep)) next = next.slice(0, -1);
|
|
318
|
-
return next;
|
|
319
|
-
};
|
|
320
|
-
const displayParent = explicitDirectory
|
|
321
|
-
? trimTrailingSeparator(raw)
|
|
322
|
-
: trimTrailingSeparator(raw.slice(0, raw.length - prefix.length));
|
|
323
|
-
|
|
324
|
-
return {
|
|
325
|
-
raw,
|
|
326
|
-
parent: parent || path.sep,
|
|
327
|
-
prefix,
|
|
328
|
-
displayParent: displayParent || (path.isAbsolute(raw) ? path.sep : ''),
|
|
329
|
-
explicitDirectory,
|
|
330
|
-
};
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
async function listWorkspacePathCompletions(partialPath, limit = 12) {
|
|
334
|
-
const query = normalizeWorkspaceCompletionInput(partialPath);
|
|
335
|
-
const entries = await fs.promises.readdir(query.parent, { withFileTypes: true });
|
|
336
|
-
const normalizedPrefix = query.prefix.toLowerCase();
|
|
337
|
-
const maxResults = Math.max(1, Math.min(Number(limit) || 12, 100));
|
|
338
|
-
|
|
339
|
-
return entries
|
|
340
|
-
.filter(entry => entry.isDirectory())
|
|
341
|
-
.filter(entry => normalizedPrefix.startsWith('.') || !entry.name.startsWith('.'))
|
|
342
|
-
.filter(entry => !normalizedPrefix || entry.name.toLowerCase().startsWith(normalizedPrefix))
|
|
343
|
-
.sort((a, b) => a.name.localeCompare(b.name))
|
|
344
|
-
.slice(0, maxResults)
|
|
345
|
-
.map(entry => {
|
|
346
|
-
const fullPath = path.join(query.parent, entry.name);
|
|
347
|
-
const displayPath = query.raw.startsWith('~')
|
|
348
|
-
? path.join(query.displayParent || '~', entry.name)
|
|
349
|
-
: fullPath;
|
|
350
|
-
return {
|
|
351
|
-
name: entry.name,
|
|
352
|
-
path: `${displayPath}${path.sep}`,
|
|
353
|
-
};
|
|
354
|
-
});
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
// iOS can fetch installed-web-app metadata outside the authenticated page
|
|
358
|
-
// request, without preserving its cookie or token query. These files contain
|
|
359
|
-
// only public product artwork and must remain available to that fetcher.
|
|
360
|
-
const publicProductAssetsDir = path.join(staticAppDir, 'farming-2');
|
|
361
|
-
app.use(routePath(BASE_PATH, '/farming-2'), express.static(publicProductAssetsDir, { index: false }));
|
|
362
|
-
|
|
363
|
-
const sendPublicProductAsset = (filename) => (_req, res) => {
|
|
364
|
-
res.sendFile(path.join(publicProductAssetsDir, filename));
|
|
365
|
-
};
|
|
366
|
-
|
|
367
|
-
app.get(['/favicon.ico', routePath(BASE_PATH, '/favicon.ico')], sendPublicProductAsset('favicon-v2.ico'));
|
|
368
|
-
app.get([
|
|
369
|
-
'/apple-touch-icon.png',
|
|
370
|
-
'/apple-touch-icon-precomposed.png',
|
|
371
|
-
'/apple-touch-icon-180x180.png',
|
|
372
|
-
routePath(BASE_PATH, '/apple-touch-icon.png'),
|
|
373
|
-
routePath(BASE_PATH, '/apple-touch-icon-precomposed.png'),
|
|
374
|
-
routePath(BASE_PATH, '/apple-touch-icon-180x180.png'),
|
|
375
|
-
], sendPublicProductAsset('app-icon-v2-180.png'));
|
|
376
|
-
|
|
377
|
-
app.get(routePath(BASE_PATH, '/j/:code'), (req, res) => {
|
|
378
|
-
if (req.method === 'HEAD') {
|
|
379
|
-
res.status(204).end();
|
|
380
|
-
return;
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
const ticket = qrShareTickets.consume(req.params.code);
|
|
384
|
-
if (!ticket || (authEnabled && !tokenAuth.verify(ticket.token))) {
|
|
385
|
-
res.status(410).send('Farming share link expired.');
|
|
386
|
-
return;
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
if (authEnabled) {
|
|
390
|
-
res.setHeader(
|
|
391
|
-
'Set-Cookie',
|
|
392
|
-
`farming_token=${encodeCookieToken(ticket.token)}; Path=/; HttpOnly; SameSite=Lax`,
|
|
393
|
-
);
|
|
394
|
-
}
|
|
395
|
-
res.redirect(302, entryPathWithQuery(ticket.targetQuery));
|
|
396
|
-
});
|
|
397
|
-
|
|
398
|
-
// Token authentication middleware (before static files)
|
|
399
|
-
app.use(tokenAuth.middleware());
|
|
400
|
-
|
|
401
|
-
// Auth status endpoint (allowed without authentication via middleware)
|
|
402
|
-
app.get(routePath(BASE_PATH, '/api/auth/status'), (req, res) => {
|
|
403
|
-
res.json({ authRequired: authEnabled });
|
|
404
|
-
});
|
|
405
|
-
|
|
406
|
-
function absoluteClientUrl(req, urlPath) {
|
|
407
|
-
const forwardedProto = String(req.headers['x-forwarded-proto'] || '').split(',')[0].trim();
|
|
408
|
-
const protocol = forwardedProto || req.protocol || 'http';
|
|
409
|
-
const forwardedHost = String(req.headers['x-forwarded-host'] || '').split(',')[0].trim();
|
|
410
|
-
const host = forwardedHost || req.headers.host || `127.0.0.1:${PORT}`;
|
|
411
|
-
return `${protocol}://${host}${urlPath}`;
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
function shareTargetPositiveInteger(value) {
|
|
415
|
-
const number = Number(value);
|
|
416
|
-
return Number.isInteger(number) && number > 0 ? String(number) : '';
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
function shareTargetString(value, maxLength) {
|
|
420
|
-
const string = String(value || '').trim();
|
|
421
|
-
if (!string || string.length > maxLength || string.includes('\0')) return '';
|
|
422
|
-
return string;
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
function shareTargetQueryFromBody(body) {
|
|
426
|
-
const target = body && typeof body === 'object' ? body.target : null;
|
|
427
|
-
if (!target || typeof target !== 'object') return '';
|
|
428
|
-
|
|
429
|
-
const kind = target.kind === 'file' ? 'file' : target.kind === 'folder' ? 'folder' : target.kind === 'agent' ? 'agent' : '';
|
|
430
|
-
const agentId = shareTargetString(target.agentId, 160);
|
|
431
|
-
const absolutePath = shareTargetString(target.absolutePath, 2048);
|
|
432
|
-
const projectLabel = shareTargetString(target.projectLabel, 160);
|
|
433
|
-
const readingAnchor = shareTargetString(target.readingAnchor, 1800);
|
|
434
|
-
if (!kind || kind === 'agent' && !agentId || kind !== 'agent' && !absolutePath && !agentId && !projectLabel) return '';
|
|
435
|
-
|
|
436
|
-
const params = new URLSearchParams();
|
|
437
|
-
params.set('ftarget', kind);
|
|
438
|
-
if (agentId) params.set('agent', agentId);
|
|
439
|
-
if (kind === 'agent' && readingAnchor && /^[A-Za-z0-9_-]+$/.test(readingAnchor)) params.set('fra', readingAnchor);
|
|
440
|
-
if (absolutePath) params.set('path', absolutePath);
|
|
441
|
-
if (projectLabel) params.set('project', projectLabel);
|
|
442
|
-
|
|
443
|
-
if (kind === 'folder') {
|
|
444
|
-
const folderPath = shareTargetString(target.folderPath, 2048);
|
|
445
|
-
if (!absolutePath && !folderPath) return '';
|
|
446
|
-
if (folderPath) params.set('folder', folderPath);
|
|
447
|
-
} else if (kind === 'file') {
|
|
448
|
-
const filePath = shareTargetString(target.filePath, 2048);
|
|
449
|
-
if (!absolutePath && !filePath) return '';
|
|
450
|
-
if (filePath) params.set('file', filePath);
|
|
451
|
-
if (target.view === 'diff') params.set('view', 'diff');
|
|
452
|
-
const line = shareTargetPositiveInteger(target.lineNumber);
|
|
453
|
-
const column = shareTargetPositiveInteger(target.column);
|
|
454
|
-
const endColumn = shareTargetPositiveInteger(target.endColumn);
|
|
455
|
-
if (line) params.set('line', line);
|
|
456
|
-
if (column) params.set('column', column);
|
|
457
|
-
if (endColumn) params.set('endColumn', endColumn);
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
if (absolutePath) {
|
|
461
|
-
const absoluteParams = new URLSearchParams(params);
|
|
462
|
-
absoluteParams.delete('agent');
|
|
463
|
-
absoluteParams.delete(kind === 'folder' ? 'folder' : 'file');
|
|
464
|
-
if (absoluteParams.toString().length <= 1800) return absoluteParams.toString();
|
|
465
|
-
params.delete('path');
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
return params.toString();
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
function entryPathWithQuery(query = '', options = {}) {
|
|
472
|
-
const entryPath = BASE_PATH || '/';
|
|
473
|
-
const params = new URLSearchParams(query || '');
|
|
474
|
-
if (options.includeToken && authEnabled) {
|
|
475
|
-
params.set('token', tokenAuth.getToken());
|
|
476
|
-
}
|
|
477
|
-
const queryString = params.toString();
|
|
478
|
-
return queryString ? `${entryPath}?${queryString}` : entryPath;
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
function entryPathWithToken(targetQuery = '') {
|
|
482
|
-
return entryPathWithQuery(targetQuery, { includeToken: true });
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
app.post(routePath(BASE_PATH, '/api/share/qr-ticket'), express.json({ limit: '8kb' }), (req, res) => {
|
|
486
|
-
try {
|
|
487
|
-
const targetQuery = shareTargetQueryFromBody(req.body);
|
|
488
|
-
const ticket = qrShareTickets.create(authEnabled ? tokenAuth.getToken() : '', { targetQuery });
|
|
489
|
-
const shortPath = routePath(BASE_PATH, `/j/${ticket.code}`);
|
|
490
|
-
const longPath = entryPathWithToken(ticket.targetQuery);
|
|
491
|
-
res.json({
|
|
492
|
-
code: ticket.code,
|
|
493
|
-
expiresAt: ticket.expiresAt,
|
|
494
|
-
ttlMs: SHARE_TICKET_TTL_MS,
|
|
495
|
-
shortPath,
|
|
496
|
-
shortUrl: absoluteClientUrl(req, shortPath),
|
|
497
|
-
longUrl: absoluteClientUrl(req, longPath),
|
|
498
|
-
tokenLabel: authEnabled ? tokenAuth.getToken() : '',
|
|
499
|
-
});
|
|
500
|
-
} catch (error) {
|
|
501
|
-
res.status(500).json({ error: error.message || 'Failed to create share ticket' });
|
|
502
|
-
}
|
|
503
|
-
});
|
|
504
|
-
|
|
505
|
-
app.delete(routePath(BASE_PATH, '/api/share/qr-ticket/:code'), (req, res) => {
|
|
506
|
-
res.json({ revoked: qrShareTickets.revoke(req.params.code) });
|
|
507
|
-
});
|
|
508
|
-
|
|
509
|
-
// Terminal assets remain available to the standalone CRT skin when React is served from dist.
|
|
510
|
-
app.use(routePath(BASE_PATH, '/vendor'), express.static(path.join(frontendDir, 'vendor')));
|
|
511
|
-
app.get(routePath(BASE_PATH, '/vendor/xterm/xterm.js'), (_req, res) => {
|
|
512
|
-
res.sendFile(xtermBrowserEntryPath);
|
|
513
|
-
});
|
|
514
|
-
app.get(routePath(BASE_PATH, '/vendor/xterm/addon-fit.js'), (_req, res) => {
|
|
515
|
-
res.sendFile(xtermFitEntryPath);
|
|
516
|
-
});
|
|
517
|
-
app.get(routePath(BASE_PATH, '/vendor/xterm/addon-webgl.js'), (_req, res) => {
|
|
518
|
-
res.sendFile(xtermWebglEntryPath);
|
|
519
|
-
});
|
|
520
|
-
app.get(routePath(BASE_PATH, '/vendor/xterm/xterm.css'), (_req, res) => {
|
|
521
|
-
res.sendFile(xtermCssPath);
|
|
522
|
-
});
|
|
523
|
-
app.use(routePath(BASE_PATH, '/vendor/material-icons'), express.static(materialIconDir));
|
|
524
|
-
app.get(routePath(BASE_PATH, '/vendor/material-icons/:iconId.svg'), (req, res) => {
|
|
525
|
-
const fallbackIcon = String(req.params.iconId || '').startsWith('folder-') ? 'folder.svg' : 'file.svg';
|
|
526
|
-
res.sendFile(path.join(materialIconDir, fallbackIcon));
|
|
527
|
-
});
|
|
528
|
-
if (BASE_PATH) {
|
|
529
|
-
app.use('/assets', express.static(path.join(staticAppDir, 'assets'), { index: false }));
|
|
530
|
-
app.use('/farming-2', express.static(path.join(staticAppDir, 'farming-2'), { index: false }));
|
|
531
|
-
}
|
|
532
|
-
const crtEntryPath = routePath(BASE_PATH, '/crt');
|
|
533
|
-
app.get(crtEntryPath, (req, res) => {
|
|
534
|
-
if (req.path.endsWith('/')) {
|
|
535
|
-
res.setHeader('Cache-Control', 'no-cache');
|
|
536
|
-
res.sendFile(path.join(crtFrontendDir, 'index.html'));
|
|
537
|
-
return;
|
|
538
|
-
}
|
|
539
|
-
const requestUrl = new URL(req.url || '/', `http://${req.headers.host || 'localhost'}`);
|
|
540
|
-
res.redirect(308, `${crtEntryPath}/${requestUrl.search}`);
|
|
541
|
-
});
|
|
542
|
-
app.use(`${crtEntryPath}/shared`, express.static(frontendDir, { index: false }));
|
|
543
|
-
app.get(`${crtEntryPath}/crt-markdown-renderer.js`, (_req, res) => {
|
|
544
|
-
res.sendFile(path.join(distDir, 'crt-markdown-renderer.js'));
|
|
545
|
-
});
|
|
546
|
-
app.get(`${crtEntryPath}/crt-markdown-renderer.css`, (_req, res) => {
|
|
547
|
-
res.sendFile(path.join(distDir, 'crt-markdown-renderer.css'));
|
|
548
|
-
});
|
|
549
|
-
app.use(`${crtEntryPath}/crt-markdown-assets`, express.static(path.join(distDir, 'crt-markdown-assets'), { index: false }));
|
|
550
|
-
app.get(`${crtEntryPath}/crt-mermaid-renderer.js`, (_req, res) => {
|
|
551
|
-
res.sendFile(path.join(distDir, 'crt-mermaid-renderer.js'));
|
|
552
|
-
});
|
|
553
|
-
app.use(`${crtEntryPath}/`, express.static(crtFrontendDir, { index: false }));
|
|
554
|
-
app.use(BASE_PATH || '/', express.static(staticAppDir, { index: false }));
|
|
555
|
-
|
|
556
|
-
app.use(routePath(BASE_PATH, '/api/files'), createWorkspaceFileRouter(agentManager, workspaceFileService, {
|
|
557
|
-
rootRegistry: workspaceRootRegistry,
|
|
558
|
-
}));
|
|
559
|
-
app.use(routePath(BASE_PATH, '/api/browsers'), createBrowserRouter(browserResourceManager, workspaceRootRegistry));
|
|
560
|
-
|
|
561
|
-
app.use(routePath(BASE_PATH, '/api/review-sessions'), createReviewSessionRouter(reviewSessionService));
|
|
562
|
-
app.use(routePath(BASE_PATH, '/api/reviews'), createReviewDiffRouter(reviewDiffService, reviewSessionService));
|
|
563
|
-
app.use(routePath(BASE_PATH, '/api/reviews'), createReviewStateRouter(reviewStateStore));
|
|
564
|
-
|
|
565
|
-
if (process.env.NODE_ENV === 'test' && process.env.FARMING_E2E_FAKE_EXECUTABLES === '1') {
|
|
566
|
-
app.post(routePath(BASE_PATH, '/api/control/e2e/close-websockets'), express.json(), (_req, res) => {
|
|
567
|
-
const clients = [...wss.clients].filter(client => client.readyState === WebSocket.OPEN);
|
|
568
|
-
res.json({ closing: clients.length, code: 1013 });
|
|
569
|
-
setImmediate(() => {
|
|
570
|
-
clients.forEach(client => client.close(1013, 'terminal stream backpressure test'));
|
|
571
|
-
});
|
|
572
|
-
});
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
app.use(routePath(BASE_PATH, '/api/control'), createControlRouter(agentManager, {
|
|
576
|
-
notifyUpdate: broadcastState,
|
|
577
|
-
allowConcurrentTestControl: process.env.NODE_ENV === 'test'
|
|
578
|
-
&& process.env.FARMING_E2E_FAKE_EXECUTABLES === '1',
|
|
579
|
-
}));
|
|
580
|
-
|
|
581
|
-
app.get([
|
|
582
|
-
BASE_PATH || '/',
|
|
583
|
-
`${BASE_PATH || ''}/`,
|
|
584
|
-
routePath(BASE_PATH, '/code'),
|
|
585
|
-
routePath(BASE_PATH, '/code/'),
|
|
586
|
-
routePath(BASE_PATH, '/error-preview'),
|
|
587
|
-
routePath(BASE_PATH, '/review'),
|
|
588
|
-
].filter(Boolean), (req, res) => {
|
|
589
|
-
const indexPath = path.join(staticAppDir, 'index.html');
|
|
590
|
-
fs.readFile(indexPath, 'utf8', (error, html) => {
|
|
591
|
-
if (error) {
|
|
592
|
-
res.status(500).send('Farming frontend is not built');
|
|
593
|
-
return;
|
|
594
|
-
}
|
|
595
|
-
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
596
|
-
res.setHeader('Cache-Control', 'no-cache');
|
|
597
|
-
const rewrittenHtml = applyIndexHtmlAppearance(
|
|
598
|
-
rewriteIndexHtmlForBasePath(html, BASE_PATH),
|
|
599
|
-
configManager.getSettings().appearance
|
|
600
|
-
);
|
|
601
|
-
const requestUrl = new URL(req.url || '/', `http://${req.headers.host || 'localhost'}`);
|
|
602
|
-
const requestToken = requestUrl.searchParams.has('token') ? tokenAuth.extractToken(req) : '';
|
|
603
|
-
const htmlWithAssetToken = authEnabled && requestToken && tokenAuth.verify(requestToken)
|
|
604
|
-
? appendIndexHtmlAssetToken(rewrittenHtml, requestToken)
|
|
605
|
-
: rewrittenHtml;
|
|
606
|
-
res.send(htmlWithAssetToken);
|
|
607
|
-
});
|
|
608
|
-
});
|
|
609
|
-
|
|
610
|
-
app.get(routePath(BASE_PATH, '/api/executables'), (req, res) => {
|
|
611
|
-
const availableAgents = getAvailableAgentsForRequest();
|
|
612
|
-
res.setHeader('Cache-Control', 'no-store');
|
|
613
|
-
res.json({
|
|
614
|
-
agents: availableAgents,
|
|
615
|
-
total: availableAgents.length
|
|
616
|
-
});
|
|
617
|
-
});
|
|
618
|
-
|
|
619
|
-
app.get(routePath(BASE_PATH, '/api/workspaces/complete'), async (req, res) => {
|
|
620
|
-
try {
|
|
621
|
-
const partialPath = typeof req.query.path === 'string' ? req.query.path : '';
|
|
622
|
-
const requestedLimit = Number(req.query.limit);
|
|
623
|
-
const suggestions = await listWorkspacePathCompletions(partialPath, requestedLimit);
|
|
624
|
-
res.json({ suggestions });
|
|
625
|
-
} catch (error) {
|
|
626
|
-
res.status(200).json({
|
|
627
|
-
suggestions: [],
|
|
628
|
-
error: error.message || 'Failed to read directory',
|
|
629
|
-
});
|
|
630
|
-
}
|
|
631
|
-
});
|
|
632
|
-
|
|
633
|
-
app.use(routePath(BASE_PATH, '/api/workspaces'), createWorkspaceDirectoryRouter());
|
|
634
|
-
|
|
635
|
-
app.get(routePath(BASE_PATH, '/api/skills'), (_req, res) => {
|
|
636
|
-
res.json({ skills: getMainAgentSkillsCatalog() });
|
|
637
|
-
});
|
|
638
|
-
|
|
639
|
-
app.get(routePath(BASE_PATH, '/api/agent-extensions'), (_req, res) => {
|
|
640
|
-
const agents = getAvailableAgentsForRequest()
|
|
641
|
-
.filter(agent => agent.category === 'coding')
|
|
642
|
-
.map(agent => {
|
|
643
|
-
const provider = String(agent.name || agent.command || '').trim().toLowerCase();
|
|
644
|
-
const configuredHomes = configManager.getAgentHomes(provider);
|
|
645
|
-
const homes = configuredHomes.length > 0
|
|
646
|
-
? configuredHomes
|
|
647
|
-
: [{ id: 'default', path: '' }];
|
|
648
|
-
return {
|
|
649
|
-
id: provider,
|
|
650
|
-
name: agent.name,
|
|
651
|
-
description: agent.description || '',
|
|
652
|
-
discoverySupported: provider === 'codex' || provider === 'claude',
|
|
653
|
-
homes: homes.map(home => ({
|
|
654
|
-
id: home.id,
|
|
655
|
-
extensions: discoverAgentExtensions({
|
|
656
|
-
provider,
|
|
657
|
-
providerHomePath: home.path,
|
|
658
|
-
}).map(extension => ({
|
|
659
|
-
id: extension.command,
|
|
660
|
-
command: extension.command,
|
|
661
|
-
name: extension.label,
|
|
662
|
-
description: extension.description,
|
|
663
|
-
kind: extension.source === 'custom' ? 'command' : extension.source || 'command',
|
|
664
|
-
scope: extension.scope || '',
|
|
665
|
-
})),
|
|
666
|
-
})),
|
|
667
|
-
};
|
|
668
|
-
});
|
|
669
|
-
res.setHeader('Cache-Control', 'no-store');
|
|
670
|
-
res.json({ agents });
|
|
671
|
-
});
|
|
672
|
-
|
|
673
|
-
app.get(routePath(BASE_PATH, '/api/slash-commands'), (req, res) => {
|
|
674
|
-
const provider = typeof req.query.provider === 'string' ? req.query.provider : '';
|
|
675
|
-
const workspace = typeof req.query.workspace === 'string' ? req.query.workspace : '';
|
|
676
|
-
res.json({ commands: discoverSlashCommands({ provider, workspace }) });
|
|
677
|
-
});
|
|
678
|
-
|
|
679
|
-
const IMAGE_ATTACHMENT_EXTENSIONS = {
|
|
680
|
-
'image/png': 'png',
|
|
681
|
-
'image/jpeg': 'jpg',
|
|
682
|
-
'image/gif': 'gif',
|
|
683
|
-
'image/webp': 'webp',
|
|
684
|
-
};
|
|
685
|
-
const AUDIO_ATTACHMENT_EXTENSIONS = {
|
|
686
|
-
'audio/aac': 'aac',
|
|
687
|
-
'audio/flac': 'flac',
|
|
688
|
-
'audio/mp4': 'm4a',
|
|
689
|
-
'audio/mpeg': 'mp3',
|
|
690
|
-
'audio/ogg': 'ogg',
|
|
691
|
-
'audio/wav': 'wav',
|
|
692
|
-
'audio/wave': 'wav',
|
|
693
|
-
'audio/webm': 'webm',
|
|
694
|
-
'audio/x-wav': 'wav',
|
|
695
|
-
};
|
|
696
|
-
const IMAGE_ATTACHMENT_RETENTION_MS = 7 * 24 * 60 * 60 * 1000;
|
|
697
|
-
const IMAGE_ATTACHMENT_GC_INTERVAL_MS = 60 * 60 * 1000;
|
|
698
|
-
const IMAGE_ATTACHMENT_FILENAME_RE = /^pasted-image-\d+-[a-f0-9]{8}\.(?:png|jpg|gif|webp)$/;
|
|
699
|
-
const AUDIO_ATTACHMENT_FILENAME_RE = /^pasted-audio-\d+-[a-f0-9]{8}\.(?:aac|flac|m4a|mp3|ogg|wav|webm)$/;
|
|
700
|
-
let lastImageAttachmentGcAt = 0;
|
|
701
|
-
|
|
702
|
-
function imageAttachmentExtension(contentType) {
|
|
703
|
-
const normalized = String(contentType || '').split(';')[0].trim().toLowerCase();
|
|
704
|
-
return IMAGE_ATTACHMENT_EXTENSIONS[normalized] || '';
|
|
705
|
-
}
|
|
706
|
-
|
|
707
|
-
function audioAttachmentExtension(contentType) {
|
|
708
|
-
const normalized = String(contentType || '').split(';')[0].trim().toLowerCase();
|
|
709
|
-
return AUDIO_ATTACHMENT_EXTENSIONS[normalized] || '';
|
|
710
|
-
}
|
|
711
|
-
|
|
712
|
-
function imageAttachmentsDir() {
|
|
713
|
-
return path.join(configManager.farmingDir, 'attachments');
|
|
714
|
-
}
|
|
715
|
-
|
|
716
|
-
async function cleanupExpiredImageAttachments(options = {}) {
|
|
717
|
-
const now = Date.now();
|
|
718
|
-
if (!options.force && now - lastImageAttachmentGcAt < IMAGE_ATTACHMENT_GC_INTERVAL_MS) return;
|
|
719
|
-
lastImageAttachmentGcAt = now;
|
|
720
|
-
|
|
721
|
-
const attachmentsDir = imageAttachmentsDir();
|
|
722
|
-
let entries = [];
|
|
723
|
-
try {
|
|
724
|
-
entries = await fs.promises.readdir(attachmentsDir, { withFileTypes: true });
|
|
725
|
-
} catch (error) {
|
|
726
|
-
if (error && error.code !== 'ENOENT') {
|
|
727
|
-
console.warn('Failed to scan image attachments:', error.message || error);
|
|
728
|
-
}
|
|
729
|
-
return;
|
|
730
|
-
}
|
|
731
|
-
|
|
732
|
-
const cutoff = now - IMAGE_ATTACHMENT_RETENTION_MS;
|
|
733
|
-
await Promise.all(entries.map(async (entry) => {
|
|
734
|
-
if (!entry.isFile() || (!IMAGE_ATTACHMENT_FILENAME_RE.test(entry.name) && !AUDIO_ATTACHMENT_FILENAME_RE.test(entry.name))) return;
|
|
735
|
-
|
|
736
|
-
const filePath = path.join(attachmentsDir, entry.name);
|
|
737
|
-
try {
|
|
738
|
-
const stat = await fs.promises.stat(filePath);
|
|
739
|
-
if (stat.mtimeMs < cutoff) {
|
|
740
|
-
await fs.promises.unlink(filePath);
|
|
741
|
-
}
|
|
742
|
-
} catch (error) {
|
|
743
|
-
if (!error || error.code !== 'ENOENT') {
|
|
744
|
-
console.warn('Failed to remove expired image attachment:', error && (error.message || error));
|
|
745
|
-
}
|
|
746
|
-
}
|
|
747
|
-
}));
|
|
748
|
-
}
|
|
749
|
-
|
|
750
|
-
void cleanupExpiredImageAttachments({ force: true });
|
|
751
|
-
|
|
752
|
-
app.post(
|
|
753
|
-
routePath(BASE_PATH, '/api/attachments/image'),
|
|
754
|
-
express.raw({ type: 'image/*', limit: '12mb' }),
|
|
755
|
-
(req, res) => {
|
|
756
|
-
const contentType = String(req.headers['content-type'] || '').split(';')[0].trim().toLowerCase();
|
|
757
|
-
const extension = imageAttachmentExtension(contentType);
|
|
758
|
-
if (!extension) {
|
|
759
|
-
res.status(415).json({ error: 'unsupported image type' });
|
|
760
|
-
return;
|
|
761
|
-
}
|
|
762
|
-
|
|
763
|
-
if (!Buffer.isBuffer(req.body) || req.body.length === 0) {
|
|
764
|
-
res.status(400).json({ error: 'empty image attachment' });
|
|
765
|
-
return;
|
|
766
|
-
}
|
|
767
|
-
|
|
768
|
-
const attachmentsDir = imageAttachmentsDir();
|
|
769
|
-
fs.mkdirSync(attachmentsDir, { recursive: true });
|
|
770
|
-
const filename = `pasted-image-${Date.now()}-${crypto.randomBytes(4).toString('hex')}.${extension}`;
|
|
771
|
-
const filePath = path.join(attachmentsDir, filename);
|
|
772
|
-
fs.writeFileSync(filePath, req.body);
|
|
773
|
-
void cleanupExpiredImageAttachments();
|
|
774
|
-
|
|
775
|
-
res.status(201).json({
|
|
776
|
-
path: filePath,
|
|
777
|
-
name: filename,
|
|
778
|
-
type: contentType,
|
|
779
|
-
size: req.body.length,
|
|
780
|
-
});
|
|
781
|
-
}
|
|
782
|
-
);
|
|
783
|
-
|
|
784
|
-
app.post(
|
|
785
|
-
routePath(BASE_PATH, '/api/attachments/audio'),
|
|
786
|
-
express.raw({ type: 'audio/*', limit: '25mb' }),
|
|
787
|
-
(req, res) => {
|
|
788
|
-
const contentType = String(req.headers['content-type'] || '').split(';')[0].trim().toLowerCase();
|
|
789
|
-
const extension = audioAttachmentExtension(contentType);
|
|
790
|
-
if (!extension) {
|
|
791
|
-
res.status(415).json({ error: 'unsupported audio type' });
|
|
792
|
-
return;
|
|
793
|
-
}
|
|
794
|
-
if (!Buffer.isBuffer(req.body) || req.body.length === 0) {
|
|
795
|
-
res.status(400).json({ error: 'empty audio attachment' });
|
|
796
|
-
return;
|
|
797
|
-
}
|
|
798
|
-
const attachmentsDir = imageAttachmentsDir();
|
|
799
|
-
fs.mkdirSync(attachmentsDir, { recursive: true });
|
|
800
|
-
const filename = `pasted-audio-${Date.now()}-${crypto.randomBytes(4).toString('hex')}.${extension}`;
|
|
801
|
-
const filePath = path.join(attachmentsDir, filename);
|
|
802
|
-
fs.writeFileSync(filePath, req.body);
|
|
803
|
-
void cleanupExpiredImageAttachments();
|
|
804
|
-
res.status(201).json({ path: filePath, name: filename, type: contentType, size: req.body.length });
|
|
805
|
-
}
|
|
806
|
-
);
|
|
807
|
-
|
|
808
|
-
app.get(routePath(BASE_PATH, '/api/codex/models'), async (req, res) => {
|
|
809
|
-
try {
|
|
810
|
-
const catalog = await codexModelOptionsCache.get('catalog');
|
|
811
|
-
res.json(catalog);
|
|
812
|
-
} catch (error) {
|
|
813
|
-
const timedOut = error && error.code === 'CODEX_MODELS_TIMEOUT';
|
|
814
|
-
res.status(timedOut ? 504 : 502).json({
|
|
815
|
-
error: error && error.message ? error.message : 'Failed to load Codex model catalog',
|
|
816
|
-
code: error && error.code ? error.code : 'CODEX_MODELS_FAILED',
|
|
817
|
-
});
|
|
818
|
-
}
|
|
819
|
-
});
|
|
820
|
-
|
|
821
|
-
app.get(routePath(BASE_PATH, '/api/claude/settings'), (req, res) => {
|
|
822
|
-
res.json({ settings: readClaudeSettingsSummary() });
|
|
823
|
-
});
|
|
824
|
-
|
|
825
|
-
app.get(routePath(BASE_PATH, '/api/usage'), async (req, res) => {
|
|
826
|
-
try {
|
|
827
|
-
const fresh = req.query.fresh === '1';
|
|
828
|
-
const live = req.query.live === '1';
|
|
829
|
-
if (fresh) usageMonitor.invalidateDailyCache();
|
|
830
|
-
const usage = await usageSummaryCache.get(
|
|
831
|
-
'summary',
|
|
832
|
-
fresh ? { force: true } : live ? { maxAgeMs: 15_000 } : {},
|
|
833
|
-
);
|
|
834
|
-
res.json({ usage });
|
|
835
|
-
} catch (error) {
|
|
836
|
-
res.status(500).json({ error: error.message || 'Failed to read usage information' });
|
|
837
|
-
}
|
|
838
|
-
});
|
|
839
|
-
|
|
840
|
-
app.get(routePath(BASE_PATH, '/api/usage/day'), async (req, res) => {
|
|
841
|
-
try {
|
|
842
|
-
const date = String(req.query.date || '').trim();
|
|
843
|
-
const detail = await usageMonitor.getUsageDay(date, {
|
|
844
|
-
fresh: req.query.fresh === '1',
|
|
845
|
-
live: req.query.live === '1',
|
|
846
|
-
});
|
|
847
|
-
res.json({ detail });
|
|
848
|
-
} catch (error) {
|
|
849
|
-
const invalidDate = error instanceof RangeError;
|
|
850
|
-
res.status(invalidDate ? 400 : 500).json({
|
|
851
|
-
error: invalidDate
|
|
852
|
-
? error.message
|
|
853
|
-
: error.message || 'Failed to read usage day information',
|
|
854
|
-
});
|
|
855
|
-
}
|
|
856
|
-
});
|
|
857
|
-
|
|
858
|
-
app.post(routePath(BASE_PATH, '/api/codex/context-windows'), express.json(), async (req, res) => {
|
|
859
|
-
try {
|
|
860
|
-
const requestedIds = Array.isArray(req.body?.agentIds)
|
|
861
|
-
? req.body.agentIds
|
|
862
|
-
.map(value => String(value || '').trim())
|
|
863
|
-
.filter(Boolean)
|
|
864
|
-
.slice(0, 20)
|
|
865
|
-
: [];
|
|
866
|
-
const requestedIdSet = new Set(requestedIds);
|
|
867
|
-
const agents = agentManager.getState().agents.filter(agent => requestedIdSet.has(agent.id));
|
|
868
|
-
const contextWindows = await codexContextWindowReader.readForAgents(agents);
|
|
869
|
-
res.json({ contextWindows });
|
|
870
|
-
} catch (error) {
|
|
871
|
-
res.status(500).json({ error: error.message || 'Failed to read Codex context windows' });
|
|
872
|
-
}
|
|
873
|
-
});
|
|
874
|
-
|
|
875
|
-
app.get(routePath(BASE_PATH, '/api/update'), async (req, res) => {
|
|
876
|
-
try {
|
|
877
|
-
const update = await updateService.check({ force: req.query.force === '1' });
|
|
878
|
-
res.json({ update });
|
|
879
|
-
} catch (error) {
|
|
880
|
-
res.status(502).json({ error: error.message || 'Failed to check for updates' });
|
|
881
|
-
}
|
|
882
|
-
});
|
|
883
|
-
|
|
884
|
-
app.post(routePath(BASE_PATH, '/api/update/install'), express.json(), async (req, res) => {
|
|
885
|
-
try {
|
|
886
|
-
const state = await updateService.startInstall({
|
|
887
|
-
assetName: req.body && typeof req.body.assetName === 'string' ? req.body.assetName : '',
|
|
888
|
-
});
|
|
889
|
-
res.status(202).json({ update: { state } });
|
|
890
|
-
} catch (error) {
|
|
891
|
-
res.status(500).json({ error: error.message || 'Failed to start update' });
|
|
892
|
-
}
|
|
893
|
-
});
|
|
894
|
-
|
|
895
|
-
app.post(routePath(BASE_PATH, '/api/update/restart'), express.json(), async (req, res) => {
|
|
896
|
-
try {
|
|
897
|
-
const state = await updateService.applyPreparedUpdate();
|
|
898
|
-
res.status(202).json({ update: { state } });
|
|
899
|
-
} catch (error) {
|
|
900
|
-
res.status(500).json({ error: error.message || 'Failed to restart for update' });
|
|
901
|
-
}
|
|
902
|
-
});
|
|
903
|
-
|
|
904
|
-
function warmCodexExecutableVersionCache() {
|
|
905
|
-
const startedAt = Date.now();
|
|
906
|
-
try {
|
|
907
|
-
const result = resolveCompatibleCodexExecutable('');
|
|
908
|
-
if (result.path) {
|
|
909
|
-
console.log(`Codex executable ready: ${result.version || 'unknown version'} (${Date.now() - startedAt}ms)`);
|
|
910
|
-
}
|
|
911
|
-
} catch (error) {
|
|
912
|
-
console.warn('Failed to warm Codex executable version cache:', error.message || error);
|
|
913
|
-
}
|
|
914
|
-
}
|
|
915
|
-
|
|
916
|
-
app.get(routePath(BASE_PATH, '/api/codex/sessions'), async (req, res) => {
|
|
917
|
-
const requestedLimit = Number(req.query.limit);
|
|
918
|
-
const limit = Number.isFinite(requestedLimit) ? Math.max(0, Math.min(1000, requestedLimit)) : 40;
|
|
919
|
-
const requestedScanLimit = Number(req.query.scanLimit);
|
|
920
|
-
const scanLimit = Number.isFinite(requestedScanLimit) ? Math.max(limit, Math.min(5000, requestedScanLimit)) : undefined;
|
|
921
|
-
const sessions = await listCodexSessions({ limit, scanLimit });
|
|
922
|
-
res.json({ sessions });
|
|
923
|
-
});
|
|
924
|
-
|
|
925
|
-
app.get(routePath(BASE_PATH, '/api/agent-sessions'), async (req, res) => {
|
|
926
|
-
try {
|
|
927
|
-
const requestedLimit = Number(req.query.limit);
|
|
928
|
-
const limit = Number.isFinite(requestedLimit) ? Math.max(0, Math.min(1000, requestedLimit)) : 60;
|
|
929
|
-
const cursor = typeof req.query.cursor === 'string' ? req.query.cursor : '';
|
|
930
|
-
const cacheOptions = req.query.force === '1'
|
|
931
|
-
? { force: true }
|
|
932
|
-
: (req.query.fresh === '1' ? { maxAgeMs: INTERACTIVE_REFRESH_CACHE_MAX_AGE_MS } : {});
|
|
933
|
-
const sessions = await agentSessionsCache.get(
|
|
934
|
-
JSON.stringify(configManager.getSettings().agentHomes || {}),
|
|
935
|
-
cacheOptions
|
|
936
|
-
);
|
|
937
|
-
const page = paginateAgentSessions(sessions, { limit: Math.max(1, limit), cursor });
|
|
938
|
-
if (page.invalidCursor) {
|
|
939
|
-
res.status(400).json({ error: 'Invalid Agent session cursor' });
|
|
940
|
-
return;
|
|
941
|
-
}
|
|
942
|
-
const displayStateByKey = new Map(configManager.listAgentSessionRecords()
|
|
943
|
-
.filter(record => record && record.providerSessionKey)
|
|
944
|
-
.map(record => [record.providerSessionKey, record]));
|
|
945
|
-
res.json({
|
|
946
|
-
sessions: page.sessions.map(session => {
|
|
947
|
-
const key = mainPageAgentSessionKey(session.provider, session.id, session.providerHomeId);
|
|
948
|
-
const displayState = displayStateByKey.get(key);
|
|
949
|
-
return typeof displayState?.displayPinned === 'boolean'
|
|
950
|
-
? { ...session, pinned: displayState.displayPinned }
|
|
951
|
-
: session;
|
|
952
|
-
}),
|
|
953
|
-
nextCursor: page.nextCursor,
|
|
954
|
-
hasMore: page.hasMore,
|
|
955
|
-
total: sessions.length,
|
|
956
|
-
});
|
|
957
|
-
} catch (error) {
|
|
958
|
-
console.error('Failed to read agent sessions:', error);
|
|
959
|
-
res.status(500).json({ error: error.message || 'Failed to read agent sessions' });
|
|
960
|
-
}
|
|
961
|
-
});
|
|
962
|
-
|
|
963
|
-
app.get(routePath(BASE_PATH, '/api/agent-sessions/search'), async (req, res) => {
|
|
964
|
-
try {
|
|
965
|
-
const query = typeof req.query.q === 'string' ? req.query.q : '';
|
|
966
|
-
const requestedLimit = Number(req.query.limit);
|
|
967
|
-
const limit = Number.isFinite(requestedLimit) ? Math.max(1, Math.min(1000, requestedLimit)) : 100;
|
|
968
|
-
const settings = configManager.getSettings();
|
|
969
|
-
const cacheOptions = req.query.force === '1'
|
|
970
|
-
? { force: true }
|
|
971
|
-
: (req.query.fresh === '1' ? { maxAgeMs: INTERACTIVE_REFRESH_CACHE_MAX_AGE_MS } : {});
|
|
972
|
-
const sessions = await withSearchTimeout(
|
|
973
|
-
agentSessionsCache.get(
|
|
974
|
-
JSON.stringify(settings.agentHomes || {}),
|
|
975
|
-
cacheOptions
|
|
976
|
-
),
|
|
977
|
-
settings.searchTimeoutMs
|
|
978
|
-
);
|
|
979
|
-
const result = searchAgentSessions(sessions, query, {
|
|
980
|
-
limit,
|
|
981
|
-
projectNames: settings.projectNames,
|
|
982
|
-
});
|
|
983
|
-
const displayStateByKey = new Map(configManager.listAgentSessionRecords()
|
|
984
|
-
.filter(record => record && record.providerSessionKey)
|
|
985
|
-
.map(record => [record.providerSessionKey, record]));
|
|
986
|
-
res.json({
|
|
987
|
-
...result,
|
|
988
|
-
sessions: result.sessions.map(session => {
|
|
989
|
-
const key = mainPageAgentSessionKey(session.provider, session.id, session.providerHomeId);
|
|
990
|
-
const displayState = displayStateByKey.get(key);
|
|
991
|
-
return typeof displayState?.displayPinned === 'boolean'
|
|
992
|
-
? { ...session, pinned: displayState.displayPinned }
|
|
993
|
-
: session;
|
|
994
|
-
}),
|
|
995
|
-
});
|
|
996
|
-
} catch (error) {
|
|
997
|
-
if (error?.code === 'ETIMEDOUT') {
|
|
998
|
-
res.status(504).json({ error: error.message });
|
|
999
|
-
return;
|
|
1000
|
-
}
|
|
1001
|
-
console.error('Failed to search agent sessions:', error);
|
|
1002
|
-
res.status(500).json({ error: error.message || 'Failed to search Agent sessions' });
|
|
1003
|
-
}
|
|
1004
|
-
});
|
|
1005
|
-
|
|
1006
|
-
app.patch(routePath(BASE_PATH, '/api/agent-sessions/:provider/:sessionId'), express.json(), (req, res) => {
|
|
1007
|
-
const provider = normalizeProvider(req.params.provider);
|
|
1008
|
-
const sessionId = String(req.params.sessionId || '').trim();
|
|
1009
|
-
const providerHomeId = String(req.body?.providerHomeId || 'default').trim() || 'default';
|
|
1010
|
-
if (!provider || !isSafeSessionId(sessionId) || !/^[A-Za-z0-9._-]+$/.test(providerHomeId)) {
|
|
1011
|
-
res.status(400).json({ error: 'Invalid Agent session' });
|
|
1012
|
-
return;
|
|
1013
|
-
}
|
|
1014
|
-
if (typeof req.body?.pinned !== 'boolean') {
|
|
1015
|
-
res.status(400).json({ error: 'Pinned state is required' });
|
|
1016
|
-
return;
|
|
1017
|
-
}
|
|
1018
|
-
const sessionKey = mainPageAgentSessionKey(provider, sessionId, providerHomeId);
|
|
1019
|
-
configManager.setProviderSessionDisplayState(sessionKey, { pinned: req.body.pinned });
|
|
1020
|
-
res.json({ sessionKey, pinned: req.body.pinned });
|
|
1021
|
-
});
|
|
1022
|
-
|
|
1023
|
-
app.post(routePath(BASE_PATH, '/api/main-page-agent-sessions'), express.json(), (req, res) => {
|
|
1024
|
-
const operation = req.body?.operation;
|
|
1025
|
-
const requestedKeys = Array.isArray(req.body?.sessionKeys) ? req.body.sessionKeys : [];
|
|
1026
|
-
const sessionKeys = [...new Set(requestedKeys.map(key => String(key || '').trim()).filter(Boolean))];
|
|
1027
|
-
if (
|
|
1028
|
-
!['add', 'remove'].includes(operation)
|
|
1029
|
-
|| sessionKeys.length === 0
|
|
1030
|
-
|| sessionKeys.length > 50
|
|
1031
|
-
|| sessionKeys.some(key => !mainPageAgentSessionFromKey(key))
|
|
1032
|
-
) {
|
|
1033
|
-
res.status(400).json({ error: 'A valid main-page Agent session mutation is required' });
|
|
1034
|
-
return;
|
|
1035
|
-
}
|
|
1036
|
-
|
|
1037
|
-
if (operation === 'add') {
|
|
1038
|
-
[...sessionKeys].reverse().forEach(sessionKey => {
|
|
1039
|
-
const session = mainPageAgentSessionFromKey(sessionKey);
|
|
1040
|
-
configManager.rememberMainPageSessionKey(sessionKey, {
|
|
1041
|
-
provider: session.provider,
|
|
1042
|
-
providerSessionId: session.sessionId,
|
|
1043
|
-
providerSessionKey: sessionKey,
|
|
1044
|
-
providerHomeId: session.providerHomeId || 'default',
|
|
1045
|
-
});
|
|
1046
|
-
});
|
|
1047
|
-
} else {
|
|
1048
|
-
configManager.removeMainPageSessionKeys(sessionKeys);
|
|
1049
|
-
}
|
|
1050
|
-
|
|
1051
|
-
const mainPageSessionKeys = configManager.getMainPageSessionKeys();
|
|
1052
|
-
agentSessionsCache.invalidate();
|
|
1053
|
-
res.json({ success: true, mainPageSessionKeys });
|
|
1054
|
-
scheduleBroadcastState();
|
|
1055
|
-
});
|
|
1056
|
-
|
|
1057
|
-
app.get(routePath(BASE_PATH, '/api/themes'), (req, res) => {
|
|
1058
|
-
const currentTheme = configManager.getSettings().theme || 'terminal';
|
|
1059
|
-
res.json({
|
|
1060
|
-
themes: themeManager.getAllThemes(),
|
|
1061
|
-
current: currentTheme
|
|
1062
|
-
});
|
|
1063
|
-
});
|
|
1064
|
-
|
|
1065
|
-
app.get(routePath(BASE_PATH, '/api/settings'), (req, res) => {
|
|
1066
|
-
res.json({
|
|
1067
|
-
settings: configManager.getSettings()
|
|
1068
|
-
});
|
|
1069
|
-
});
|
|
1070
|
-
|
|
1071
|
-
app.get(routePath(BASE_PATH, '/api/workspaces/discovered'), (req, res) => {
|
|
1072
|
-
const requestedLimit = Number(req.query.limit);
|
|
1073
|
-
const limit = Number.isFinite(requestedLimit) ? Math.max(0, Math.min(20, requestedLimit)) : 12;
|
|
1074
|
-
const agent = typeof req.query.agent === 'string' ? req.query.agent : '';
|
|
1075
|
-
const cacheToken = JSON.stringify({ limit, agent });
|
|
1076
|
-
workspaceDiscoveryCache.get(cacheToken)
|
|
1077
|
-
.then(workspaces => {
|
|
1078
|
-
res.json({ workspaces });
|
|
1079
|
-
})
|
|
1080
|
-
.catch(error => {
|
|
1081
|
-
console.error('Failed to discover workspaces:', error);
|
|
1082
|
-
res.status(500).json({ error: error.message || 'Failed to discover workspaces' });
|
|
1083
|
-
});
|
|
1084
|
-
});
|
|
1085
|
-
|
|
1086
|
-
app.get(routePath(BASE_PATH, '/api/agents/:agentId/session-text'), async (req, res) => {
|
|
1087
|
-
const text = await agentManager.getAgentSessionText(req.params.agentId);
|
|
1088
|
-
if (text === null) {
|
|
1089
|
-
res.status(404).json({ error: 'Agent not found' });
|
|
1090
|
-
return;
|
|
1091
|
-
}
|
|
1092
|
-
|
|
1093
|
-
res.type('text/plain');
|
|
1094
|
-
res.send(text);
|
|
1095
|
-
});
|
|
1096
|
-
|
|
1097
|
-
app.get(routePath(BASE_PATH, '/api/agents/:agentId/session-view'), async (req, res) => {
|
|
1098
|
-
const sessionView = await agentManager.getAgentSessionView(req.params.agentId);
|
|
1099
|
-
if (!sessionView) {
|
|
1100
|
-
res.status(404).json({ error: 'Agent not found' });
|
|
1101
|
-
return;
|
|
1102
|
-
}
|
|
1103
|
-
|
|
1104
|
-
res.json({ session: sessionView });
|
|
1105
|
-
});
|
|
1106
|
-
|
|
1107
|
-
app.get(routePath(BASE_PATH, '/api/agents/:agentId/json-cli-transcript'), async (req, res) => {
|
|
1108
|
-
try {
|
|
1109
|
-
const transcript = agentManager.getJsonCliTranscript(req.params.agentId, {
|
|
1110
|
-
maxTurns: DEFAULT_TRANSCRIPT_MAX_TURNS,
|
|
1111
|
-
});
|
|
1112
|
-
res.json({ transcript });
|
|
1113
|
-
} catch (error) {
|
|
1114
|
-
const message = error && error.message ? error.message : 'Failed to read JSON CLI transcript';
|
|
1115
|
-
res.status(message === 'Agent not found' ? 404 : 409).json({ error: message });
|
|
1116
|
-
}
|
|
1117
|
-
});
|
|
1118
|
-
|
|
1119
|
-
app.get(routePath(BASE_PATH, '/api/agents/:agentId/acp-session'), async (req, res) => {
|
|
1120
|
-
try {
|
|
1121
|
-
res.json({
|
|
1122
|
-
session: agentManager.getAcpSession(req.params.agentId, {
|
|
1123
|
-
includeUpdates: req.query.includeUpdates === '1',
|
|
1124
|
-
includeEntries: req.query.includeEntries !== '0',
|
|
1125
|
-
}),
|
|
1126
|
-
});
|
|
1127
|
-
} catch (error) {
|
|
1128
|
-
const message = error && error.message ? error.message : 'Failed to read ACP session';
|
|
1129
|
-
res.status(message === 'Agent not found' ? 404 : 409).json({ error: message });
|
|
1130
|
-
}
|
|
1131
|
-
});
|
|
1132
|
-
|
|
1133
|
-
app.get(routePath(BASE_PATH, '/api/agents/:agentId/acp-transcript'), async (req, res) => {
|
|
1134
|
-
try {
|
|
1135
|
-
const requestedMaxTurns = Number.parseInt(String(req.query.maxTurns || ''), 10);
|
|
1136
|
-
const maxTurns = Number.isFinite(requestedMaxTurns)
|
|
1137
|
-
? Math.min(MAX_TRANSCRIPT_TURNS, Math.max(20, requestedMaxTurns))
|
|
1138
|
-
: DEFAULT_TRANSCRIPT_MAX_TURNS;
|
|
1139
|
-
const requestedRevision = Number.parseInt(String(req.query.sinceRevision || ''), 10);
|
|
1140
|
-
const externalMedia = req.query.media === 'external-v1';
|
|
1141
|
-
res.json({ transcript: agentManager.getAcpTranscript(req.params.agentId, {
|
|
1142
|
-
maxTurns,
|
|
1143
|
-
...(externalMedia
|
|
1144
|
-
? {
|
|
1145
|
-
mediaPathPrefix: routePath(
|
|
1146
|
-
BASE_PATH,
|
|
1147
|
-
`/api/agents/${encodeURIComponent(req.params.agentId)}/acp-media`
|
|
1148
|
-
),
|
|
1149
|
-
}
|
|
1150
|
-
: {}),
|
|
1151
|
-
...(Number.isFinite(requestedRevision) && requestedRevision >= 0
|
|
1152
|
-
? { sinceRevision: requestedRevision }
|
|
1153
|
-
: {}),
|
|
1154
|
-
}) });
|
|
1155
|
-
} catch (error) {
|
|
1156
|
-
const message = error && error.message ? error.message : 'Failed to read ACP transcript';
|
|
1157
|
-
res.status(message === 'Agent not found' ? 404 : 409).json({ error: message });
|
|
1158
|
-
}
|
|
1159
|
-
});
|
|
1160
|
-
|
|
1161
|
-
app.get(routePath(BASE_PATH, '/api/agents/:agentId/acp-media/:entryId/:mediaId'), (req, res) => {
|
|
1162
|
-
try {
|
|
1163
|
-
const media = agentManager.getAcpTranscriptMedia(
|
|
1164
|
-
req.params.agentId,
|
|
1165
|
-
req.params.entryId,
|
|
1166
|
-
req.params.mediaId
|
|
1167
|
-
);
|
|
1168
|
-
const decoded = decodeAcpTranscriptMedia(media);
|
|
1169
|
-
if (!decoded) {
|
|
1170
|
-
res.status(415).json({ error: 'unsupported ACP transcript media' });
|
|
1171
|
-
return;
|
|
1172
|
-
}
|
|
1173
|
-
res.setHeader('Cache-Control', 'private, max-age=31536000, immutable');
|
|
1174
|
-
res.setHeader('X-Content-Type-Options', 'nosniff');
|
|
1175
|
-
res.type(decoded.mimeType).send(decoded.content);
|
|
1176
|
-
} catch (error) {
|
|
1177
|
-
const message = error && error.message ? error.message : 'Failed to read ACP transcript media';
|
|
1178
|
-
const status = message === 'Agent not found'
|
|
1179
|
-
|| message === 'ACP transcript entry not found'
|
|
1180
|
-
|| message === 'ACP transcript media not found'
|
|
1181
|
-
? 404
|
|
1182
|
-
: 409;
|
|
1183
|
-
res.status(status).json({ error: message });
|
|
1184
|
-
}
|
|
1185
|
-
});
|
|
1186
|
-
|
|
1187
|
-
app.get(routePath(BASE_PATH, '/api/agents/:agentId/acp-tool-details/:toolCallId'), async (req, res) => {
|
|
1188
|
-
try {
|
|
1189
|
-
res.json(agentManager.getAcpToolDetail(req.params.agentId, req.params.toolCallId));
|
|
1190
|
-
} catch (error) {
|
|
1191
|
-
const message = error && error.message ? error.message : 'Failed to read ACP tool details';
|
|
1192
|
-
const status = message === 'Agent not found' || message === 'ACP tool call not found' ? 404 : 409;
|
|
1193
|
-
res.status(status).json({ error: message });
|
|
1194
|
-
}
|
|
1195
|
-
});
|
|
1196
|
-
|
|
1197
|
-
app.post(routePath(BASE_PATH, '/api/agents/:agentId/acp-terminals/:terminalId/kill'), (req, res) => {
|
|
1198
|
-
try {
|
|
1199
|
-
res.json(agentManager.killAcpTerminal(req.params.agentId, req.params.terminalId));
|
|
1200
|
-
} catch (error) {
|
|
1201
|
-
const message = error && error.message ? error.message : 'Failed to stop ACP terminal';
|
|
1202
|
-
const status = message === 'Agent not found' || message === 'Unknown ACP terminal' ? 404 : 409;
|
|
1203
|
-
res.status(status).json({ error: message });
|
|
1204
|
-
}
|
|
1205
|
-
});
|
|
1206
|
-
|
|
1207
|
-
app.post(routePath(BASE_PATH, '/api/agents/:agentId/acp-terminals/:terminalId/input'), express.json({ limit: '96kb' }), (req, res) => {
|
|
1208
|
-
try {
|
|
1209
|
-
res.json(agentManager.inputAcpTerminal(req.params.agentId, req.params.terminalId, req.body?.input));
|
|
1210
|
-
} catch (error) {
|
|
1211
|
-
const message = error && error.message ? error.message : 'Failed to write ACP terminal input';
|
|
1212
|
-
const status = message === 'Agent not found' || message === 'Unknown ACP terminal' ? 404 : 409;
|
|
1213
|
-
res.status(status).json({ error: message });
|
|
1214
|
-
}
|
|
1215
|
-
});
|
|
1216
|
-
|
|
1217
|
-
app.post(routePath(BASE_PATH, '/api/agents/:agentId/acp-terminals/:terminalId/resize'), express.json(), (req, res) => {
|
|
1218
|
-
try {
|
|
1219
|
-
res.json(agentManager.resizeAcpTerminal(
|
|
1220
|
-
req.params.agentId,
|
|
1221
|
-
req.params.terminalId,
|
|
1222
|
-
req.body?.cols,
|
|
1223
|
-
req.body?.rows,
|
|
1224
|
-
));
|
|
1225
|
-
} catch (error) {
|
|
1226
|
-
const message = error && error.message ? error.message : 'Failed to resize ACP terminal';
|
|
1227
|
-
const status = message === 'Agent not found' || message === 'Unknown ACP terminal' ? 404 : 409;
|
|
1228
|
-
res.status(status).json({ error: message });
|
|
1229
|
-
}
|
|
1230
|
-
});
|
|
1231
|
-
|
|
1232
|
-
app.post(routePath(BASE_PATH, '/api/agents/:agentId/acp-subagents/:sessionId/cancel'), async (req, res) => {
|
|
1233
|
-
try {
|
|
1234
|
-
res.json(await agentManager.cancelAcpSubagent(req.params.agentId, req.params.sessionId));
|
|
1235
|
-
} catch (error) {
|
|
1236
|
-
const message = error && error.message ? error.message : 'Failed to stop ACP subagent';
|
|
1237
|
-
const status = message === 'Agent not found' || message === 'ACP subagent session not found' ? 404 : 409;
|
|
1238
|
-
res.status(status).json({ error: message });
|
|
1239
|
-
}
|
|
1240
|
-
});
|
|
1241
|
-
|
|
1242
|
-
app.post(routePath(BASE_PATH, '/api/agents/:agentId/acp-patches/:toolCallId/decision'), express.json({ limit: '16kb' }), async (req, res) => {
|
|
1243
|
-
try {
|
|
1244
|
-
res.json(await agentManager.decideAcpPatch(
|
|
1245
|
-
req.params.agentId,
|
|
1246
|
-
req.params.toolCallId,
|
|
1247
|
-
req.body?.path,
|
|
1248
|
-
req.body?.decision,
|
|
1249
|
-
));
|
|
1250
|
-
} catch (error) {
|
|
1251
|
-
const message = error && error.message ? error.message : 'Failed to decide ACP patch';
|
|
1252
|
-
const status = Number(error?.statusCode) || (message === 'Agent not found' || message === 'ACP tool call not found' ? 404 : 409);
|
|
1253
|
-
res.status(status).json({ error: message });
|
|
1254
|
-
}
|
|
1255
|
-
});
|
|
1256
|
-
|
|
1257
|
-
app.get(routePath(BASE_PATH, '/api/agents/:agentId/acp-sessions'), async (req, res) => {
|
|
1258
|
-
try {
|
|
1259
|
-
const result = await agentManager.listAcpSessions(req.params.agentId, {
|
|
1260
|
-
cwd: typeof req.query.cwd === 'string' ? req.query.cwd : '',
|
|
1261
|
-
cursor: typeof req.query.cursor === 'string' ? req.query.cursor : '',
|
|
1262
|
-
});
|
|
1263
|
-
res.json(result);
|
|
1264
|
-
} catch (error) {
|
|
1265
|
-
const message = error && error.message ? error.message : 'Failed to list ACP sessions';
|
|
1266
|
-
res.status(message === 'Agent not found' ? 404 : 409).json({ error: message });
|
|
1267
|
-
}
|
|
1268
|
-
});
|
|
1269
|
-
|
|
1270
|
-
app.post(routePath(BASE_PATH, '/api/agents/:agentId/acp-permission'), express.json(), (req, res) => {
|
|
1271
|
-
try {
|
|
1272
|
-
const result = agentManager.respondToAcpPermission(
|
|
1273
|
-
req.params.agentId,
|
|
1274
|
-
req.body?.requestId,
|
|
1275
|
-
req.body?.optionId,
|
|
1276
|
-
req.body?.cancelled === true
|
|
1277
|
-
);
|
|
1278
|
-
res.json(result);
|
|
1279
|
-
} catch (error) {
|
|
1280
|
-
const message = error && error.message ? error.message : 'Failed to respond to ACP permission';
|
|
1281
|
-
res.status(message === 'Agent not found' ? 404 : 409).json({ error: message });
|
|
1282
|
-
}
|
|
1283
|
-
});
|
|
1284
|
-
|
|
1285
|
-
app.post(routePath(BASE_PATH, '/api/agents/:agentId/acp-elicitation'), express.json(), (req, res) => {
|
|
1286
|
-
try {
|
|
1287
|
-
const result = agentManager.respondToAcpElicitation(
|
|
1288
|
-
req.params.agentId,
|
|
1289
|
-
req.body?.requestId,
|
|
1290
|
-
req.body?.action,
|
|
1291
|
-
req.body?.content
|
|
1292
|
-
);
|
|
1293
|
-
res.json(result);
|
|
1294
|
-
} catch (error) {
|
|
1295
|
-
const message = error && error.message ? error.message : 'Failed to respond to ACP input request';
|
|
1296
|
-
res.status(message === 'Agent not found' ? 404 : 409).json({ error: message });
|
|
1297
|
-
}
|
|
1298
|
-
});
|
|
1299
|
-
|
|
1300
|
-
app.post(routePath(BASE_PATH, '/api/agents/:agentId/acp-session/authenticate'), express.json(), async (req, res) => {
|
|
1301
|
-
try {
|
|
1302
|
-
res.json(await agentManager.authenticateAcpAgent(req.params.agentId, req.body?.methodId));
|
|
1303
|
-
} catch (error) {
|
|
1304
|
-
const message = error && error.message ? error.message : 'Failed to authenticate ACP Agent';
|
|
1305
|
-
res.status(message === 'Agent not found' ? 404 : 409).json({ error: message });
|
|
1306
|
-
}
|
|
1307
|
-
});
|
|
1308
|
-
|
|
1309
|
-
app.post(routePath(BASE_PATH, '/api/agents/:agentId/acp-session/logout'), async (req, res) => {
|
|
1310
|
-
try {
|
|
1311
|
-
res.json(await agentManager.logoutAcpAgent(req.params.agentId));
|
|
1312
|
-
} catch (error) {
|
|
1313
|
-
const message = error && error.message ? error.message : 'Failed to log out ACP Agent';
|
|
1314
|
-
res.status(message === 'Agent not found' ? 404 : 409).json({ error: message });
|
|
1315
|
-
}
|
|
1316
|
-
});
|
|
1317
|
-
|
|
1318
|
-
app.post(routePath(BASE_PATH, '/api/agents/:agentId/acp-session/fork'), express.json(), async (req, res) => {
|
|
1319
|
-
try {
|
|
1320
|
-
res.json(await agentManager.forkAcpSession(req.params.agentId, req.body || {}));
|
|
1321
|
-
} catch (error) {
|
|
1322
|
-
const message = error && error.message ? error.message : 'Failed to fork ACP session';
|
|
1323
|
-
res.status(message === 'Agent not found' ? 404 : 409).json({ error: message });
|
|
1324
|
-
}
|
|
1325
|
-
});
|
|
1326
|
-
|
|
1327
|
-
app.delete(routePath(BASE_PATH, '/api/agents/:agentId/acp-sessions/:sessionId'), async (req, res) => {
|
|
1328
|
-
try {
|
|
1329
|
-
res.json(await agentManager.deleteAcpSession(req.params.agentId, req.params.sessionId));
|
|
1330
|
-
} catch (error) {
|
|
1331
|
-
const message = error && error.message ? error.message : 'Failed to delete ACP session';
|
|
1332
|
-
res.status(message === 'Agent not found' ? 404 : 409).json({ error: message });
|
|
1333
|
-
}
|
|
1334
|
-
});
|
|
1335
|
-
|
|
1336
|
-
app.post(routePath(BASE_PATH, '/api/agents/:agentId/acp-session/close'), async (req, res) => {
|
|
1337
|
-
try {
|
|
1338
|
-
res.json(await agentManager.closeAcpSession(req.params.agentId));
|
|
1339
|
-
} catch (error) {
|
|
1340
|
-
const message = error && error.message ? error.message : 'Failed to close ACP session';
|
|
1341
|
-
res.status(message === 'Agent not found' ? 404 : 409).json({ error: message });
|
|
1342
|
-
}
|
|
1343
|
-
});
|
|
1344
|
-
|
|
1345
|
-
app.patch(routePath(BASE_PATH, '/api/agents/:agentId/acp-session'), express.json(), async (req, res) => {
|
|
1346
|
-
try {
|
|
1347
|
-
if (typeof req.body?.modeId === 'string') {
|
|
1348
|
-
res.json(await agentManager.setAcpSessionMode(req.params.agentId, req.body.modeId));
|
|
1349
|
-
return;
|
|
1350
|
-
}
|
|
1351
|
-
if (Array.isArray(req.body?.configOptions)) {
|
|
1352
|
-
res.json(await agentManager.setAcpSessionConfigOptions(req.params.agentId, req.body.configOptions));
|
|
1353
|
-
return;
|
|
1354
|
-
}
|
|
1355
|
-
if (typeof req.body?.configId === 'string' && Object.prototype.hasOwnProperty.call(req.body, 'value')) {
|
|
1356
|
-
res.json(await agentManager.setAcpSessionConfigOption(
|
|
1357
|
-
req.params.agentId,
|
|
1358
|
-
req.body.configId,
|
|
1359
|
-
req.body.value
|
|
1360
|
-
));
|
|
1361
|
-
return;
|
|
1362
|
-
}
|
|
1363
|
-
res.status(400).json({ error: 'ACP modeId, configOptions, or configId/value is required' });
|
|
1364
|
-
} catch (error) {
|
|
1365
|
-
const message = error && error.message ? error.message : 'Failed to update ACP session';
|
|
1366
|
-
res.status(message === 'Agent not found' ? 404 : 409).json({ error: message });
|
|
1367
|
-
}
|
|
1368
|
-
});
|
|
1369
|
-
|
|
1370
|
-
app.post(routePath(BASE_PATH, '/api/agents/:agentId/codex-terminal-profile'), express.json(), async (req, res) => {
|
|
1371
|
-
const requestAbort = new globalThis.AbortController();
|
|
1372
|
-
const abortRequest = () => requestAbort.abort(new Error('Terminal profile request was canceled'));
|
|
1373
|
-
const abortClosedResponse = () => {
|
|
1374
|
-
if (!res.writableEnded) abortRequest();
|
|
1375
|
-
};
|
|
1376
|
-
req.once('aborted', abortRequest);
|
|
1377
|
-
res.once('close', abortClosedResponse);
|
|
1378
|
-
try {
|
|
1379
|
-
const profile = await agentManager.setCodexTerminalProfile(req.params.agentId, {
|
|
1380
|
-
model: req.body?.model,
|
|
1381
|
-
effort: req.body?.effort,
|
|
1382
|
-
serviceTier: req.body?.serviceTier,
|
|
1383
|
-
}, {
|
|
1384
|
-
signal: requestAbort.signal,
|
|
1385
|
-
timeoutMs: 44_000,
|
|
1386
|
-
});
|
|
1387
|
-
res.json({ profile });
|
|
1388
|
-
} catch (error) {
|
|
1389
|
-
const message = error && error.message ? error.message : 'Failed to update Codex Terminal profile';
|
|
1390
|
-
const status = message === 'Agent not found'
|
|
1391
|
-
? 404
|
|
1392
|
-
: /^A valid Codex /.test(message)
|
|
1393
|
-
? 400
|
|
1394
|
-
: 409;
|
|
1395
|
-
if (!res.headersSent && !res.destroyed) res.status(status).json({ error: message });
|
|
1396
|
-
} finally {
|
|
1397
|
-
req.off('aborted', abortRequest);
|
|
1398
|
-
res.off('close', abortClosedResponse);
|
|
1399
|
-
}
|
|
1400
|
-
});
|
|
1401
|
-
|
|
1402
|
-
app.patch(routePath(BASE_PATH, '/api/agents/:agentId'), express.json(), async (req, res) => {
|
|
1403
|
-
if (!await requireAgentRecoveryForHttp(res)) return;
|
|
1404
|
-
const body = req.body || {};
|
|
1405
|
-
const updates = {};
|
|
1406
|
-
const providedPatchFields = [
|
|
1407
|
-
'customTitle',
|
|
1408
|
-
'task',
|
|
1409
|
-
'pinned',
|
|
1410
|
-
'unread',
|
|
1411
|
-
'archived',
|
|
1412
|
-
'readAttentionSeq',
|
|
1413
|
-
'readOutputEpoch',
|
|
1414
|
-
'readOutputSeq',
|
|
1415
|
-
'launchPermissionMode',
|
|
1416
|
-
'agentRuntimeMode',
|
|
1417
|
-
].filter(field => Object.prototype.hasOwnProperty.call(body, field));
|
|
1418
|
-
const lifecyclePatchFields = providedPatchFields.filter(field => (
|
|
1419
|
-
field === 'launchPermissionMode'
|
|
1420
|
-
|| field === 'agentRuntimeMode'
|
|
1421
|
-
|| (field === 'archived' && body.archived === true)
|
|
1422
|
-
));
|
|
1423
|
-
if (lifecyclePatchFields.length > 0 && providedPatchFields.length > 1) {
|
|
1424
|
-
res.status(400).json({
|
|
1425
|
-
error: 'Archive, permission restart, and runtime switch must be requested separately from other Agent updates',
|
|
1426
|
-
});
|
|
1427
|
-
return;
|
|
1428
|
-
}
|
|
1429
|
-
const ordinaryPatchGroups = [
|
|
1430
|
-
providedPatchFields.includes('customTitle') ? 'customTitle' : '',
|
|
1431
|
-
providedPatchFields.includes('task') ? 'task' : '',
|
|
1432
|
-
providedPatchFields.some(field => [
|
|
1433
|
-
'pinned',
|
|
1434
|
-
'unread',
|
|
1435
|
-
'archived',
|
|
1436
|
-
'readAttentionSeq',
|
|
1437
|
-
'readOutputEpoch',
|
|
1438
|
-
'readOutputSeq',
|
|
1439
|
-
].includes(field)) ? 'flags' : '',
|
|
1440
|
-
].filter(Boolean);
|
|
1441
|
-
if (ordinaryPatchGroups.length > 1) {
|
|
1442
|
-
res.status(400).json({
|
|
1443
|
-
error: 'Agent title, task, and flags must be updated in separate requests',
|
|
1444
|
-
});
|
|
1445
|
-
return;
|
|
1446
|
-
}
|
|
1447
|
-
|
|
1448
|
-
await agentManager.whenAgentLifecycleIdle(req.params.agentId);
|
|
1449
|
-
|
|
1450
|
-
if (typeof body.customTitle === 'string') {
|
|
1451
|
-
const result = agentManager.renameAgent(req.params.agentId, body.customTitle);
|
|
1452
|
-
if (result.error) {
|
|
1453
|
-
const status = result.error === 'Agent not found'
|
|
1454
|
-
? 404
|
|
1455
|
-
: (result.error.startsWith('Failed to ') ? 500 : 409);
|
|
1456
|
-
res.status(status).json({ error: result.error });
|
|
1457
|
-
return;
|
|
1458
|
-
}
|
|
1459
|
-
updates.customTitle = result.customTitle;
|
|
1460
|
-
}
|
|
1461
|
-
|
|
1462
|
-
if (typeof body.task === 'string') {
|
|
1463
|
-
const result = agentManager.setAgentTask(req.params.agentId, body.task);
|
|
1464
|
-
if (result.error) {
|
|
1465
|
-
const status = result.error === 'Agent not found'
|
|
1466
|
-
? 404
|
|
1467
|
-
: (result.error.startsWith('Failed to ') ? 500 : 409);
|
|
1468
|
-
res.status(status).json({ error: result.error });
|
|
1469
|
-
return;
|
|
1470
|
-
}
|
|
1471
|
-
updates.task = result.task;
|
|
1472
|
-
}
|
|
1473
|
-
|
|
1474
|
-
const flagPatch = {};
|
|
1475
|
-
['pinned', 'unread', 'archived'].forEach((flagName) => {
|
|
1476
|
-
if (typeof body[flagName] === 'boolean') {
|
|
1477
|
-
flagPatch[flagName] = body[flagName];
|
|
1478
|
-
}
|
|
1479
|
-
});
|
|
1480
|
-
if (typeof body.readAttentionSeq === 'number' && Number.isFinite(body.readAttentionSeq)) {
|
|
1481
|
-
flagPatch.readAttentionSeq = body.readAttentionSeq;
|
|
1482
|
-
}
|
|
1483
|
-
if (
|
|
1484
|
-
typeof body.readOutputEpoch === 'string'
|
|
1485
|
-
&& body.readOutputEpoch
|
|
1486
|
-
&& typeof body.readOutputSeq === 'number'
|
|
1487
|
-
&& Number.isFinite(body.readOutputSeq)
|
|
1488
|
-
) {
|
|
1489
|
-
flagPatch.readOutputEpoch = body.readOutputEpoch;
|
|
1490
|
-
flagPatch.readOutputSeq = body.readOutputSeq;
|
|
1491
|
-
}
|
|
1492
|
-
|
|
1493
|
-
if (flagPatch.archived === true) {
|
|
1494
|
-
const result = await agentManager.archiveAgent(req.params.agentId);
|
|
1495
|
-
if (result.error) {
|
|
1496
|
-
const status = result.stopped === true
|
|
1497
|
-
? 409
|
|
1498
|
-
: (result.error === 'Agent not found' ? 404 : 400);
|
|
1499
|
-
res.status(status).json(result);
|
|
1500
|
-
return;
|
|
1501
|
-
}
|
|
1502
|
-
Object.assign(updates, result);
|
|
1503
|
-
delete updates.agentId;
|
|
1504
|
-
delete flagPatch.archived;
|
|
1505
|
-
}
|
|
1506
|
-
|
|
1507
|
-
let flagUpdateRequiresState = false;
|
|
1508
|
-
if (Object.keys(flagPatch).length > 0) {
|
|
1509
|
-
const result = agentManager.updateAgentFlags(req.params.agentId, flagPatch);
|
|
1510
|
-
if (result.error) {
|
|
1511
|
-
const status = result.error === 'Agent not found'
|
|
1512
|
-
? 404
|
|
1513
|
-
: (result.error.startsWith('Failed to ') ? 500 : 409);
|
|
1514
|
-
res.status(status).json({ error: result.error });
|
|
1515
|
-
return;
|
|
1516
|
-
}
|
|
1517
|
-
Object.assign(updates, result);
|
|
1518
|
-
delete updates.agentId;
|
|
1519
|
-
flagUpdateRequiresState = result.requiresState === true;
|
|
1520
|
-
}
|
|
1521
|
-
|
|
1522
|
-
if (typeof body.launchPermissionMode === 'string') {
|
|
1523
|
-
const result = await agentManager.syncCodexTerminalPermissionMode(req.params.agentId, body.launchPermissionMode);
|
|
1524
|
-
if (result.error) {
|
|
1525
|
-
const status = result.error === 'Agent not found' ? 404 : 400;
|
|
1526
|
-
res.status(status).json({ error: result.error });
|
|
1527
|
-
return;
|
|
1528
|
-
}
|
|
1529
|
-
updates.launchPermissionMode = result.launchPermissionMode;
|
|
1530
|
-
if (result.restarted === true) updates.restarted = true;
|
|
1531
|
-
if (result.restartedAgentId) updates.restartedAgentId = result.restartedAgentId;
|
|
1532
|
-
}
|
|
1533
|
-
|
|
1534
|
-
if (typeof body.agentRuntimeMode === 'string') {
|
|
1535
|
-
const result = await agentManager.restartAgentRuntimeMode(req.params.agentId, body.agentRuntimeMode);
|
|
1536
|
-
if (result.error) {
|
|
1537
|
-
const status = result.error === 'Agent not found' ? 404 : 400;
|
|
1538
|
-
res.status(status).json({ error: result.error });
|
|
1539
|
-
return;
|
|
1540
|
-
}
|
|
1541
|
-
updates.agentRuntimeMode = result.agentRuntimeMode;
|
|
1542
|
-
if (result.restarted === true) updates.restarted = true;
|
|
1543
|
-
if (result.restartedAgentId) updates.restartedAgentId = result.restartedAgentId;
|
|
1544
|
-
if (result.switchFailed === true) updates.switchFailed = true;
|
|
1545
|
-
if (result.warning) updates.warning = result.warning;
|
|
1546
|
-
}
|
|
1547
|
-
|
|
1548
|
-
if (Object.keys(updates).length === 0) {
|
|
1549
|
-
res.status(400).json({ error: 'customTitle, task, pinned, unread, archived, readAttentionSeq, readOutputEpoch/readOutputSeq, launchPermissionMode, or agentRuntimeMode is required' });
|
|
1550
|
-
return;
|
|
1551
|
-
}
|
|
1552
|
-
|
|
1553
|
-
if (flagUpdateRequiresState || typeof body.task === 'string' || typeof body.customTitle === 'string' || typeof body.launchPermissionMode === 'string' || typeof body.agentRuntimeMode === 'string') {
|
|
1554
|
-
scheduleBroadcastState();
|
|
1555
|
-
}
|
|
1556
|
-
res.json({ agentId: req.params.agentId, ...updates });
|
|
1557
|
-
});
|
|
1558
|
-
|
|
1559
|
-
app.post(routePath(BASE_PATH, '/api/agents/:agentId/reorder'), express.json(), async (req, res) => {
|
|
1560
|
-
if (!await requireAgentRecoveryForHttp(res)) return;
|
|
1561
|
-
await agentManager.whenAgentLifecycleIdle(req.params.agentId);
|
|
1562
|
-
const result = agentManager.reorderAgent(req.params.agentId, {
|
|
1563
|
-
beforeAgentId: req.body?.beforeAgentId,
|
|
1564
|
-
afterAgentId: req.body?.afterAgentId,
|
|
1565
|
-
});
|
|
1566
|
-
if (result.error) {
|
|
1567
|
-
const status = result.error === 'Agent not found'
|
|
1568
|
-
? 404
|
|
1569
|
-
: (result.error.startsWith('Failed to ') ? 500 : 409);
|
|
1570
|
-
res.status(status).json({ error: result.error });
|
|
1571
|
-
return;
|
|
1572
|
-
}
|
|
1573
|
-
scheduleBroadcastState();
|
|
1574
|
-
res.json(result);
|
|
1575
|
-
});
|
|
1576
|
-
|
|
1577
|
-
app.post(routePath(BASE_PATH, '/api/agents/:agentId/fork'), express.json(), async (req, res) => {
|
|
1578
|
-
if (!await requireAgentRecoveryForHttp(res)) return;
|
|
1579
|
-
const mode = req.body && typeof req.body.mode === 'string' ? req.body.mode : 'same-worktree';
|
|
1580
|
-
const targetRuntime = req.body && typeof req.body.targetRuntime === 'string'
|
|
1581
|
-
? req.body.targetRuntime
|
|
1582
|
-
: '';
|
|
1583
|
-
if (targetRuntime && targetRuntime !== 'chat') {
|
|
1584
|
-
res.status(400).json({ error: 'Unsupported Fork target runtime' });
|
|
1585
|
-
return;
|
|
1586
|
-
}
|
|
1587
|
-
const requestId = typeof req.body?.requestId === 'string' ? req.body.requestId.trim() : '';
|
|
1588
|
-
if (!/^[A-Za-z0-9._:-]{1,160}$/.test(requestId)) {
|
|
1589
|
-
res.status(400).json({ error: 'Fork requires a valid requestId' });
|
|
1590
|
-
return;
|
|
1591
|
-
}
|
|
1592
|
-
const result = await agentManager.forkAgent(req.params.agentId, mode, {
|
|
1593
|
-
requestId,
|
|
1594
|
-
...(targetRuntime ? { targetRuntime } : {}),
|
|
1595
|
-
...(Number.isSafeInteger(req.body?.expectedRevision)
|
|
1596
|
-
? { expectedRevision: req.body.expectedRevision }
|
|
1597
|
-
: {}),
|
|
1598
|
-
});
|
|
1599
|
-
if (result.error) {
|
|
1600
|
-
const status = result.error === 'Agent not found' ? 404 : 400;
|
|
1601
|
-
res.status(status).json(result);
|
|
1602
|
-
return;
|
|
1603
|
-
}
|
|
1604
|
-
|
|
1605
|
-
try {
|
|
1606
|
-
const membership = configManager.mountProjectWorkspace(result.workspace);
|
|
1607
|
-
result.projectWorkspaces = membership.projectWorkspaces;
|
|
1608
|
-
result.pinnedProjectWorkspaces = membership.pinnedProjectWorkspaces;
|
|
1609
|
-
} catch (error) {
|
|
1610
|
-
broadcastState();
|
|
1611
|
-
const mountError = error.message || 'Failed to create Project';
|
|
1612
|
-
res.status(500).json({
|
|
1613
|
-
...result,
|
|
1614
|
-
error: `${mountError}. Retry the same Fork request to reconcile Project membership.`,
|
|
1615
|
-
retryable: true,
|
|
1616
|
-
});
|
|
1617
|
-
return;
|
|
1618
|
-
}
|
|
1619
|
-
broadcastState();
|
|
1620
|
-
res.status(201).json(result);
|
|
1621
|
-
});
|
|
1622
|
-
|
|
1623
|
-
function resolveProjectActionRoot(rootId) {
|
|
1624
|
-
const root = workspaceRootRegistry.resolve(rootId);
|
|
1625
|
-
if (!root || root.kind === 'global') {
|
|
1626
|
-
throw new WorkspaceFileError('project workspace is required', 400);
|
|
1627
|
-
}
|
|
1628
|
-
return root;
|
|
1629
|
-
}
|
|
1630
|
-
|
|
1631
|
-
app.post(routePath(BASE_PATH, '/api/projects/reveal'), express.json(), async (req, res) => {
|
|
1632
|
-
try {
|
|
1633
|
-
const root = resolveProjectActionRoot(req.body?.rootId);
|
|
1634
|
-
const command = process.platform === 'darwin'
|
|
1635
|
-
? 'open'
|
|
1636
|
-
: process.platform === 'win32'
|
|
1637
|
-
? 'explorer'
|
|
1638
|
-
: 'xdg-open';
|
|
1639
|
-
await execFileAsync(command, [root.canonicalPath], {
|
|
1640
|
-
timeout: 15000,
|
|
1641
|
-
maxBuffer: 1024 * 1024,
|
|
1642
|
-
});
|
|
1643
|
-
res.json({ revealed: true, workspace: root.canonicalPath });
|
|
1644
|
-
} catch (error) {
|
|
1645
|
-
const status = error instanceof WorkspaceFileError ? error.statusCode : 500;
|
|
1646
|
-
res.status(status).json({ error: error.message || 'Failed to reveal project workspace' });
|
|
1647
|
-
}
|
|
1648
|
-
});
|
|
1649
|
-
|
|
1650
|
-
app.post(routePath(BASE_PATH, '/api/projects/mount'), express.json(), async (req, res) => {
|
|
1651
|
-
try {
|
|
1652
|
-
const workspace = await canonicalProjectWorkspace(req.body?.workspace);
|
|
1653
|
-
const membership = configManager.mountProjectWorkspace(workspace);
|
|
1654
|
-
broadcastState();
|
|
1655
|
-
res.json(membership);
|
|
1656
|
-
} catch (error) {
|
|
1657
|
-
res.status(400).json({ error: error.message || 'Failed to create Project' });
|
|
1658
|
-
}
|
|
1659
|
-
});
|
|
1660
|
-
|
|
1661
|
-
app.post(routePath(BASE_PATH, '/api/projects/remove'), express.json(), (req, res) => {
|
|
1662
|
-
try {
|
|
1663
|
-
const membership = configManager.removeProjectWorkspace(req.body?.workspace);
|
|
1664
|
-
broadcastState();
|
|
1665
|
-
res.json(membership);
|
|
1666
|
-
} catch (error) {
|
|
1667
|
-
res.status(400).json({ error: error.message || 'Failed to remove Project' });
|
|
1668
|
-
}
|
|
1669
|
-
});
|
|
1670
|
-
|
|
1671
|
-
app.post(routePath(BASE_PATH, '/api/projects/pin'), express.json(), (req, res) => {
|
|
1672
|
-
try {
|
|
1673
|
-
const membership = configManager.setProjectWorkspacePinned(
|
|
1674
|
-
req.body?.workspace,
|
|
1675
|
-
req.body?.pinned === true
|
|
1676
|
-
);
|
|
1677
|
-
broadcastState();
|
|
1678
|
-
res.json(membership);
|
|
1679
|
-
} catch (error) {
|
|
1680
|
-
res.status(400).json({ error: error.message || 'Failed to update Project pin' });
|
|
1681
|
-
}
|
|
1682
|
-
});
|
|
1683
|
-
|
|
1684
|
-
app.patch(routePath(BASE_PATH, '/api/projects/name'), express.json(), (req, res) => {
|
|
1685
|
-
try {
|
|
1686
|
-
const result = configManager.setProjectName(req.body?.workspace, req.body?.name);
|
|
1687
|
-
scheduleBroadcastState();
|
|
1688
|
-
res.json(result);
|
|
1689
|
-
} catch (error) {
|
|
1690
|
-
res.status(400).json({ error: error.message || 'Failed to rename Project' });
|
|
1691
|
-
}
|
|
1692
|
-
});
|
|
1693
|
-
|
|
1694
|
-
app.post(routePath(BASE_PATH, '/api/projects/create-worktree'), express.json(), async (req, res) => {
|
|
1695
|
-
const requestId = typeof req.body?.requestId === 'string' ? req.body.requestId.trim() : '';
|
|
1696
|
-
if (!/^[A-Za-z0-9._:-]{1,160}$/.test(requestId)) {
|
|
1697
|
-
res.status(400).json({ error: 'Project worktree creation requires a valid requestId' });
|
|
1698
|
-
return;
|
|
1699
|
-
}
|
|
1700
|
-
try {
|
|
1701
|
-
const root = resolveProjectActionRoot(req.body?.rootId);
|
|
1702
|
-
const created = await agentManager.createPermanentWorktree(root.canonicalPath, { requestId });
|
|
1703
|
-
broadcastState();
|
|
1704
|
-
res.status(201).json({
|
|
1705
|
-
...created,
|
|
1706
|
-
});
|
|
1707
|
-
} catch (error) {
|
|
1708
|
-
const status = error instanceof WorkspaceFileError ? error.statusCode : 400;
|
|
1709
|
-
const operation = configManager.getProjectOperation?.(requestId);
|
|
1710
|
-
res.status(status).json({
|
|
1711
|
-
error: error.message || 'Failed to create permanent worktree',
|
|
1712
|
-
requestId,
|
|
1713
|
-
...(operation?.state === 'pending' ? { retryable: true } : {}),
|
|
1714
|
-
...(['unknown', 'blocked'].includes(operation?.state) ? { uncertain: true } : {}),
|
|
1715
|
-
});
|
|
1716
|
-
}
|
|
1717
|
-
});
|
|
1718
|
-
|
|
1719
|
-
app.post(routePath(BASE_PATH, '/api/projects/delete-worktree'), express.json(), async (req, res) => {
|
|
1720
|
-
if (!await requireAgentRecoveryForHttp(res)) return;
|
|
1721
|
-
const body = req.body || {};
|
|
1722
|
-
const workspace = typeof body.workspace === 'string' ? body.workspace : '';
|
|
1723
|
-
const requestId = typeof body.requestId === 'string' ? body.requestId.trim() : '';
|
|
1724
|
-
if (!/^[A-Za-z0-9._:-]{1,160}$/.test(requestId)) {
|
|
1725
|
-
res.status(400).json({ error: 'Project worktree deletion requires a valid requestId' });
|
|
1726
|
-
return;
|
|
1727
|
-
}
|
|
1728
|
-
|
|
1729
|
-
const result = await agentManager.deleteForkWorktreeProject(workspace, {
|
|
1730
|
-
force: body.force === true,
|
|
1731
|
-
requestId,
|
|
1732
|
-
});
|
|
1733
|
-
if (result.error) {
|
|
1734
|
-
broadcastState();
|
|
1735
|
-
const status = result.requiresForce
|
|
1736
|
-
? 409
|
|
1737
|
-
: (result.error === 'Workspace not found' || result.error === 'Workspace is required' ? 404 : 400);
|
|
1738
|
-
res.status(status).json(result);
|
|
1739
|
-
return;
|
|
1740
|
-
}
|
|
1741
|
-
broadcastState();
|
|
1742
|
-
res.json(result);
|
|
1743
|
-
});
|
|
1744
|
-
|
|
1745
|
-
app.post(routePath(BASE_PATH, '/api/codex/sessions/:sessionId/resume'), express.json(), async (req, res) => {
|
|
1746
|
-
await startResumedAgentSession(req, res, 'codex', req.params.sessionId);
|
|
1747
|
-
});
|
|
1748
|
-
|
|
1749
|
-
app.post(routePath(BASE_PATH, '/api/agent-sessions/:provider/:sessionId/resume'), express.json(), async (req, res) => {
|
|
1750
|
-
await startResumedAgentSession(req, res, req.params.provider, req.params.sessionId);
|
|
1751
|
-
});
|
|
1752
|
-
|
|
1753
|
-
const pendingResumeStarts = new Map();
|
|
1754
|
-
const pendingProjectWorkspaceResolutions = new Map();
|
|
1755
|
-
|
|
1756
|
-
function resumedAgentStartKey(provider, sessionId, options = {}) {
|
|
1757
|
-
return [
|
|
1758
|
-
provider,
|
|
1759
|
-
options.providerHomeId || 'default',
|
|
1760
|
-
sessionId,
|
|
1761
|
-
options.fork === true ? 'fork' : 'resume',
|
|
1762
|
-
options.asMain === true ? 'main' : 'agent',
|
|
1763
|
-
].join(':');
|
|
1764
|
-
}
|
|
1765
|
-
|
|
1766
|
-
function findResumedAgent(provider, sessionId, providerHomeId = '') {
|
|
1767
|
-
return findActiveAgentClaimingSession(agentManager.getState().agents, provider, { id: sessionId, providerHomeId });
|
|
1768
|
-
}
|
|
1769
|
-
|
|
1770
|
-
function isMainAgentSessionWorkspace(session) {
|
|
1771
|
-
const values = [session && session.cwd, session && session.workspace];
|
|
1772
|
-
return values.some(value => {
|
|
1773
|
-
const normalized = String(value || '').trim().replace(/[\\/]+$/, '');
|
|
1774
|
-
return normalized === '~/.farming' || /(^|[/\\])\.farming$/.test(normalized);
|
|
1775
|
-
});
|
|
1776
|
-
}
|
|
1777
|
-
|
|
1778
|
-
function rememberMainPageAgentSession(provider, sessionId, providerHomeId = '') {
|
|
1779
|
-
const sessionKey = mainPageAgentSessionKey(provider, sessionId, providerHomeId);
|
|
1780
|
-
if (typeof configManager.rememberMainPageSessionKey === 'function') {
|
|
1781
|
-
configManager.rememberMainPageSessionKey(sessionKey, {
|
|
1782
|
-
provider,
|
|
1783
|
-
providerSessionId: sessionId,
|
|
1784
|
-
providerSessionKey: sessionKey,
|
|
1785
|
-
providerHomeId: providerHomeId || 'default',
|
|
1786
|
-
source: 'resume',
|
|
1787
|
-
});
|
|
1788
|
-
return;
|
|
1789
|
-
}
|
|
1790
|
-
const currentKeys = typeof configManager.getMainPageSessionKeys === 'function'
|
|
1791
|
-
? configManager.getMainPageSessionKeys()
|
|
1792
|
-
: (Array.isArray(configManager.getSettings().mainPageSessionKeys) ? configManager.getSettings().mainPageSessionKeys : []);
|
|
1793
|
-
configManager.updateSettings({
|
|
1794
|
-
mainPageSessionKeys: [
|
|
1795
|
-
sessionKey,
|
|
1796
|
-
...currentKeys.filter(key => key !== sessionKey),
|
|
1797
|
-
],
|
|
1798
|
-
});
|
|
1799
|
-
}
|
|
1800
|
-
|
|
1801
|
-
function forgetMainPageAgentSession(provider, sessionId, providerHomeId = '') {
|
|
1802
|
-
const sessionKey = mainPageAgentSessionKey(provider, sessionId, providerHomeId);
|
|
1803
|
-
if (typeof configManager.removeMainPageSessionKey === 'function') {
|
|
1804
|
-
configManager.removeMainPageSessionKey(sessionKey);
|
|
1805
|
-
return;
|
|
1806
|
-
}
|
|
1807
|
-
const currentKeys = typeof configManager.getMainPageSessionKeys === 'function'
|
|
1808
|
-
? configManager.getMainPageSessionKeys()
|
|
1809
|
-
: (Array.isArray(configManager.getSettings().mainPageSessionKeys) ? configManager.getSettings().mainPageSessionKeys : []);
|
|
1810
|
-
if (!currentKeys.includes(sessionKey)) return;
|
|
1811
|
-
configManager.updateSettings({
|
|
1812
|
-
mainPageSessionKeys: currentKeys.filter(key => key !== sessionKey),
|
|
1813
|
-
});
|
|
1814
|
-
}
|
|
1815
|
-
|
|
1816
|
-
function savedFarmingAgentSession(provider, sessionId, providerHomeId = '') {
|
|
1817
|
-
if (typeof configManager.getAgentSessionRecordForProviderSessionKey !== 'function') return null;
|
|
1818
|
-
const sessionKey = mainPageAgentSessionKey(provider, sessionId, providerHomeId);
|
|
1819
|
-
return sessionKey
|
|
1820
|
-
? configManager.getAgentSessionRecordForProviderSessionKey(sessionKey)
|
|
1821
|
-
: null;
|
|
1822
|
-
}
|
|
1823
|
-
|
|
1824
|
-
async function resumeAgentSessionById(provider, rawSessionId, options = {}) {
|
|
1825
|
-
const normalizedProvider = normalizeProvider(provider);
|
|
1826
|
-
const sessionId = String(rawSessionId || '').trim();
|
|
1827
|
-
const providerHomeId = typeof options.providerHomeId === 'string' && options.providerHomeId.trim() ? options.providerHomeId.trim() : 'default';
|
|
1828
|
-
if (!normalizedProvider || !isSafeSessionId(sessionId)) {
|
|
1829
|
-
return { error: 'invalid session id', status: 400 };
|
|
1830
|
-
}
|
|
1831
|
-
|
|
1832
|
-
const shouldFork = options.fork === true;
|
|
1833
|
-
const requestedAsMain = options.asMain === true && !shouldFork;
|
|
1834
|
-
const shouldRememberMainPageSession = options.rememberMainPageSession !== false && !shouldFork && !requestedAsMain;
|
|
1835
|
-
const pendingResumeId = resumedAgentStartKey(normalizedProvider, sessionId, {
|
|
1836
|
-
fork: shouldFork,
|
|
1837
|
-
asMain: requestedAsMain,
|
|
1838
|
-
providerHomeId,
|
|
1839
|
-
});
|
|
1840
|
-
if (!shouldFork) {
|
|
1841
|
-
const existingAgent = findResumedAgent(normalizedProvider, sessionId, providerHomeId);
|
|
1842
|
-
if (existingAgent) {
|
|
1843
|
-
if (shouldRememberMainPageSession) rememberMainPageAgentSession(normalizedProvider, sessionId, providerHomeId);
|
|
1844
|
-
const projectWorkspace = requestedAsMain
|
|
1845
|
-
? ''
|
|
1846
|
-
: await canonicalProjectWorkspace(
|
|
1847
|
-
existingAgent.gitWorktree?.workspace || existingAgent.projectWorkspace || existingAgent.cwd
|
|
1848
|
-
);
|
|
1849
|
-
return { agentId: existingAgent.id, projectWorkspace, reused: true };
|
|
1850
|
-
}
|
|
1851
|
-
}
|
|
1852
|
-
const pendingStart = pendingResumeStarts.get(pendingResumeId);
|
|
1853
|
-
if (pendingStart) {
|
|
1854
|
-
const result = await pendingStart;
|
|
1855
|
-
if (result.error) {
|
|
1856
|
-
return result;
|
|
1857
|
-
}
|
|
1858
|
-
if (shouldRememberMainPageSession) rememberMainPageAgentSession(normalizedProvider, sessionId, providerHomeId);
|
|
1859
|
-
return {
|
|
1860
|
-
agentId: result.agentId,
|
|
1861
|
-
projectWorkspace: result.projectWorkspace || '',
|
|
1862
|
-
reused: true,
|
|
1863
|
-
pending: true,
|
|
1864
|
-
...(result.claimed ? { claimed: true } : {}),
|
|
1865
|
-
};
|
|
1866
|
-
}
|
|
1867
|
-
|
|
1868
|
-
const startPromise = (async () => {
|
|
1869
|
-
let session = await findAgentSession(normalizedProvider, sessionId, {
|
|
1870
|
-
limit: 1000,
|
|
1871
|
-
providerLimit: 1000,
|
|
1872
|
-
scanLimit: 5000,
|
|
1873
|
-
providerHomeId,
|
|
1874
|
-
providerHomes: configuredProviderHomes(),
|
|
1875
|
-
});
|
|
1876
|
-
if (
|
|
1877
|
-
options.allowUnarchiveArchived === true
|
|
1878
|
-
&& normalizedProvider === 'codex'
|
|
1879
|
-
&& !requestedAsMain
|
|
1880
|
-
) {
|
|
1881
|
-
const providerHomes = configuredProviderHomes();
|
|
1882
|
-
const configuredHomePath = (providerHomes.codex || [])
|
|
1883
|
-
.find(home => home.id === providerHomeId)?.path || '';
|
|
1884
|
-
const unarchiveResult = await agentManager.ensureCodexSessionAvailable(sessionId, {
|
|
1885
|
-
providerHomeId,
|
|
1886
|
-
providerHomePath: session?.providerHomePath || configuredHomePath,
|
|
1887
|
-
providerHomes,
|
|
1888
|
-
cwd: session?.cwd || session?.workspace || '',
|
|
1889
|
-
});
|
|
1890
|
-
if (unarchiveResult?.error) return unarchiveResult;
|
|
1891
|
-
session = await findAgentSession(normalizedProvider, sessionId, {
|
|
1892
|
-
limit: 1000,
|
|
1893
|
-
providerLimit: 1000,
|
|
1894
|
-
scanLimit: 5000,
|
|
1895
|
-
providerHomeId,
|
|
1896
|
-
providerHomes,
|
|
1897
|
-
}) || (session ? { ...session, archived: false } : session);
|
|
1898
|
-
}
|
|
1899
|
-
if (session && session.archived && !shouldFork) {
|
|
1900
|
-
forgetMainPageAgentSession(normalizedProvider, sessionId, providerHomeId);
|
|
1901
|
-
return {
|
|
1902
|
-
error: `${session.providerName || normalizedProvider} session is archived. Unarchive it before resuming.`,
|
|
1903
|
-
status: 409,
|
|
1904
|
-
archived: true,
|
|
1905
|
-
};
|
|
1906
|
-
}
|
|
1907
|
-
if (!shouldFork && !requestedAsMain) {
|
|
1908
|
-
const claimingAgent = findActiveAgentClaimingSession(agentManager.getState().agents, normalizedProvider, {
|
|
1909
|
-
id: sessionId,
|
|
1910
|
-
providerHomeId,
|
|
1911
|
-
...(session || {}),
|
|
1912
|
-
});
|
|
1913
|
-
if (claimingAgent) {
|
|
1914
|
-
if (shouldRememberMainPageSession) rememberMainPageAgentSession(normalizedProvider, sessionId, providerHomeId);
|
|
1915
|
-
const projectWorkspace = await canonicalProjectWorkspace(
|
|
1916
|
-
claimingAgent.gitWorktree?.workspace || claimingAgent.projectWorkspace || claimingAgent.cwd
|
|
1917
|
-
);
|
|
1918
|
-
return { agentId: claimingAgent.id, projectWorkspace, reused: true, claimed: true };
|
|
1919
|
-
}
|
|
1920
|
-
}
|
|
1921
|
-
|
|
1922
|
-
const resumeAsMain = requestedAsMain && isMainAgentSessionWorkspace(session);
|
|
1923
|
-
if (requestedAsMain && !resumeAsMain) {
|
|
1924
|
-
return { error: 'session is not a Main Agent session', status: 400 };
|
|
1925
|
-
}
|
|
1926
|
-
|
|
1927
|
-
const savedSession = shouldFork
|
|
1928
|
-
? null
|
|
1929
|
-
: savedFarmingAgentSession(normalizedProvider, sessionId, session
|
|
1930
|
-
? (session.providerHomeId || providerHomeId)
|
|
1931
|
-
: providerHomeId);
|
|
1932
|
-
const hasRequestedCustomTitle = Object.prototype.hasOwnProperty.call(options, 'customTitle');
|
|
1933
|
-
const savedAttentionSeq = Number(savedSession?.attentionSeq) || 0;
|
|
1934
|
-
const savedReadAttentionSeq = Number(savedSession?.readAttentionSeq) || 0;
|
|
1935
|
-
const workingDirectory = session && (session.cwd || session.workspace) ? (session.cwd || session.workspace) : null;
|
|
1936
|
-
const projectWorkspace = resumeAsMain
|
|
1937
|
-
? ''
|
|
1938
|
-
: await canonicalProjectWorkspace(
|
|
1939
|
-
savedSession?.projectWorkspace || (session ? (session.workspace || session.cwd || '') : workingDirectory)
|
|
1940
|
-
);
|
|
1941
|
-
const command = buildAgentSessionResumeCommand(normalizedProvider, sessionId, {
|
|
1942
|
-
fork: shouldFork,
|
|
1943
|
-
cwd: workingDirectory,
|
|
1944
|
-
modelProvider: normalizedProvider === 'codex'
|
|
1945
|
-
? resolveCodexResumeModelProvider(session ? session.providerHomePath : '')
|
|
1946
|
-
: '',
|
|
1947
|
-
});
|
|
1948
|
-
|
|
1949
|
-
if (!command) {
|
|
1950
|
-
return { error: 'invalid session id', status: 400 };
|
|
1951
|
-
}
|
|
1952
|
-
|
|
1953
|
-
return new Promise((resolve) => {
|
|
1954
|
-
const resolvedProviderHomeId = session ? (session.providerHomeId || providerHomeId) : providerHomeId;
|
|
1955
|
-
const resumeSource = resumedAgentSource(normalizedProvider, sessionId, resolvedProviderHomeId);
|
|
1956
|
-
const startResult = agentManager.startAgent(command, workingDirectory, (agentId, error) => {
|
|
1957
|
-
if (error) {
|
|
1958
|
-
resolve({ error, status: 400 });
|
|
1959
|
-
return;
|
|
1960
|
-
}
|
|
1961
|
-
|
|
1962
|
-
if (!agentId) {
|
|
1963
|
-
resolve({ error: 'failed to resume agent session', status: 500 });
|
|
1964
|
-
return;
|
|
1965
|
-
}
|
|
1966
|
-
|
|
1967
|
-
resolve({ agentId, projectWorkspace });
|
|
1968
|
-
}, {
|
|
1969
|
-
wantsMain: resumeAsMain,
|
|
1970
|
-
task: savedSession?.task || (session ? session.title : ''),
|
|
1971
|
-
workflowTemplate: savedSession?.workflowTemplate || '',
|
|
1972
|
-
customTitle: hasRequestedCustomTitle
|
|
1973
|
-
? (typeof options.customTitle === 'string' ? options.customTitle : '')
|
|
1974
|
-
: (savedSession?.customTitle || ''),
|
|
1975
|
-
customTitleExplicit: hasRequestedCustomTitle,
|
|
1976
|
-
requiredCliVersion: normalizedProvider === 'codex' && session ? session.cliVersion : '',
|
|
1977
|
-
projectWorkspace,
|
|
1978
|
-
source: shouldFork ? resumeSource.replace('-history:', '-history-fork:') : resumeSource,
|
|
1979
|
-
agentRuntimeMode: ['chat', 'acp'].includes(options.agentRuntimeMode) ? 'chat' : 'terminal',
|
|
1980
|
-
acpHistoryMode: options.acpHistoryMode === 'resume' ? 'resume' : 'load',
|
|
1981
|
-
providerHomeId: resolvedProviderHomeId,
|
|
1982
|
-
providerHomePath: session ? (session.providerHomePath || '') : '',
|
|
1983
|
-
providerSessionTitle: session?.title || savedSession?.providerSessionTitle || '',
|
|
1984
|
-
persistentSessionId: savedSession?.id || '',
|
|
1985
|
-
pinned: savedSession?.pinned === true,
|
|
1986
|
-
projectOrder: savedSession?.projectOrder,
|
|
1987
|
-
pinnedOrder: savedSession?.pinnedOrder,
|
|
1988
|
-
attentionSeq: savedAttentionSeq,
|
|
1989
|
-
readAttentionSeq: savedReadAttentionSeq,
|
|
1990
|
-
attentionUpdatedAt: savedSession?.attentionUpdatedAt,
|
|
1991
|
-
readAttentionAt: savedSession?.readAttentionAt,
|
|
1992
|
-
attentionReason: savedSession?.attentionReason,
|
|
1993
|
-
attentionOutputEpoch: savedSession?.attentionOutputEpoch,
|
|
1994
|
-
attentionOutputSeq: savedSession?.attentionOutputSeq,
|
|
1995
|
-
readOutputEpoch: savedSession?.readOutputEpoch,
|
|
1996
|
-
readOutputSeq: savedSession?.readOutputSeq,
|
|
1997
|
-
autoReadInitialAttention: options.autoReadInitialAttention === true
|
|
1998
|
-
&& savedAttentionSeq <= savedReadAttentionSeq,
|
|
1999
|
-
preserveProviderSessionProfile: normalizedProvider === 'codex',
|
|
2000
|
-
});
|
|
2001
|
-
Promise.resolve(startResult).catch((error) => {
|
|
2002
|
-
resolve({ error: error.message || 'failed to resume agent session', status: 500 });
|
|
2003
|
-
});
|
|
2004
|
-
});
|
|
2005
|
-
})();
|
|
2006
|
-
pendingResumeStarts.set(pendingResumeId, startPromise);
|
|
2007
|
-
|
|
2008
|
-
const result = await startPromise;
|
|
2009
|
-
if (pendingResumeStarts.get(pendingResumeId) === startPromise) {
|
|
2010
|
-
pendingResumeStarts.delete(pendingResumeId);
|
|
2011
|
-
}
|
|
2012
|
-
|
|
2013
|
-
if (result.error) {
|
|
2014
|
-
return result;
|
|
2015
|
-
}
|
|
2016
|
-
|
|
2017
|
-
if (shouldRememberMainPageSession) rememberMainPageAgentSession(normalizedProvider, sessionId, providerHomeId);
|
|
2018
|
-
if (result.reused) {
|
|
2019
|
-
return {
|
|
2020
|
-
agentId: result.agentId,
|
|
2021
|
-
projectWorkspace: result.projectWorkspace || '',
|
|
2022
|
-
reused: true,
|
|
2023
|
-
...(result.claimed ? { claimed: true } : {}),
|
|
2024
|
-
};
|
|
2025
|
-
}
|
|
2026
|
-
return {
|
|
2027
|
-
agentId: result.agentId,
|
|
2028
|
-
projectWorkspace: result.projectWorkspace || '',
|
|
2029
|
-
};
|
|
2030
|
-
}
|
|
2031
|
-
|
|
2032
|
-
async function canonicalProjectWorkspace(workspace) {
|
|
2033
|
-
const candidate = configManager.expandWorkspacePath(String(workspace || '').trim());
|
|
2034
|
-
if (!candidate) return '';
|
|
2035
|
-
const pending = pendingProjectWorkspaceResolutions.get(candidate);
|
|
2036
|
-
if (pending) return pending;
|
|
2037
|
-
|
|
2038
|
-
const resolution = (async () => {
|
|
2039
|
-
try {
|
|
2040
|
-
const worktree = await inspectGitWorktree(candidate);
|
|
2041
|
-
if (worktree?.workspace) return worktree.workspace;
|
|
2042
|
-
} catch (error) {
|
|
2043
|
-
console.warn('Failed to resolve project worktree:', candidate, error?.message || error);
|
|
2044
|
-
}
|
|
2045
|
-
try {
|
|
2046
|
-
return fs.realpathSync(path.resolve(candidate));
|
|
2047
|
-
} catch {
|
|
2048
|
-
return candidate;
|
|
2049
|
-
}
|
|
2050
|
-
})();
|
|
2051
|
-
pendingProjectWorkspaceResolutions.set(candidate, resolution);
|
|
2052
|
-
try {
|
|
2053
|
-
return await resolution;
|
|
2054
|
-
} finally {
|
|
2055
|
-
if (pendingProjectWorkspaceResolutions.get(candidate) === resolution) {
|
|
2056
|
-
pendingProjectWorkspaceResolutions.delete(candidate);
|
|
2057
|
-
}
|
|
2058
|
-
}
|
|
2059
|
-
}
|
|
2060
|
-
|
|
2061
|
-
async function startResumedAgentSession(req, res, provider, rawSessionId) {
|
|
2062
|
-
const shouldFork = req.body && req.body.fork === true;
|
|
2063
|
-
const requestedAsMain = req.body && req.body.asMain === true && !shouldFork;
|
|
2064
|
-
const allowUnarchiveArchived = req.body && req.body.unarchiveArchived === true && !shouldFork && !requestedAsMain;
|
|
2065
|
-
const providerHomeId = req.body && typeof req.body.providerHomeId === 'string'
|
|
2066
|
-
? req.body.providerHomeId
|
|
2067
|
-
: '';
|
|
2068
|
-
const sessionKey = mainPageAgentSessionKey(provider, rawSessionId, providerHomeId);
|
|
2069
|
-
const mainPageSessionWasRemembered = !shouldFork
|
|
2070
|
-
&& !requestedAsMain
|
|
2071
|
-
&& configManager.getMainPageSessionKeys().includes(sessionKey);
|
|
2072
|
-
if (
|
|
2073
|
-
req.body
|
|
2074
|
-
&& Object.prototype.hasOwnProperty.call(req.body, 'customTitle')
|
|
2075
|
-
&& typeof req.body.customTitle !== 'string'
|
|
2076
|
-
) {
|
|
2077
|
-
res.status(400).json({ error: 'customTitle must be a string' });
|
|
2078
|
-
return;
|
|
2079
|
-
}
|
|
2080
|
-
const result = await resumeAgentSessionById(provider, rawSessionId, {
|
|
2081
|
-
fork: shouldFork,
|
|
2082
|
-
asMain: requestedAsMain,
|
|
2083
|
-
allowUnarchiveArchived,
|
|
2084
|
-
providerHomeId,
|
|
2085
|
-
...(req.body && Object.prototype.hasOwnProperty.call(req.body, 'customTitle')
|
|
2086
|
-
? { customTitle: req.body.customTitle }
|
|
2087
|
-
: {}),
|
|
2088
|
-
agentRuntimeMode: req.body && ['chat', 'acp'].includes(req.body.agentRuntimeMode) ? 'chat' : 'terminal',
|
|
2089
|
-
acpHistoryMode: req.body && req.body.acpHistoryMode === 'resume' ? 'resume' : 'load',
|
|
2090
|
-
});
|
|
2091
|
-
|
|
2092
|
-
if (result.error) {
|
|
2093
|
-
res.status(result.status || 400).json({ error: result.error });
|
|
2094
|
-
return;
|
|
2095
|
-
}
|
|
2096
|
-
|
|
2097
|
-
let projectMembership = {
|
|
2098
|
-
projectWorkspaces: configManager.getSettings().projectWorkspaces || [],
|
|
2099
|
-
pinnedProjectWorkspaces: configManager.getSettings().pinnedProjectWorkspaces || [],
|
|
2100
|
-
};
|
|
2101
|
-
try {
|
|
2102
|
-
if (!requestedAsMain && result.projectWorkspace) {
|
|
2103
|
-
projectMembership = configManager.mountProjectWorkspace(result.projectWorkspace);
|
|
2104
|
-
}
|
|
2105
|
-
} catch (error) {
|
|
2106
|
-
let rollbackError = '';
|
|
2107
|
-
if (!result.reused) {
|
|
2108
|
-
try {
|
|
2109
|
-
const rollback = await agentManager.archiveAgent(result.agentId, {
|
|
2110
|
-
reason: 'project-mount-failed',
|
|
2111
|
-
recordHistory: false,
|
|
2112
|
-
requireEngineExit: true,
|
|
2113
|
-
scheduleProviderArchive: false,
|
|
2114
|
-
});
|
|
2115
|
-
if (rollback?.error) rollbackError = rollback.error;
|
|
2116
|
-
} catch (cleanupError) {
|
|
2117
|
-
rollbackError = cleanupError?.message || String(cleanupError);
|
|
2118
|
-
}
|
|
2119
|
-
}
|
|
2120
|
-
if (!mainPageSessionWasRemembered && !rollbackError) {
|
|
2121
|
-
forgetMainPageAgentSession(provider, rawSessionId, providerHomeId || 'default');
|
|
2122
|
-
}
|
|
2123
|
-
broadcastState();
|
|
2124
|
-
const mountError = error.message || 'Failed to create Project';
|
|
2125
|
-
res.status(500).json({
|
|
2126
|
-
error: rollbackError
|
|
2127
|
-
? `${mountError}. Rollback failed: ${rollbackError}`
|
|
2128
|
-
: mountError,
|
|
2129
|
-
...(rollbackError ? { rollbackError } : {}),
|
|
2130
|
-
});
|
|
2131
|
-
return;
|
|
2132
|
-
}
|
|
2133
|
-
broadcastState();
|
|
2134
|
-
if (result.reused) {
|
|
2135
|
-
res.status(200).json({
|
|
2136
|
-
agentId: result.agentId,
|
|
2137
|
-
...(result.projectWorkspace ? { projectWorkspace: result.projectWorkspace } : {}),
|
|
2138
|
-
projectWorkspaces: projectMembership.projectWorkspaces,
|
|
2139
|
-
pinnedProjectWorkspaces: projectMembership.pinnedProjectWorkspaces,
|
|
2140
|
-
reused: true,
|
|
2141
|
-
...(result.claimed ? { claimed: true } : {}),
|
|
2142
|
-
...(result.pending ? { pending: true } : {}),
|
|
2143
|
-
});
|
|
2144
|
-
return;
|
|
2145
|
-
}
|
|
2146
|
-
|
|
2147
|
-
res.status(201).json({
|
|
2148
|
-
agentId: result.agentId,
|
|
2149
|
-
...(result.projectWorkspace ? { projectWorkspace: result.projectWorkspace } : {}),
|
|
2150
|
-
projectWorkspaces: projectMembership.projectWorkspaces,
|
|
2151
|
-
pinnedProjectWorkspaces: projectMembership.pinnedProjectWorkspaces,
|
|
2152
|
-
});
|
|
2153
|
-
}
|
|
2154
|
-
|
|
2155
|
-
async function autoResumeMainPageAgentSessions() {
|
|
2156
|
-
if (typeof agentManager.whenRecovered === 'function') {
|
|
2157
|
-
await agentManager.whenRecovered();
|
|
2158
|
-
}
|
|
2159
|
-
|
|
2160
|
-
const sessions = mainPageAgentSessionsToAutoResume(configManager.getSettings());
|
|
2161
|
-
if (sessions.length === 0) return;
|
|
2162
|
-
|
|
2163
|
-
let knownSessions;
|
|
2164
|
-
try {
|
|
2165
|
-
knownSessions = await agentSessionsCache.get(
|
|
2166
|
-
JSON.stringify(configManager.getSettings().agentHomes || {}),
|
|
2167
|
-
{ maxAgeMs: INTERACTIVE_REFRESH_CACHE_MAX_AGE_MS }
|
|
2168
|
-
);
|
|
2169
|
-
} catch (error) {
|
|
2170
|
-
console.warn('Failed to load Agent session catalog for auto-resume:', error && (error.message || error));
|
|
2171
|
-
return;
|
|
2172
|
-
}
|
|
2173
|
-
const knownSessionByKey = new Map(knownSessions.map(session => [
|
|
2174
|
-
mainPageAgentSessionKey(session.provider, session.id, session.providerHomeId || 'default'),
|
|
2175
|
-
session,
|
|
2176
|
-
]));
|
|
2177
|
-
|
|
2178
|
-
let resumedCount = 0;
|
|
2179
|
-
for (const session of sessions) {
|
|
2180
|
-
try {
|
|
2181
|
-
const sessionDetails = knownSessionByKey.get(mainPageAgentSessionKey(
|
|
2182
|
-
session.provider,
|
|
2183
|
-
session.sessionId,
|
|
2184
|
-
session.providerHomeId || 'default'
|
|
2185
|
-
));
|
|
2186
|
-
if (!sessionDetails) {
|
|
2187
|
-
console.warn('Dropping stale main-page session from auto-resume:', session.provider, session.sessionId);
|
|
2188
|
-
forgetMainPageAgentSession(session.provider, session.sessionId, session.providerHomeId || 'default');
|
|
2189
|
-
continue;
|
|
2190
|
-
}
|
|
2191
|
-
|
|
2192
|
-
const claimingAgent = findActiveAgentClaimingSession(agentManager.getState().agents, session.provider, {
|
|
2193
|
-
id: session.sessionId,
|
|
2194
|
-
...(sessionDetails || {}),
|
|
2195
|
-
});
|
|
2196
|
-
if (claimingAgent) {
|
|
2197
|
-
continue;
|
|
2198
|
-
}
|
|
2199
|
-
|
|
2200
|
-
const result = await resumeAgentSessionById(session.provider, session.sessionId, {
|
|
2201
|
-
rememberMainPageSession: false,
|
|
2202
|
-
providerHomeId: session.providerHomeId || 'default',
|
|
2203
|
-
autoReadInitialAttention: true,
|
|
2204
|
-
});
|
|
2205
|
-
if (result.error) {
|
|
2206
|
-
const message = String(result.error || '').toLowerCase();
|
|
2207
|
-
if (session.provider === 'qoder' && message.includes('invalid session identifier')) {
|
|
2208
|
-
console.warn('Dropping stale qoder session from auto-resume:', session.provider, session.sessionId, result.error);
|
|
2209
|
-
forgetMainPageAgentSession(session.provider, session.sessionId, session.providerHomeId || 'default');
|
|
2210
|
-
continue;
|
|
2211
|
-
}
|
|
2212
|
-
console.warn('Failed to auto-resume main page agent session:', session.provider, session.sessionId, result.error);
|
|
2213
|
-
} else {
|
|
2214
|
-
resumedCount += 1;
|
|
2215
|
-
}
|
|
2216
|
-
} catch (error) {
|
|
2217
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
2218
|
-
console.warn('Failed to auto-resume main page agent session:', session.provider, session.sessionId, message);
|
|
2219
|
-
}
|
|
2220
|
-
}
|
|
2221
|
-
|
|
2222
|
-
if (resumedCount > 0) {
|
|
2223
|
-
broadcastState();
|
|
2224
|
-
}
|
|
2225
|
-
}
|
|
2226
|
-
|
|
2227
|
-
app.post(routePath(BASE_PATH, '/api/settings'), express.json(), async (req, res) => {
|
|
2228
|
-
const settingsPatch = { ...(req.body || {}) };
|
|
2229
|
-
delete settingsPatch.mainPageSessionKeys;
|
|
2230
|
-
delete settingsPatch.projectWorkspaces;
|
|
2231
|
-
delete settingsPatch.pinnedProjectWorkspaces;
|
|
2232
|
-
const browserConfigurationKeys = [
|
|
2233
|
-
'browserSource',
|
|
2234
|
-
'browserExecutablePath',
|
|
2235
|
-
'browserExternalCdpUrl',
|
|
2236
|
-
];
|
|
2237
|
-
const changesBrowserExtension = Object.prototype.hasOwnProperty.call(settingsPatch, 'browserExtensionEnabled');
|
|
2238
|
-
const changesBrowserConfiguration = browserConfigurationKeys.some(key =>
|
|
2239
|
-
Object.prototype.hasOwnProperty.call(settingsPatch, key)
|
|
2240
|
-
);
|
|
2241
|
-
const currentSettings = configManager.getSettings();
|
|
2242
|
-
const browserExtensionEnabled = settingsPatch.browserExtensionEnabled === true;
|
|
2243
|
-
const desiredBrowserEnabled = changesBrowserExtension
|
|
2244
|
-
? browserExtensionEnabled
|
|
2245
|
-
: currentSettings.browserExtensionEnabled === true;
|
|
2246
|
-
if ((changesBrowserExtension && browserExtensionEnabled) || changesBrowserConfiguration) {
|
|
2247
|
-
const desiredSelection = browserResourceManager.browserSelection({
|
|
2248
|
-
...currentSettings,
|
|
2249
|
-
...settingsPatch,
|
|
2250
|
-
});
|
|
2251
|
-
const probe = await browserResourceManager.probeCapability(desiredSelection);
|
|
2252
|
-
if (
|
|
2253
|
-
(changesBrowserConfiguration || desiredBrowserEnabled)
|
|
2254
|
-
&& (!probe.runtimeCapability || probe.runtimeCapability.error)
|
|
2255
|
-
) {
|
|
2256
|
-
res.status(400).json({
|
|
2257
|
-
error: probe.runtimeCapability?.error
|
|
2258
|
-
|| 'Install Farming-managed Chromium, choose a system Chromium browser, or configure a loopback external CDP endpoint',
|
|
2259
|
-
code: 'BROWSER_EXECUTABLE_NOT_FOUND',
|
|
2260
|
-
});
|
|
2261
|
-
return;
|
|
2262
|
-
}
|
|
2263
|
-
}
|
|
2264
|
-
if (
|
|
2265
|
-
currentSettings.browserExtensionEnabled === true
|
|
2266
|
-
&& (
|
|
2267
|
-
(changesBrowserExtension && !browserExtensionEnabled)
|
|
2268
|
-
|| changesBrowserConfiguration
|
|
2269
|
-
)
|
|
2270
|
-
) {
|
|
2271
|
-
try {
|
|
2272
|
-
await browserResourceManager.stopAll();
|
|
2273
|
-
} catch (error) {
|
|
2274
|
-
res.status(Number(error?.status) || 500).json({
|
|
2275
|
-
error: error?.message || 'Browser extension could not be disabled',
|
|
2276
|
-
code: error?.code || 'BROWSER_DISABLE_FAILED',
|
|
2277
|
-
});
|
|
2278
|
-
return;
|
|
2279
|
-
}
|
|
2280
|
-
}
|
|
2281
|
-
configManager.updateSettings(settingsPatch);
|
|
2282
|
-
if (changesBrowserExtension || changesBrowserConfiguration) {
|
|
2283
|
-
await browserResourceManager.refreshCapability();
|
|
2284
|
-
}
|
|
2285
|
-
agentSessionsCache.invalidate();
|
|
2286
|
-
res.json({
|
|
2287
|
-
success: true,
|
|
2288
|
-
settings: configManager.getSettings()
|
|
2289
|
-
});
|
|
2290
|
-
scheduleBroadcastState();
|
|
2291
|
-
});
|
|
2292
|
-
|
|
2293
|
-
app.post(routePath(BASE_PATH, '/api/themes/:themeId/set'), express.json(), (req, res) => {
|
|
2294
|
-
const theme = themeManager.getTheme(req.params.themeId);
|
|
2295
|
-
if (!theme) {
|
|
2296
|
-
res.status(404).json({ error: 'Theme not found' });
|
|
2297
|
-
return;
|
|
2298
|
-
}
|
|
2299
|
-
|
|
2300
|
-
configManager.updateSettings({ theme: req.params.themeId });
|
|
2301
|
-
res.json({ success: true, theme: req.params.themeId });
|
|
2302
|
-
});
|
|
2303
|
-
|
|
2304
|
-
app.get(routePath(BASE_PATH, '/api/themes/:themeId/settings'), (req, res) => {
|
|
2305
|
-
const theme = themeManager.getTheme(req.params.themeId);
|
|
2306
|
-
if (!theme) {
|
|
2307
|
-
res.status(404).json({ error: 'Theme not found' });
|
|
2308
|
-
return;
|
|
2309
|
-
}
|
|
2310
|
-
|
|
2311
|
-
const settings = themeManager.getThemeSettings(req.params.themeId);
|
|
2312
|
-
res.json({ settings });
|
|
2313
|
-
});
|
|
2314
|
-
|
|
2315
|
-
app.post(routePath(BASE_PATH, '/api/themes/:themeId/settings'), express.json(), (req, res) => {
|
|
2316
|
-
const theme = themeManager.getTheme(req.params.themeId);
|
|
2317
|
-
if (!theme) {
|
|
2318
|
-
res.status(404).json({ error: 'Theme not found' });
|
|
2319
|
-
return;
|
|
2320
|
-
}
|
|
2321
|
-
|
|
2322
|
-
const success = themeManager.updateThemeSettings(req.params.themeId, req.body);
|
|
2323
|
-
if (success) {
|
|
2324
|
-
res.json({ success: true, settings: themeManager.getThemeSettings(req.params.themeId) });
|
|
2325
|
-
} else {
|
|
2326
|
-
res.status(500).json({ error: 'Failed to update theme settings' });
|
|
2327
|
-
}
|
|
2328
|
-
});
|
|
2329
|
-
|
|
2330
|
-
app.get(routePath(BASE_PATH, '/api/themes/:themeId'), (req, res) => {
|
|
2331
|
-
const theme = themeManager.getTheme(req.params.themeId);
|
|
2332
|
-
if (!theme) {
|
|
2333
|
-
res.status(404).json({ error: 'Theme not found' });
|
|
2334
|
-
return;
|
|
2335
|
-
}
|
|
2336
|
-
|
|
2337
|
-
const css = themeManager.getThemeCSS(req.params.themeId);
|
|
2338
|
-
res.json({
|
|
2339
|
-
theme,
|
|
2340
|
-
css
|
|
2341
|
-
});
|
|
2342
|
-
});
|
|
2343
|
-
|
|
2344
|
-
wss.on('connection', (ws, req) => {
|
|
2345
|
-
const url = new URL(req.url || '/', `http://${req.headers.host || 'localhost'}`);
|
|
2346
|
-
const viewerPrefix = routePath(BASE_PATH, '/api/browsers/');
|
|
2347
|
-
if (url.pathname.startsWith(viewerPrefix) && url.pathname.endsWith('/viewer')) {
|
|
2348
|
-
if (authEnabled && !tokenAuth.verifyWebSocket(req)) {
|
|
2349
|
-
ws.close(4001, 'Authentication required');
|
|
2350
|
-
return;
|
|
2351
|
-
}
|
|
2352
|
-
const encodedId = url.pathname.slice(viewerPrefix.length, -'/viewer'.length);
|
|
2353
|
-
try {
|
|
2354
|
-
browserResourceManager.attachViewer(decodeURIComponent(encodedId), ws);
|
|
2355
|
-
} catch (error) {
|
|
2356
|
-
ws.close(4004, error?.message || 'Browser resource not found');
|
|
2357
|
-
}
|
|
2358
|
-
return;
|
|
2359
|
-
}
|
|
2360
|
-
if (url.pathname !== WS_PATH) {
|
|
2361
|
-
ws.close(1008, 'Invalid path');
|
|
2362
|
-
return;
|
|
2363
|
-
}
|
|
2364
|
-
|
|
2365
|
-
// Verify token for WebSocket connections when auth is enabled.
|
|
2366
|
-
if (authEnabled && !tokenAuth.verifyWebSocket(req)) {
|
|
2367
|
-
ws.close(4001, 'Authentication required');
|
|
2368
|
-
return;
|
|
2369
|
-
}
|
|
2370
|
-
|
|
2371
|
-
console.log('Client connected');
|
|
2372
|
-
ws.connectionId = crypto.randomUUID();
|
|
2373
|
-
|
|
2374
|
-
ws.on('message', (message) => {
|
|
2375
|
-
try {
|
|
2376
|
-
const data = JSON.parse(message);
|
|
2377
|
-
const validation = validateClientMessage(data);
|
|
2378
|
-
if (!validation.ok) {
|
|
2379
|
-
ws.send(JSON.stringify({
|
|
2380
|
-
type: 'protocol-error',
|
|
2381
|
-
protocolVersion: PROTOCOL_VERSION,
|
|
2382
|
-
requestId: typeof data?.requestId === 'string' ? data.requestId : '',
|
|
2383
|
-
message: validation.error,
|
|
2384
|
-
}));
|
|
2385
|
-
return;
|
|
2386
|
-
}
|
|
2387
|
-
handleMessage(ws, data);
|
|
2388
|
-
} catch (e) {
|
|
2389
|
-
console.error('Failed to parse message:', e);
|
|
2390
|
-
ws.send(JSON.stringify({
|
|
2391
|
-
type: 'protocol-error',
|
|
2392
|
-
protocolVersion: PROTOCOL_VERSION,
|
|
2393
|
-
requestId: '',
|
|
2394
|
-
message: 'message must be valid JSON',
|
|
2395
|
-
}));
|
|
2396
|
-
}
|
|
2397
|
-
});
|
|
2398
|
-
|
|
2399
|
-
ws.on('close', () => {
|
|
2400
|
-
clearWorkspaceFileWatch(ws);
|
|
2401
|
-
console.log('Client disconnected');
|
|
2402
|
-
});
|
|
2403
|
-
|
|
2404
|
-
ws.send(JSON.stringify({
|
|
2405
|
-
type: 'protocol-hello',
|
|
2406
|
-
protocolVersion: PROTOCOL_VERSION,
|
|
2407
|
-
minProtocolVersion: MIN_PROTOCOL_VERSION,
|
|
2408
|
-
}));
|
|
2409
|
-
sendState(ws);
|
|
2410
|
-
});
|
|
2411
|
-
|
|
2412
|
-
const MAIN_AGENT_RESTART_COMMANDS = new Set(['codex', 'claude', 'opencode', 'qoder', 'bash', 'zsh']);
|
|
2413
|
-
|
|
2414
|
-
function normalizeMainAgentRestartCommand(command) {
|
|
2415
|
-
const normalized = String(command || '').trim();
|
|
2416
|
-
return MAIN_AGENT_RESTART_COMMANDS.has(normalized) ? normalized : '';
|
|
2417
|
-
}
|
|
2418
|
-
|
|
2419
|
-
function restartMainAgent(ws, command) {
|
|
2420
|
-
const normalizedCommand = normalizeMainAgentRestartCommand(command);
|
|
2421
|
-
if (!normalizedCommand) {
|
|
2422
|
-
ws.send(JSON.stringify({ type: 'error', message: 'Unsupported Main Agent restart command' }));
|
|
2423
|
-
return;
|
|
2424
|
-
}
|
|
2425
|
-
|
|
2426
|
-
void (async () => {
|
|
2427
|
-
try {
|
|
2428
|
-
const state = agentManager.getState();
|
|
2429
|
-
const currentMain = state.agents.find(agent => (
|
|
2430
|
-
agent.id === state.mainAgentId || agent.isMain === true
|
|
2431
|
-
));
|
|
2432
|
-
if (currentMain) {
|
|
2433
|
-
const killed = await agentManager.killAgent(currentMain.id);
|
|
2434
|
-
if (killed?.error) {
|
|
2435
|
-
ws.send(JSON.stringify({ type: 'error', message: killed.error }));
|
|
2436
|
-
return;
|
|
2437
|
-
}
|
|
2438
|
-
}
|
|
2439
|
-
|
|
2440
|
-
await agentManager.startAgent(normalizedCommand, null, (agentId, error) => {
|
|
2441
|
-
if (error) {
|
|
2442
|
-
ws.send(JSON.stringify({ type: 'error', message: error }));
|
|
2443
|
-
} else if (agentId) {
|
|
2444
|
-
ws.agentId = agentId;
|
|
2445
|
-
broadcastState();
|
|
2446
|
-
ws.send(JSON.stringify({ type: 'agent-started', agentId }));
|
|
2447
|
-
}
|
|
2448
|
-
}, {
|
|
2449
|
-
wantsMain: true
|
|
2450
|
-
});
|
|
2451
|
-
} catch (error) {
|
|
2452
|
-
const message = error instanceof Error ? error.message : 'Failed to restart Main Agent';
|
|
2453
|
-
ws.send(JSON.stringify({ type: 'error', message }));
|
|
2454
|
-
broadcastState();
|
|
2455
|
-
}
|
|
2456
|
-
})();
|
|
2457
|
-
}
|
|
2458
|
-
|
|
2459
|
-
async function killAgentFromMessage(ws, agentId, options = {}) {
|
|
2460
|
-
try {
|
|
2461
|
-
const requested = await agentManager.requestKillAgent(agentId, {
|
|
2462
|
-
acknowledgeUnprovenAcpExit: options.acknowledgeUnprovenAcpExit === true,
|
|
2463
|
-
});
|
|
2464
|
-
const result = requested.result;
|
|
2465
|
-
if (result?.error) {
|
|
2466
|
-
ws.send(JSON.stringify({ type: 'error', message: result.error }));
|
|
2467
|
-
} else if (result?.accepted) {
|
|
2468
|
-
ws.send(JSON.stringify({ type: 'agent-delete-accepted', ...result }));
|
|
2469
|
-
void requested.completion.then(completed => {
|
|
2470
|
-
if (completed?.error && ws.readyState === WebSocket.OPEN) {
|
|
2471
|
-
ws.send(JSON.stringify({ type: 'error', message: completed.error }));
|
|
2472
|
-
}
|
|
2473
|
-
}).catch(error => {
|
|
2474
|
-
if (ws.readyState === WebSocket.OPEN) {
|
|
2475
|
-
ws.send(JSON.stringify({
|
|
2476
|
-
type: 'error',
|
|
2477
|
-
message: error instanceof Error ? error.message : 'Failed to stop Agent',
|
|
2478
|
-
}));
|
|
2479
|
-
}
|
|
2480
|
-
}).finally(() => broadcastState());
|
|
2481
|
-
}
|
|
2482
|
-
} catch (error) {
|
|
2483
|
-
ws.send(JSON.stringify({
|
|
2484
|
-
type: 'error',
|
|
2485
|
-
message: error instanceof Error ? error.message : 'Failed to stop Agent',
|
|
2486
|
-
}));
|
|
2487
|
-
} finally {
|
|
2488
|
-
broadcastState();
|
|
2489
|
-
}
|
|
2490
|
-
}
|
|
2491
|
-
|
|
2492
|
-
async function sendInputMessage(ws, data) {
|
|
2493
|
-
const targetAgentId = resolveInputTargetAgentId(ws, data);
|
|
2494
|
-
if (!targetAgentId) return;
|
|
2495
|
-
|
|
2496
|
-
const inputParts = inputPartsFromMessage(data);
|
|
2497
|
-
if (inputParts.length === 0) return;
|
|
2498
|
-
await agentManager.sendInput(targetAgentId, inputParts);
|
|
2499
|
-
}
|
|
2500
|
-
|
|
2501
|
-
async function sendComposerInputMessage(ws, data) {
|
|
2502
|
-
const targetAgentId = resolveInputTargetAgentId(ws, data);
|
|
2503
|
-
const requestId = typeof data.requestId === 'string' ? data.requestId.trim() : '';
|
|
2504
|
-
const responseAgentId = targetAgentId || (typeof data.agentId === 'string' ? data.agentId : '');
|
|
2505
|
-
const respond = (accepted, message = '', uncertain = false) => {
|
|
2506
|
-
if (!requestId || !responseAgentId) return;
|
|
2507
|
-
ws.send(JSON.stringify({
|
|
2508
|
-
type: 'composer-input-result',
|
|
2509
|
-
requestId,
|
|
2510
|
-
agentId: responseAgentId,
|
|
2511
|
-
accepted,
|
|
2512
|
-
...(message ? { message } : {}),
|
|
2513
|
-
...(uncertain ? { uncertain: true } : {}),
|
|
2514
|
-
}));
|
|
2515
|
-
};
|
|
2516
|
-
const message = typeof data.message === 'string' ? data.message : '';
|
|
2517
|
-
if (!/^[A-Za-z0-9._:-]{1,160}$/.test(requestId)) {
|
|
2518
|
-
if (responseAgentId) {
|
|
2519
|
-
ws.send(JSON.stringify({
|
|
2520
|
-
type: 'composer-input-result',
|
|
2521
|
-
requestId: requestId || 'invalid-request',
|
|
2522
|
-
agentId: responseAgentId,
|
|
2523
|
-
accepted: false,
|
|
2524
|
-
message: 'Structured Composer input requires a valid requestId',
|
|
2525
|
-
}));
|
|
2526
|
-
}
|
|
2527
|
-
return;
|
|
2528
|
-
}
|
|
2529
|
-
const content = [];
|
|
2530
|
-
if (message.trim()) content.push({ type: 'text', text: message });
|
|
2531
|
-
const attachmentsRoot = path.resolve(imageAttachmentsDir());
|
|
2532
|
-
const attachments = Array.isArray(data.attachments) ? data.attachments.slice(0, 8) : [];
|
|
2533
|
-
for (const attachment of attachments) {
|
|
2534
|
-
if (!['image', 'audio'].includes(attachment?.kind) || typeof attachment.path !== 'string') continue;
|
|
2535
|
-
const filePath = path.resolve(attachment.path);
|
|
2536
|
-
if (!filePath.startsWith(`${attachmentsRoot}${path.sep}`)) continue;
|
|
2537
|
-
const mimeType = typeof attachment.type === 'string' && attachment.kind === 'image' && /^image\/(?:png|jpe?g|gif|webp)$/i.test(attachment.type)
|
|
2538
|
-
? attachment.type.toLowerCase()
|
|
2539
|
-
: (typeof attachment.type === 'string' && attachment.kind === 'audio' && Object.hasOwn(AUDIO_ATTACHMENT_EXTENSIONS, attachment.type.toLowerCase())
|
|
2540
|
-
? attachment.type.toLowerCase()
|
|
2541
|
-
: '');
|
|
2542
|
-
if (!mimeType) continue;
|
|
2543
|
-
try {
|
|
2544
|
-
const data = await fs.promises.readFile(filePath);
|
|
2545
|
-
if (data.length === 0 || data.length > 12 * 1024 * 1024) continue;
|
|
2546
|
-
content.push({
|
|
2547
|
-
type: attachment.kind,
|
|
2548
|
-
data: data.toString('base64'),
|
|
2549
|
-
mimeType,
|
|
2550
|
-
path: filePath,
|
|
2551
|
-
uri: pathToFileURL(filePath).href,
|
|
2552
|
-
});
|
|
2553
|
-
} catch {
|
|
2554
|
-
// The text fallback already explains failed or unavailable uploads.
|
|
2555
|
-
}
|
|
2556
|
-
}
|
|
2557
|
-
if (!targetAgentId || content.length === 0) {
|
|
2558
|
-
respond(false, 'Composer message is empty');
|
|
2559
|
-
return;
|
|
2560
|
-
}
|
|
2561
|
-
const targetAgent = agentManager.getState().agents.find(agent => agent.id === targetAgentId);
|
|
2562
|
-
const structuredRuntime = targetAgent && runtimeKind(targetAgent) !== 'terminal';
|
|
2563
|
-
if (!structuredRuntime) {
|
|
2564
|
-
const error = 'Terminal Composer input requires the active terminal owner';
|
|
2565
|
-
if (requestId) respond(false, error);
|
|
2566
|
-
else ws.send(JSON.stringify({ type: 'error', message: error }));
|
|
2567
|
-
return;
|
|
2568
|
-
}
|
|
2569
|
-
try {
|
|
2570
|
-
await agentManager.sendComposerMessage(targetAgentId, content, { requestId });
|
|
2571
|
-
respond(true);
|
|
2572
|
-
} catch (error) {
|
|
2573
|
-
const message = error && error.message ? error.message : 'Failed to send Composer message';
|
|
2574
|
-
if (requestId) respond(false, message, error?.uncertain === true);
|
|
2575
|
-
else ws.send(JSON.stringify({ type: 'error', message }));
|
|
2576
|
-
}
|
|
2577
|
-
}
|
|
2578
|
-
|
|
2579
|
-
function handleMessage(ws, data) {
|
|
2580
|
-
switch (data.type) {
|
|
2581
|
-
case 'protocol-hello':
|
|
2582
|
-
if (!protocolCompatible(data.protocolVersion)) {
|
|
2583
|
-
ws.close(4002, `Unsupported Farming protocol version ${data.protocolVersion}`);
|
|
2584
|
-
return;
|
|
2585
|
-
}
|
|
2586
|
-
ws.protocolVersion = data.protocolVersion;
|
|
2587
|
-
break;
|
|
2588
|
-
case 'start-agent': {
|
|
2589
|
-
const workspace = data.workspace || null;
|
|
2590
|
-
void (async () => {
|
|
2591
|
-
const projectWorkspace = data.asMain === true
|
|
2592
|
-
? ''
|
|
2593
|
-
: await canonicalProjectWorkspace(
|
|
2594
|
-
typeof data.projectWorkspace === 'string' && data.projectWorkspace.trim()
|
|
2595
|
-
? data.projectWorkspace
|
|
2596
|
-
: workspace
|
|
2597
|
-
);
|
|
2598
|
-
agentManager.startAgent(data.command, workspace, (agentId, error) => {
|
|
2599
|
-
if (error) {
|
|
2600
|
-
ws.send(JSON.stringify({ type: 'error', message: error }));
|
|
2601
|
-
} else if (agentId) {
|
|
2602
|
-
void (async () => {
|
|
2603
|
-
try {
|
|
2604
|
-
if (projectWorkspace) configManager.mountProjectWorkspace(projectWorkspace);
|
|
2605
|
-
} catch (mountError) {
|
|
2606
|
-
let rollbackError = '';
|
|
2607
|
-
try {
|
|
2608
|
-
const rollback = await agentManager.archiveAgent(agentId, {
|
|
2609
|
-
reason: 'project-mount-failed',
|
|
2610
|
-
recordHistory: false,
|
|
2611
|
-
requireEngineExit: true,
|
|
2612
|
-
scheduleProviderArchive: false,
|
|
2613
|
-
});
|
|
2614
|
-
if (rollback?.error) rollbackError = rollback.error;
|
|
2615
|
-
} catch (cleanupError) {
|
|
2616
|
-
rollbackError = cleanupError?.message || String(cleanupError);
|
|
2617
|
-
}
|
|
2618
|
-
broadcastState();
|
|
2619
|
-
const errorMessage = mountError.message || 'Failed to create Project';
|
|
2620
|
-
ws.send(JSON.stringify({
|
|
2621
|
-
type: 'error',
|
|
2622
|
-
message: rollbackError
|
|
2623
|
-
? `${errorMessage}. Rollback failed: ${rollbackError}`
|
|
2624
|
-
: errorMessage,
|
|
2625
|
-
}));
|
|
2626
|
-
return;
|
|
2627
|
-
}
|
|
2628
|
-
ws.agentId = agentId;
|
|
2629
|
-
broadcastState();
|
|
2630
|
-
ws.send(JSON.stringify({ type: 'agent-started', agentId }));
|
|
2631
|
-
})().catch(callbackError => {
|
|
2632
|
-
console.warn('Failed to finish started Agent transition:', agentId, callbackError?.message || callbackError);
|
|
2633
|
-
});
|
|
2634
|
-
}
|
|
2635
|
-
}, {
|
|
2636
|
-
wantsMain: data.asMain === true,
|
|
2637
|
-
projectWorkspace,
|
|
2638
|
-
task: typeof data.task === 'string' ? data.task : '',
|
|
2639
|
-
workflowTemplate: typeof data.workflowTemplate === 'string' ? data.workflowTemplate : '',
|
|
2640
|
-
customTitle: typeof data.customTitle === 'string' ? data.customTitle : '',
|
|
2641
|
-
createRequestId: typeof data.requestId === 'string' ? data.requestId : '',
|
|
2642
|
-
codexApprovalMode: typeof data.codexApprovalMode === 'string' ? data.codexApprovalMode : undefined,
|
|
2643
|
-
agentRuntimeMode: ['json', 'acp', 'chat'].includes(data.agentRuntimeMode) ? data.agentRuntimeMode : 'terminal',
|
|
2644
|
-
acpHistoryMode: data.acpHistoryMode === 'resume' ? 'resume' : 'load',
|
|
2645
|
-
providerHomeId: typeof data.providerHomeId === 'string' ? data.providerHomeId : '',
|
|
2646
|
-
...(Array.isArray(data.additionalDirectories) ? { additionalDirectories: data.additionalDirectories } : {}),
|
|
2647
|
-
...(Array.isArray(data.mcpServers) ? { mcpServers: data.mcpServers } : {}),
|
|
2648
|
-
...(data.dangerouslySkipPermissions === true ? { dangerouslySkipPermissions: true } : {}),
|
|
2649
|
-
});
|
|
2650
|
-
})().catch(error => {
|
|
2651
|
-
ws.send(JSON.stringify({
|
|
2652
|
-
type: 'error',
|
|
2653
|
-
message: error.message || 'Failed to resolve Project workspace',
|
|
2654
|
-
}));
|
|
2655
|
-
});
|
|
2656
|
-
break;
|
|
2657
|
-
}
|
|
2658
|
-
case 'input':
|
|
2659
|
-
{
|
|
2660
|
-
void sendInputMessage(ws, data);
|
|
2661
|
-
}
|
|
2662
|
-
break;
|
|
2663
|
-
|
|
2664
|
-
case 'composer-input':
|
|
2665
|
-
{
|
|
2666
|
-
void sendComposerInputMessage(ws, data);
|
|
2667
|
-
}
|
|
2668
|
-
break;
|
|
2669
|
-
|
|
2670
|
-
case 'acp-permission-response':
|
|
2671
|
-
try {
|
|
2672
|
-
agentManager.respondToAcpPermission(
|
|
2673
|
-
data.agentId,
|
|
2674
|
-
data.requestId,
|
|
2675
|
-
data.optionId,
|
|
2676
|
-
data.cancelled === true
|
|
2677
|
-
);
|
|
2678
|
-
} catch (error) {
|
|
2679
|
-
ws.send(JSON.stringify({
|
|
2680
|
-
type: 'error',
|
|
2681
|
-
message: error && error.message ? error.message : 'Failed to respond to ACP permission',
|
|
2682
|
-
}));
|
|
2683
|
-
}
|
|
2684
|
-
break;
|
|
2685
|
-
|
|
2686
|
-
case 'interrupt-agent':
|
|
2687
|
-
if (data.agentId) {
|
|
2688
|
-
void agentManager.interruptAgent(data.agentId);
|
|
2689
|
-
}
|
|
2690
|
-
break;
|
|
2691
|
-
|
|
2692
|
-
case 'focus-agent':
|
|
2693
|
-
ws.focusedAgentId = data.agentId;
|
|
2694
|
-
if (data.streamScope === 'focused' || data.streamScope === 'all') {
|
|
2695
|
-
ws.streamScope = data.streamScope;
|
|
2696
|
-
}
|
|
2697
|
-
if (data.previewScope === 'none' || data.previewScope === 'focused' || data.previewScope === 'all') {
|
|
2698
|
-
ws.previewScope = data.previewScope;
|
|
2699
|
-
}
|
|
2700
|
-
if (data.refreshState === true) {
|
|
2701
|
-
sendState(ws);
|
|
2702
|
-
}
|
|
2703
|
-
break;
|
|
2704
|
-
|
|
2705
|
-
case 'resize-agent':
|
|
2706
|
-
if (data.agentId && Number.isFinite(data.cols) && Number.isFinite(data.rows)) {
|
|
2707
|
-
agentManager.requestAgentSessionResize(data.agentId, data.cols, data.rows);
|
|
2708
|
-
}
|
|
2709
|
-
break;
|
|
2710
|
-
|
|
2711
|
-
case 'clear-terminal':
|
|
2712
|
-
if (data.agentId) void agentManager.clearAgentSessionBuffer(data.agentId);
|
|
2713
|
-
break;
|
|
2714
|
-
|
|
2715
|
-
case 'watch-workspace-files':
|
|
2716
|
-
watchWorkspaceFiles(ws, data);
|
|
2717
|
-
break;
|
|
2718
|
-
|
|
2719
|
-
case 'unwatch-workspace-files':
|
|
2720
|
-
clearWorkspaceFileWatch(ws, data.agentId);
|
|
2721
|
-
break;
|
|
2722
|
-
|
|
2723
|
-
case 'kill-agent':
|
|
2724
|
-
void killAgentFromMessage(ws, data.agentId, {
|
|
2725
|
-
acknowledgeUnprovenAcpExit: data.acknowledgeUnprovenAcpExit === true,
|
|
2726
|
-
});
|
|
2727
|
-
break;
|
|
2728
|
-
|
|
2729
|
-
case 'restart-main-agent':
|
|
2730
|
-
restartMainAgent(ws, data.command);
|
|
2731
|
-
break;
|
|
2732
|
-
|
|
2733
|
-
default:
|
|
2734
|
-
console.log('Unknown message type:', data.type);
|
|
2735
|
-
}
|
|
2736
|
-
}
|
|
2737
|
-
|
|
2738
|
-
const { resolveInputTargetAgentId } = require('./input-routing');
|
|
2739
|
-
|
|
2740
|
-
function sendWorkspaceFileWatchError(ws, error) {
|
|
2741
|
-
if (ws.readyState !== WebSocket.OPEN) return;
|
|
2742
|
-
const message = error instanceof WorkspaceFileError ? error.message : 'failed to watch workspace files';
|
|
2743
|
-
ws.send(JSON.stringify({ type: 'error', message }));
|
|
2744
|
-
}
|
|
2745
|
-
|
|
2746
|
-
function isCurrentWorkspaceFileWatch(ws, agentId, record) {
|
|
2747
|
-
return Boolean(
|
|
2748
|
-
!record.cancelled
|
|
2749
|
-
&& ws.workspaceFileUnsubscribes?.get(agentId) === record
|
|
2750
|
-
);
|
|
2751
|
-
}
|
|
2752
|
-
|
|
2753
|
-
function closeWorkspaceFileWatchRecord(record) {
|
|
2754
|
-
record.cancelled = true;
|
|
2755
|
-
if (!record.unsubscribe) return;
|
|
2756
|
-
Promise.resolve(record.unsubscribe()).catch((error) => {
|
|
2757
|
-
console.error('Failed to clear workspace file watch:', error);
|
|
2758
|
-
});
|
|
2759
|
-
}
|
|
2760
|
-
|
|
2761
|
-
function clearWorkspaceFileWatch(ws, agentId = null) {
|
|
2762
|
-
const watches = ws.workspaceFileUnsubscribes;
|
|
2763
|
-
if (!watches) return;
|
|
2764
|
-
|
|
2765
|
-
const entries = agentId
|
|
2766
|
-
? [[agentId, watches.get(agentId)]]
|
|
2767
|
-
: Array.from(watches.entries());
|
|
2768
|
-
|
|
2769
|
-
entries.forEach(([watchedAgentId, record]) => {
|
|
2770
|
-
if (!record) return;
|
|
2771
|
-
if (watches.get(watchedAgentId) === record) watches.delete(watchedAgentId);
|
|
2772
|
-
closeWorkspaceFileWatchRecord(record);
|
|
2773
|
-
});
|
|
2774
|
-
|
|
2775
|
-
if (watches.size === 0) {
|
|
2776
|
-
ws.workspaceFileUnsubscribes = null;
|
|
2777
|
-
}
|
|
2778
|
-
}
|
|
2779
|
-
|
|
2780
|
-
async function watchWorkspaceFiles(ws, data) {
|
|
2781
|
-
try {
|
|
2782
|
-
if (!data.agentId) {
|
|
2783
|
-
throw new WorkspaceFileError('agentId is required', 400);
|
|
2784
|
-
}
|
|
2785
|
-
if (!ws.workspaceFileUnsubscribes) {
|
|
2786
|
-
ws.workspaceFileUnsubscribes = new Map();
|
|
2787
|
-
}
|
|
2788
|
-
const watches = ws.workspaceFileUnsubscribes;
|
|
2789
|
-
const existing = watches.get(data.agentId);
|
|
2790
|
-
if (existing) {
|
|
2791
|
-
const watching = await existing.ready;
|
|
2792
|
-
if (watching && isCurrentWorkspaceFileWatch(ws, data.agentId, existing) && ws.readyState === WebSocket.OPEN) {
|
|
2793
|
-
ws.send(JSON.stringify({
|
|
2794
|
-
type: 'workspace-file-watch',
|
|
2795
|
-
agentId: data.agentId,
|
|
2796
|
-
watching: true,
|
|
2797
|
-
}));
|
|
2798
|
-
}
|
|
2799
|
-
return;
|
|
2800
|
-
}
|
|
2801
|
-
|
|
2802
|
-
const root = resolveWorkspaceRoot(agentManager, data.agentId);
|
|
2803
|
-
const record = {
|
|
2804
|
-
cancelled: false,
|
|
2805
|
-
unsubscribe: null,
|
|
2806
|
-
ready: null,
|
|
2807
|
-
};
|
|
2808
|
-
record.ready = (async () => {
|
|
2809
|
-
const unsubscribe = await workspaceFileService.subscribe(root, (event) => {
|
|
2810
|
-
if (!isCurrentWorkspaceFileWatch(ws, data.agentId, record) || ws.readyState !== WebSocket.OPEN) return;
|
|
2811
|
-
ws.send(JSON.stringify({
|
|
2812
|
-
type: 'workspace-file-event',
|
|
2813
|
-
event: {
|
|
2814
|
-
agentId: data.agentId,
|
|
2815
|
-
...event,
|
|
2816
|
-
},
|
|
2817
|
-
}));
|
|
2818
|
-
});
|
|
2819
|
-
record.unsubscribe = unsubscribe;
|
|
2820
|
-
if (!isCurrentWorkspaceFileWatch(ws, data.agentId, record) || ws.readyState !== WebSocket.OPEN) {
|
|
2821
|
-
closeWorkspaceFileWatchRecord(record);
|
|
2822
|
-
return false;
|
|
2823
|
-
}
|
|
2824
|
-
return true;
|
|
2825
|
-
})();
|
|
2826
|
-
watches.set(data.agentId, record);
|
|
2827
|
-
try {
|
|
2828
|
-
const watching = await record.ready;
|
|
2829
|
-
if (watching && isCurrentWorkspaceFileWatch(ws, data.agentId, record) && ws.readyState === WebSocket.OPEN) {
|
|
2830
|
-
ws.send(JSON.stringify({
|
|
2831
|
-
type: 'workspace-file-watch',
|
|
2832
|
-
agentId: data.agentId,
|
|
2833
|
-
watching: true,
|
|
2834
|
-
}));
|
|
2835
|
-
}
|
|
2836
|
-
} catch (error) {
|
|
2837
|
-
if (watches.get(data.agentId) === record) watches.delete(data.agentId);
|
|
2838
|
-
closeWorkspaceFileWatchRecord(record);
|
|
2839
|
-
if (watches.size === 0 && ws.workspaceFileUnsubscribes === watches) {
|
|
2840
|
-
ws.workspaceFileUnsubscribes = null;
|
|
2841
|
-
}
|
|
2842
|
-
throw error;
|
|
2843
|
-
}
|
|
2844
|
-
} catch (error) {
|
|
2845
|
-
sendWorkspaceFileWatchError(ws, error);
|
|
2846
|
-
}
|
|
2847
|
-
}
|
|
2848
|
-
|
|
2849
|
-
function buildStatePayload() {
|
|
2850
|
-
const state = agentManager.getState();
|
|
2851
|
-
const settings = configManager.getSettings();
|
|
2852
|
-
return {
|
|
2853
|
-
...state,
|
|
2854
|
-
agents: state.agents.map(agent => ({
|
|
2855
|
-
...agent,
|
|
2856
|
-
workspaceRootId: rootIdForPath(agent.projectWorkspace || agent.gitWorktree?.workspace || agent.cwd),
|
|
2857
|
-
})),
|
|
2858
|
-
workspaceRoots: workspaceRootRegistry.list(),
|
|
2859
|
-
mainPageSessionKeys: typeof configManager.getMainPageSessionKeys === 'function'
|
|
2860
|
-
? configManager.getMainPageSessionKeys()
|
|
2861
|
-
: (Array.isArray(settings.mainPageSessionKeys) ? settings.mainPageSessionKeys : []),
|
|
2862
|
-
projectWorkspaces: settings.projectWorkspaces || [],
|
|
2863
|
-
pinnedProjectWorkspaces: settings.pinnedProjectWorkspaces || [],
|
|
2864
|
-
};
|
|
2865
|
-
}
|
|
2866
|
-
|
|
2867
|
-
function sendState(ws) {
|
|
2868
|
-
const state = buildStatePayload();
|
|
2869
|
-
ws.send(JSON.stringify({ type: 'state', state }));
|
|
2870
|
-
agentManager.getPreviewPayloads().forEach((preview) => {
|
|
2871
|
-
sendPreview(ws, preview);
|
|
2872
|
-
});
|
|
2873
|
-
}
|
|
2874
|
-
|
|
2875
|
-
function previewForClient(preview, client) {
|
|
2876
|
-
if (!preview || !client || client.focusedAgentId !== preview.agentId || !preview.previewSnapshot) {
|
|
2877
|
-
return preview;
|
|
2878
|
-
}
|
|
2879
|
-
// The active terminal is hydrated from its authoritative session-view and
|
|
2880
|
-
// receives live session-output. Sending its sidebar snapshot again adds a
|
|
2881
|
-
// large, unrelated React update to every keystroke in a full-screen TUI.
|
|
2882
|
-
return {
|
|
2883
|
-
...preview,
|
|
2884
|
-
previewSnapshot: null,
|
|
2885
|
-
};
|
|
2886
|
-
}
|
|
2887
|
-
|
|
2888
|
-
function sendPreview(ws, preview) {
|
|
2889
|
-
ws.send(JSON.stringify({
|
|
2890
|
-
type: 'session-preview',
|
|
2891
|
-
preview: previewForClient(preview, ws),
|
|
2892
|
-
}));
|
|
2893
|
-
}
|
|
2894
|
-
|
|
2895
|
-
const STATE_BROADCAST_INTERVAL_MS = 120;
|
|
2896
|
-
const PREVIEW_BROADCAST_INTERVAL_MS = 500;
|
|
2897
|
-
const SESSION_STREAM_BROADCAST_INTERVAL_MS = 33;
|
|
2898
|
-
const AGENT_ACTIVITY_BROADCAST_DELAY_MS = SESSION_STREAM_BROADCAST_INTERVAL_MS;
|
|
2899
|
-
const MAX_SESSION_STREAM_CLIENT_BUFFERED_AMOUNT = 4 * 1024 * 1024;
|
|
2900
|
-
let stateBroadcastTimer = null;
|
|
2901
|
-
let lastStateBroadcastAt = 0;
|
|
2902
|
-
const pendingPreviewBroadcasts = new Map();
|
|
2903
|
-
const pendingAgentActivityBroadcasts = new Map();
|
|
2904
|
-
const pendingAgentUpdates = new Map();
|
|
2905
|
-
const pendingSessionStreams = new Map();
|
|
2906
|
-
let sessionStreamBroadcastTimer = null;
|
|
2907
|
-
let lastSessionStreamBroadcastAt = 0;
|
|
2908
|
-
|
|
2909
|
-
function broadcastState() {
|
|
2910
|
-
lastStateBroadcastAt = Date.now();
|
|
2911
|
-
stateBroadcastTimer = null;
|
|
2912
|
-
const state = buildStatePayload();
|
|
2913
|
-
const message = JSON.stringify({ type: 'state', state });
|
|
2914
|
-
wss.clients.forEach(client => {
|
|
2915
|
-
if (client.readyState === WebSocket.OPEN) {
|
|
2916
|
-
client.send(message);
|
|
2917
|
-
}
|
|
2918
|
-
});
|
|
2919
|
-
}
|
|
2920
|
-
|
|
2921
|
-
function scheduleBroadcastState() {
|
|
2922
|
-
const now = Date.now();
|
|
2923
|
-
const elapsed = now - lastStateBroadcastAt;
|
|
2924
|
-
|
|
2925
|
-
if (elapsed >= STATE_BROADCAST_INTERVAL_MS) {
|
|
2926
|
-
broadcastState();
|
|
2927
|
-
return;
|
|
2928
|
-
}
|
|
2929
|
-
|
|
2930
|
-
if (stateBroadcastTimer) {
|
|
2931
|
-
return;
|
|
2932
|
-
}
|
|
2933
|
-
|
|
2934
|
-
stateBroadcastTimer = setTimeout(() => {
|
|
2935
|
-
broadcastState();
|
|
2936
|
-
}, STATE_BROADCAST_INTERVAL_MS - elapsed);
|
|
2937
|
-
}
|
|
2938
|
-
|
|
2939
|
-
function broadcastSessionPreview(preview) {
|
|
2940
|
-
wss.clients.forEach((client) => {
|
|
2941
|
-
const previewAllowed = client.previewScope !== 'none'
|
|
2942
|
-
&& (client.previewScope !== 'focused' || client.focusedAgentId === preview.agentId);
|
|
2943
|
-
if (client.readyState === WebSocket.OPEN && previewAllowed) {
|
|
2944
|
-
sendPreview(client, preview);
|
|
2945
|
-
}
|
|
2946
|
-
});
|
|
2947
|
-
}
|
|
2948
|
-
|
|
2949
|
-
function schedulePreviewBroadcast(preview) {
|
|
2950
|
-
const agentId = preview && preview.agentId;
|
|
2951
|
-
if (!agentId) {
|
|
2952
|
-
broadcastSessionPreview(preview);
|
|
2953
|
-
return;
|
|
2954
|
-
}
|
|
2955
|
-
|
|
2956
|
-
const now = Date.now();
|
|
2957
|
-
const entry = pendingPreviewBroadcasts.get(agentId) || {
|
|
2958
|
-
lastAt: 0,
|
|
2959
|
-
timer: null,
|
|
2960
|
-
preview: null,
|
|
2961
|
-
};
|
|
2962
|
-
entry.preview = preview;
|
|
2963
|
-
|
|
2964
|
-
const elapsed = now - entry.lastAt;
|
|
2965
|
-
if (elapsed >= PREVIEW_BROADCAST_INTERVAL_MS) {
|
|
2966
|
-
if (entry.timer) {
|
|
2967
|
-
clearTimeout(entry.timer);
|
|
2968
|
-
entry.timer = null;
|
|
2969
|
-
}
|
|
2970
|
-
entry.lastAt = now;
|
|
2971
|
-
const latest = entry.preview;
|
|
2972
|
-
entry.preview = null;
|
|
2973
|
-
pendingPreviewBroadcasts.set(agentId, entry);
|
|
2974
|
-
broadcastSessionPreview(latest);
|
|
2975
|
-
return;
|
|
2976
|
-
}
|
|
2977
|
-
|
|
2978
|
-
if (!entry.timer) {
|
|
2979
|
-
entry.timer = setTimeout(() => {
|
|
2980
|
-
entry.timer = null;
|
|
2981
|
-
entry.lastAt = Date.now();
|
|
2982
|
-
const latest = entry.preview;
|
|
2983
|
-
entry.preview = null;
|
|
2984
|
-
pendingPreviewBroadcasts.set(agentId, entry);
|
|
2985
|
-
if (latest) {
|
|
2986
|
-
broadcastSessionPreview(latest);
|
|
2987
|
-
}
|
|
2988
|
-
}, PREVIEW_BROADCAST_INTERVAL_MS - elapsed);
|
|
2989
|
-
}
|
|
2990
|
-
|
|
2991
|
-
pendingPreviewBroadcasts.set(agentId, entry);
|
|
2992
|
-
}
|
|
2993
|
-
|
|
2994
|
-
agentManager.onUpdate(() => {
|
|
2995
|
-
scheduleBroadcastState();
|
|
2996
|
-
});
|
|
2997
|
-
|
|
2998
|
-
agentManager.on('provider-session-updated', () => {
|
|
2999
|
-
agentSessionsCache.invalidate();
|
|
3000
|
-
scheduleBroadcastState();
|
|
3001
|
-
});
|
|
3002
|
-
|
|
3003
|
-
function broadcastAgentActivity(activity) {
|
|
3004
|
-
const message = JSON.stringify({
|
|
3005
|
-
type: 'agent-activity',
|
|
3006
|
-
activity,
|
|
3007
|
-
});
|
|
3008
|
-
wss.clients.forEach((client) => {
|
|
3009
|
-
if (client.readyState === WebSocket.OPEN) {
|
|
3010
|
-
client.send(message);
|
|
3011
|
-
}
|
|
3012
|
-
});
|
|
3013
|
-
}
|
|
3014
|
-
|
|
3015
|
-
function scheduleAgentActivityBroadcast(activity) {
|
|
3016
|
-
if (!activity || !activity.agentId) return;
|
|
3017
|
-
const agentId = activity.agentId;
|
|
3018
|
-
const existing = pendingAgentActivityBroadcasts.get(agentId);
|
|
3019
|
-
if (existing) {
|
|
3020
|
-
existing.activity = activity;
|
|
3021
|
-
return;
|
|
3022
|
-
}
|
|
3023
|
-
const entry = {
|
|
3024
|
-
activity,
|
|
3025
|
-
timer: setTimeout(() => {
|
|
3026
|
-
pendingAgentActivityBroadcasts.delete(agentId);
|
|
3027
|
-
broadcastAgentActivity(entry.activity);
|
|
3028
|
-
}, AGENT_ACTIVITY_BROADCAST_DELAY_MS),
|
|
3029
|
-
};
|
|
3030
|
-
entry.timer.unref?.();
|
|
3031
|
-
pendingAgentActivityBroadcasts.set(agentId, entry);
|
|
3032
|
-
}
|
|
3033
|
-
|
|
3034
|
-
agentManager.onAgentActivity(scheduleAgentActivityBroadcast);
|
|
3035
|
-
|
|
3036
|
-
function scheduleAgentUpdate(update) {
|
|
3037
|
-
const patch = sanitizeAgentUpdatePatch(update?.patch);
|
|
3038
|
-
if (!update?.agentId || !patch) return;
|
|
3039
|
-
const existing = pendingAgentUpdates.get(update.agentId);
|
|
3040
|
-
if (existing) {
|
|
3041
|
-
Object.assign(existing.patch, patch);
|
|
3042
|
-
return;
|
|
3043
|
-
}
|
|
3044
|
-
const entry = {
|
|
3045
|
-
patch,
|
|
3046
|
-
timer: setTimeout(() => {
|
|
3047
|
-
pendingAgentUpdates.delete(update.agentId);
|
|
3048
|
-
const message = JSON.stringify({
|
|
3049
|
-
type: 'agent-update',
|
|
3050
|
-
update: { agentId: update.agentId, patch: entry.patch },
|
|
3051
|
-
});
|
|
3052
|
-
wss.clients.forEach((client) => {
|
|
3053
|
-
if (client.readyState === WebSocket.OPEN) client.send(message);
|
|
3054
|
-
});
|
|
3055
|
-
}, AGENT_ACTIVITY_BROADCAST_DELAY_MS),
|
|
3056
|
-
};
|
|
3057
|
-
entry.timer.unref?.();
|
|
3058
|
-
pendingAgentUpdates.set(update.agentId, entry);
|
|
3059
|
-
}
|
|
3060
|
-
|
|
3061
|
-
agentManager.on('agent-update', scheduleAgentUpdate);
|
|
3062
|
-
|
|
3063
|
-
function broadcastAgentRead(read) {
|
|
3064
|
-
if (!read || !read.agentId) return;
|
|
3065
|
-
const message = JSON.stringify({ type: 'agent-read', read });
|
|
3066
|
-
wss.clients.forEach((client) => {
|
|
3067
|
-
if (client.readyState === WebSocket.OPEN) {
|
|
3068
|
-
client.send(message);
|
|
3069
|
-
}
|
|
3070
|
-
});
|
|
3071
|
-
}
|
|
3072
|
-
|
|
3073
|
-
agentManager.on('agent-read', broadcastAgentRead);
|
|
3074
|
-
|
|
3075
|
-
function sessionStreamKey(stream) {
|
|
3076
|
-
return `${stream.agentId || ''}\0${stream.sessionSource || ''}`;
|
|
3077
|
-
}
|
|
3078
|
-
|
|
3079
|
-
function broadcastSessionStream(stream) {
|
|
3080
|
-
const message = JSON.stringify({
|
|
3081
|
-
type: 'session-output',
|
|
3082
|
-
stream
|
|
3083
|
-
});
|
|
3084
|
-
deliverSessionStreamToClients(wss.clients, stream, {
|
|
3085
|
-
openState: WebSocket.OPEN,
|
|
3086
|
-
maxBufferedAmount: MAX_SESSION_STREAM_CLIENT_BUFFERED_AMOUNT,
|
|
3087
|
-
message,
|
|
3088
|
-
});
|
|
3089
|
-
}
|
|
3090
|
-
|
|
3091
|
-
function flushSessionStreams() {
|
|
3092
|
-
sessionStreamBroadcastTimer = null;
|
|
3093
|
-
const streams = Array.from(pendingSessionStreams.values());
|
|
3094
|
-
pendingSessionStreams.clear();
|
|
3095
|
-
if (streams.length > 0) lastSessionStreamBroadcastAt = Date.now();
|
|
3096
|
-
streams.forEach(broadcastSessionStream);
|
|
3097
|
-
}
|
|
3098
|
-
|
|
3099
|
-
function scheduleSessionStreamBroadcast(stream) {
|
|
3100
|
-
if (!stream || !stream.agentId) return;
|
|
3101
|
-
const now = Date.now();
|
|
3102
|
-
if (shouldBroadcastSessionStreamImmediately({
|
|
3103
|
-
pendingCount: pendingSessionStreams.size,
|
|
3104
|
-
lastBroadcastAt: lastSessionStreamBroadcastAt,
|
|
3105
|
-
now,
|
|
3106
|
-
intervalMs: SESSION_STREAM_BROADCAST_INTERVAL_MS,
|
|
3107
|
-
})) {
|
|
3108
|
-
lastSessionStreamBroadcastAt = now;
|
|
3109
|
-
broadcastSessionStream(coalesceSessionStream(null, stream));
|
|
3110
|
-
return;
|
|
3111
|
-
}
|
|
3112
|
-
const key = sessionStreamKey(stream);
|
|
3113
|
-
const existing = pendingSessionStreams.get(key);
|
|
3114
|
-
pendingSessionStreams.set(key, coalesceSessionStream(existing, stream));
|
|
3115
|
-
|
|
3116
|
-
if (!sessionStreamBroadcastTimer) {
|
|
3117
|
-
sessionStreamBroadcastTimer = setTimeout(flushSessionStreams, SESSION_STREAM_BROADCAST_INTERVAL_MS);
|
|
3118
|
-
if (typeof sessionStreamBroadcastTimer.unref === 'function') sessionStreamBroadcastTimer.unref();
|
|
3119
|
-
}
|
|
3120
|
-
}
|
|
3121
|
-
|
|
3122
|
-
agentManager.onSessionStream((stream) => {
|
|
3123
|
-
scheduleSessionStreamBroadcast(stream);
|
|
3124
|
-
});
|
|
3125
|
-
|
|
3126
|
-
agentManager.onSessionPreview((preview) => {
|
|
3127
|
-
schedulePreviewBroadcast(preview);
|
|
3128
|
-
});
|
|
3129
|
-
|
|
3130
|
-
agentManager.onSystemStats((systemStats) => {
|
|
3131
|
-
const usageSnapshot = agentManager.getAgentUsageSnapshots();
|
|
3132
|
-
const message = JSON.stringify({
|
|
3133
|
-
type: 'system-stats',
|
|
3134
|
-
stats: {
|
|
3135
|
-
...systemStats,
|
|
3136
|
-
ip: getPrimaryLocalIP(),
|
|
3137
|
-
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone
|
|
3138
|
-
},
|
|
3139
|
-
uptime: agentManager.getUptime(),
|
|
3140
|
-
usageRate: {
|
|
3141
|
-
windowMs: usageSnapshot.windowMs,
|
|
3142
|
-
estimatedTokensPerMinute: usageSnapshot.estimatedTokensPerMinute,
|
|
3143
|
-
source: usageSnapshot.source,
|
|
3144
|
-
}
|
|
3145
|
-
});
|
|
3146
|
-
wss.clients.forEach(client => {
|
|
3147
|
-
if (client.readyState === WebSocket.OPEN) {
|
|
3148
|
-
client.send(message);
|
|
3149
|
-
}
|
|
3150
|
-
});
|
|
3151
|
-
});
|
|
3152
|
-
|
|
3153
|
-
let serverStarted = false;
|
|
3154
|
-
let terminalRuntimeCleanupPromise = null;
|
|
3155
|
-
|
|
3156
|
-
function runTerminalRuntimeStartupCleanup() {
|
|
3157
|
-
if (!terminalRuntimeCleanupPromise) {
|
|
3158
|
-
terminalRuntimeCleanupPromise = cleanupTerminalRuntime({ configDir: configManager.farmingDir })
|
|
3159
|
-
.catch(error => {
|
|
3160
|
-
console.warn('Failed to clean terminal runtime leftovers:', error && (error.message || error));
|
|
3161
|
-
return null;
|
|
3162
|
-
});
|
|
3163
|
-
}
|
|
3164
|
-
return terminalRuntimeCleanupPromise;
|
|
3165
|
-
}
|
|
3166
|
-
|
|
3167
|
-
function startServer() {
|
|
3168
|
-
if (serverStarted) return server;
|
|
3169
|
-
serverStarted = true;
|
|
3170
|
-
|
|
3171
|
-
void Promise.all([
|
|
3172
|
-
runTerminalRuntimeStartupCleanup(),
|
|
3173
|
-
browserRuntimeRecoveryPromise,
|
|
3174
|
-
]).finally(() => {
|
|
3175
|
-
server.listen(PORT, () => {
|
|
3176
|
-
const token = tokenAuth.getToken();
|
|
3177
|
-
const localIPs = getLocalIPs();
|
|
3178
|
-
const entryPath = BASE_PATH || '/';
|
|
3179
|
-
const entrySuffix = authEnabled ? `${entryPath}?token=${token}` : entryPath;
|
|
3180
|
-
|
|
3181
|
-
console.log('');
|
|
3182
|
-
console.log(' Farming server running on:');
|
|
3183
|
-
console.log('');
|
|
3184
|
-
console.log(` Local: http://localhost:${PORT}${entrySuffix}`);
|
|
3185
|
-
localIPs.forEach(ip => {
|
|
3186
|
-
console.log(` Network: http://${ip}:${PORT}${entrySuffix}`);
|
|
3187
|
-
});
|
|
3188
|
-
console.log('');
|
|
3189
|
-
if (authEnabled) {
|
|
3190
|
-
console.log(` Token: ${token}`);
|
|
3191
|
-
const tokenInfo = tokenAuth.getTokenInfo();
|
|
3192
|
-
if (tokenInfo) {
|
|
3193
|
-
console.log(` Token style: ${tokenInfo.style} (${tokenInfo.source}, ~${tokenInfo.entropyBits} bits)`);
|
|
3194
|
-
}
|
|
3195
|
-
} else {
|
|
3196
|
-
console.log(' Token auth: disabled');
|
|
3197
|
-
}
|
|
3198
|
-
console.log('');
|
|
3199
|
-
setTimeout(warmCodexExecutableVersionCache, 100);
|
|
3200
|
-
void autoResumeMainPageAgentSessions();
|
|
3201
|
-
});
|
|
3202
|
-
});
|
|
3203
|
-
|
|
3204
|
-
return server;
|
|
3205
|
-
}
|
|
3206
|
-
|
|
3207
|
-
if (require.main === module) {
|
|
3208
|
-
startServer();
|
|
3209
|
-
}
|
|
3210
|
-
|
|
3211
|
-
module.exports = {
|
|
3212
|
-
app,
|
|
3213
|
-
server,
|
|
3214
|
-
wss,
|
|
3215
|
-
agentManager,
|
|
3216
|
-
browserResourceManager,
|
|
3217
|
-
workspaceFileService,
|
|
3218
|
-
handleMessage,
|
|
3219
|
-
resolveCliBinDir,
|
|
3220
|
-
resolveInputTargetAgentId,
|
|
3221
|
-
rewriteIndexHtmlForBasePath,
|
|
3222
|
-
appendIndexHtmlAssetToken,
|
|
3223
|
-
startServer,
|
|
3224
|
-
runTerminalRuntimeStartupCleanup,
|
|
3225
|
-
};
|