farming-code 2.2.6
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/LICENSE +21 -0
- package/README.md +287 -0
- package/README.zh_cn.md +329 -0
- package/SECURITY.md +29 -0
- package/THIRD_PARTY_NOTICES.md +106 -0
- package/backend/acp/client-services.js +312 -0
- package/backend/acp/patch-decisions.js +137 -0
- package/backend/acp/permission-security.js +108 -0
- package/backend/acp-runtime.js +1144 -0
- package/backend/acp-session-state.js +448 -0
- package/backend/acp-transcript.js +169 -0
- package/backend/agent-activity.js +186 -0
- package/backend/agent-env.js +250 -0
- package/backend/agent-json-stream.js +268 -0
- package/backend/agent-manager.js +4259 -0
- package/backend/agent-memory-report.js +583 -0
- package/backend/agent-order.js +162 -0
- package/backend/agent-provider-session.js +287 -0
- package/backend/agent-session-history.js +888 -0
- package/backend/app-server-api.js +917 -0
- package/backend/async-cache.js +94 -0
- package/backend/auth.js +178 -0
- package/backend/claude-settings.js +121 -0
- package/backend/cli-agents.js +484 -0
- package/backend/codex-app-server-home.js +39 -0
- package/backend/codex-app-server-runtime.js +901 -0
- package/backend/codex-context-window.js +189 -0
- package/backend/codex-models.js +229 -0
- package/backend/codex-rollout-follower.js +243 -0
- package/backend/codex-session-history.js +553 -0
- package/backend/codex-transcript-sanitizer.js +103 -0
- package/backend/codex-transcript.js +2341 -0
- package/backend/command-runner-child.js +67 -0
- package/backend/config-manager.js +815 -0
- package/backend/control-api.js +151 -0
- package/backend/data/chinese-poetic-words.json +8223 -0
- package/backend/data/poetic-word-sources/china.json +49209 -0
- package/backend/data/poetic-word-sources/india.json +9763 -0
- package/backend/data/poetic-word-sources/japan.json +8455 -0
- package/backend/executable-discovery.js +207 -0
- package/backend/farming-app-cli.js +790 -0
- package/backend/farming-cli.js +400 -0
- package/backend/farming-session-store.js +373 -0
- package/backend/haiku-token.js +772 -0
- package/backend/index-html.js +49 -0
- package/backend/input-parts.js +54 -0
- package/backend/input-routing.js +24 -0
- package/backend/json-cli-runtime.js +161 -0
- package/backend/local-session-engine.js +620 -0
- package/backend/main-agent-skills.js +292 -0
- package/backend/main-page-session.js +119 -0
- package/backend/native-pty-host-client.js +516 -0
- package/backend/native-pty-host-path.js +22 -0
- package/backend/native-pty-host.js +816 -0
- package/backend/native-session-engine.js +196 -0
- package/backend/network.js +23 -0
- package/backend/npm-update-helper.js +196 -0
- package/backend/packaged-node-pty.js +79 -0
- package/backend/provider-session-id.js +47 -0
- package/backend/qr-share-tickets.js +100 -0
- package/backend/review-diff-router.js +180 -0
- package/backend/review-diff-service.js +1270 -0
- package/backend/review-session-router.js +74 -0
- package/backend/review-session-service.js +432 -0
- package/backend/review-session-store.js +144 -0
- package/backend/review-state-router.js +116 -0
- package/backend/review-state-store.js +287 -0
- package/backend/run-history-store.js +70 -0
- package/backend/server.js +2446 -0
- package/backend/session-engine-bridge.js +110 -0
- package/backend/session-engine-router.js +66 -0
- package/backend/session-engine.js +39 -0
- package/backend/shell-busy-integration.js +472 -0
- package/backend/slash-command-discovery.js +296 -0
- package/backend/storage-layout.js +101 -0
- package/backend/system-monitor.js +77 -0
- package/backend/terminal-runtime-cleanup.js +80 -0
- package/backend/terminal-screen-state.js +240 -0
- package/backend/terminal-screen-worker-pool.js +165 -0
- package/backend/terminal-screen-worker-thread.js +161 -0
- package/backend/terminal-screen-worker.js +255 -0
- package/backend/terminal-status.js +356 -0
- package/backend/theme-manager.js +145 -0
- package/backend/update-service.js +1183 -0
- package/backend/usage-forecast.js +86 -0
- package/backend/usage-monitor.js +1357 -0
- package/backend/workspace-discovery.js +311 -0
- package/backend/workspace-file-router.js +431 -0
- package/backend/workspace-file-service.js +2765 -0
- package/bin/farming +5 -0
- package/config/farming.deploy.env.example +13 -0
- package/config/farming.install.env.example +17 -0
- package/dist/assets/App-BrP-ENHg.css +1 -0
- package/dist/assets/App-Dza5yEDe.js +124 -0
- package/dist/assets/FileEditorMarkdownPreview-DRDNQ8mX.css +1 -0
- package/dist/assets/FileEditorMarkdownPreview-elKWc8Im.js +404 -0
- package/dist/assets/FileEditorPane-BQLocXOp.js +5 -0
- package/dist/assets/IconGlyphs-RkMAdBXF.js +1 -0
- package/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- package/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- package/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- package/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- package/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- package/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- package/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- package/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- package/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- package/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- package/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- package/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- package/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- package/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- package/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- package/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- package/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- package/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- package/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- package/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- package/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- package/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- package/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- package/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- package/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- package/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- package/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- package/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- package/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- package/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- package/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- package/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- package/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- package/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- package/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- package/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- package/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- package/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- package/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- package/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- package/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- package/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- package/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- package/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- package/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- package/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- package/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- package/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- package/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- package/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- package/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- package/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- package/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- package/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- package/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- package/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- package/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- package/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- package/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- package/dist/assets/ProjectFilesSection-lS1azbgj.js +12 -0
- package/dist/assets/ReviewPage-DgnmgU2T.js +3 -0
- package/dist/assets/abap-Cc5D5h5i.js +1 -0
- package/dist/assets/abnfDiagram-VRR7QNED-BgawzDQT.js +1 -0
- package/dist/assets/apex-BKopwLmx.js +1 -0
- package/dist/assets/api-D1lyBYIQ.js +1 -0
- package/dist/assets/arc-CWoVlxJw.js +1 -0
- package/dist/assets/architecture-TIHT7OUA-CbJBFh3w.js +1 -0
- package/dist/assets/architectureDiagram-ZJ3FMSHR-DUShwPg0.js +36 -0
- package/dist/assets/array-HeX70jSN.js +1 -0
- package/dist/assets/azcli-DI-C34WA.js +1 -0
- package/dist/assets/bat-Cbp6u42z.js +1 -0
- package/dist/assets/bicep-4QA1jTCM.js +2 -0
- package/dist/assets/blockDiagram-677ZJIJ3-CeGZviXR.js +132 -0
- package/dist/assets/c4Diagram-LMCZKHZV-CLhYht6S.js +10 -0
- package/dist/assets/cameligo-Dda_adPs.js +1 -0
- package/dist/assets/channel-Uw90AO0b.js +1 -0
- package/dist/assets/chunk-2Q5K7J3B-d5WWh8jo.js +1 -0
- package/dist/assets/chunk-32BRIVSS-LFYpT-Cu.js +1 -0
- package/dist/assets/chunk-52WLFC77-BH6jDw9J.js +10 -0
- package/dist/assets/chunk-5FCAYU7R-C7AmIhYv.js +1 -0
- package/dist/assets/chunk-5HE753X5-C8cXaouP.js +1 -0
- package/dist/assets/chunk-5JV3BV7I-C1PtimcA.js +1 -0
- package/dist/assets/chunk-5TONJI2A-_81zUME8.js +2 -0
- package/dist/assets/chunk-5VM5RSS4-Dlw7AXWp.js +15 -0
- package/dist/assets/chunk-7BUUIJ7U-ZBRhjLOh.js +1 -0
- package/dist/assets/chunk-BIQX33UG-CWmzxeAw.js +1 -0
- package/dist/assets/chunk-C7G6YPKG-B_mIUlb7.js +1 -0
- package/dist/assets/chunk-CQNSW5MT-CMayolwI.js +1 -0
- package/dist/assets/chunk-CYSBUYHQ-lSPbrL3Z.js +1 -0
- package/dist/assets/chunk-DECur_0Z.js +1 -0
- package/dist/assets/chunk-EMLP6XTP-D9-jtCnM.js +1 -0
- package/dist/assets/chunk-EX3LRPZG-C63hVuAJ.js +231 -0
- package/dist/assets/chunk-FWX5IMBZ-CboX-WF_.js +2 -0
- package/dist/assets/chunk-HOUHSVGY-BOgSd6Un.js +1 -0
- package/dist/assets/chunk-ICXQ74PX-C0K-pgZt.js +2 -0
- package/dist/assets/chunk-JG7HCLWE-Cib606u_.js +2 -0
- package/dist/assets/chunk-JWPE2WC7-DSyBRaRG.js +1 -0
- package/dist/assets/chunk-KEIR6QF5-CNOHik0T.js +161 -0
- package/dist/assets/chunk-MOJQB5TN-CVMfwN_7.js +88 -0
- package/dist/assets/chunk-MOZMSUNE-D6JeZqmE.js +1 -0
- package/dist/assets/chunk-OGEWGWER-DLT3pxyw.js +1 -0
- package/dist/assets/chunk-OSBZ3O6U-C0W8KSeg.js +1 -0
- package/dist/assets/chunk-PUDLZKDR-Cd079yIQ.js +156 -0
- package/dist/assets/chunk-Q4XR5HBZ-D0jHa6SO.js +70 -0
- package/dist/assets/chunk-QBLGF6JB-p4omp7uO.js +1 -0
- package/dist/assets/chunk-R7FJI6CG-CZ_p9hSr.js +1 -0
- package/dist/assets/chunk-RYQCIY6F-CZA9pvFX.js +1 -0
- package/dist/assets/chunk-U6XO7XAA-CcXKsWxp.js +2 -0
- package/dist/assets/chunk-V7JOEXUC-CVDtR75H.js +206 -0
- package/dist/assets/chunk-VAUOI2AC-B-W2_9Ot.js +1 -0
- package/dist/assets/chunk-VR4S4FIN-FD3kWh32.js +1 -0
- package/dist/assets/chunk-WYO6CB5R-C2bGaS5r.js +127 -0
- package/dist/assets/chunk-XXDRQBXY-BBXwjhUx.js +1 -0
- package/dist/assets/chunk-Y2CYZVJY-Di0sYzi1.js +1 -0
- package/dist/assets/chunk-YOTPTUD7-BXHmEk89.js +1 -0
- package/dist/assets/chunk-ZGVPDNZ5-ymgts0zo.js +62 -0
- package/dist/assets/chunk-ZIRB5QZD-DncwlQQc.js +32 -0
- package/dist/assets/classDiagram-OUVF2IWQ-DKW_dtku.js +1 -0
- package/dist/assets/classDiagram-v2-EOCWNBFH-C8Fi7-yv.js +1 -0
- package/dist/assets/clojure-CvY8A83L.js +1 -0
- package/dist/assets/code-dark-Cjb1QGhu.css +1 -0
- package/dist/assets/code-mobile-3C2Rr-CN.css +1 -0
- package/dist/assets/codicon-ngg6Pgfi.ttf +0 -0
- package/dist/assets/coffee-4LX2Sv8A.js +1 -0
- package/dist/assets/core-ZlAPicox.js +3 -0
- package/dist/assets/cose-bilkent-JH36ORCC-BNIAz0nN.js +1 -0
- package/dist/assets/cpp-D4AzSV28.js +1 -0
- package/dist/assets/csharp-C4nWN4Yd.js +1 -0
- package/dist/assets/csp-CiEaaS5Z.js +1 -0
- package/dist/assets/css-DXxBRsbk.js +3 -0
- package/dist/assets/css.worker-DyDu5ynT.js +89 -0
- package/dist/assets/cssMode-DDWxTPm_.js +1 -0
- package/dist/assets/cynefin-VYW2F7L2-DgXGHlRt.js +1 -0
- package/dist/assets/cynefinDiagram-TSTJHNR4-CwpobXIl.js +62 -0
- package/dist/assets/cypher-DOMzqyFA.js +1 -0
- package/dist/assets/cytoscape.esm-Cl8iC3D7.js +321 -0
- package/dist/assets/dagre-DXlcHgr3.js +1 -0
- package/dist/assets/dagre-VKFMJZFB-Dkhlfjna.js +4 -0
- package/dist/assets/dart-BWS7MXt_.js +1 -0
- package/dist/assets/defaultLocale-GJwWH1Jr.js +1 -0
- package/dist/assets/diagram-FQU43EPY-hA8dicp6.js +3 -0
- package/dist/assets/diagram-G47NLZAW-BTY6JTzO.js +24 -0
- package/dist/assets/diagram-NH7WQ7WH-xkaHHX9M.js +24 -0
- package/dist/assets/diagram-OA4YK3LP-gk_fg9UU.js +30 -0
- package/dist/assets/diagram-WEI45ONY-BvC8xd6T.js +41 -0
- package/dist/assets/dist-ClAL-Tv5.js +1 -0
- package/dist/assets/dockerfile-Baf4U3xT.js +1 -0
- package/dist/assets/ebnfDiagram-CCIWWBDH-CR7RaNat.js +1 -0
- package/dist/assets/ecl-CT7udlkQ.js +1 -0
- package/dist/assets/editor-CtbFZlk0.css +1 -0
- package/dist/assets/editor.api2-B9h6iFPK.js +872 -0
- package/dist/assets/editor.worker-Dpt_xPrs.js +26 -0
- package/dist/assets/elixir-CIbAZw5l.js +1 -0
- package/dist/assets/erDiagram-Q63AITRT-_M-JFPbj.js +85 -0
- package/dist/assets/eventmodeling-45OFAUF4-DXYTQ2nt.js +1 -0
- package/dist/assets/file-icons-EFUGSSwf.js +1 -0
- package/dist/assets/flow9-Dqiw45WV.js +1 -0
- package/dist/assets/flowDiagram-23GEKE2U-DAiS5Y7G.js +1 -0
- package/dist/assets/freemarker2-QBsTERH-.js +3 -0
- package/dist/assets/fsharp-E8E2L9jU.js +1 -0
- package/dist/assets/ganttDiagram-NO4QXBWP-Bl8P1yf_.js +292 -0
- package/dist/assets/gitGraph-TEB2WS4Q-CzTBjEvV.js +1 -0
- package/dist/assets/gitGraphDiagram-IHSO6WYX-DoHa5ysz.js +106 -0
- package/dist/assets/go-CouVSN4D.js +1 -0
- package/dist/assets/graphlib-CbSvZcZU.js +1 -0
- package/dist/assets/graphql-a6h6h5-t.js +1 -0
- package/dist/assets/handlebars-CuoxPo5A.js +1 -0
- package/dist/assets/hcl-vso3e6N-.js +1 -0
- package/dist/assets/html-gX5PEu0Q.js +1 -0
- package/dist/assets/html.worker-IWNXhnIC.js +502 -0
- package/dist/assets/htmlMode-DIWUbUxd.js +1 -0
- package/dist/assets/index-BpHPeJf0.js +10 -0
- package/dist/assets/info-DKCQHKI2-DznP7j3s.js +1 -0
- package/dist/assets/infoDiagram-FWYZ7A6U-DSFcSuUs.js +2 -0
- package/dist/assets/ini-SwZZkotR.js +1 -0
- package/dist/assets/init-BPLPMQ3Y.js +1 -0
- package/dist/assets/ishikawaDiagram-FXEZZL3T-DdkqullI.js +70 -0
- package/dist/assets/java-CtRLC4_3.js +1 -0
- package/dist/assets/javascript-BYKGIUGs.js +1 -0
- package/dist/assets/journeyDiagram-5HDEW3XC-B4hRnQJr.js +139 -0
- package/dist/assets/json.worker-c4h5s80L.js +58 -0
- package/dist/assets/jsonMode-AFmQQP6u.js +7 -0
- package/dist/assets/jsx-runtime-BthL93pI.js +1 -0
- package/dist/assets/julia-CuJWFKQH.js +1 -0
- package/dist/assets/kanban-definition-HUTT4EX6-tm2FZB2E.js +89 -0
- package/dist/assets/katex-C8DPyI6s.js +257 -0
- package/dist/assets/kotlin-B_kBhsre.js +1 -0
- package/dist/assets/less-CtrHIfz3.js +2 -0
- package/dist/assets/lexon-yVt1pJLu.js +1 -0
- package/dist/assets/line-CmsjK4kn.js +1 -0
- package/dist/assets/linear-Dld3_ERu.js +1 -0
- package/dist/assets/liquid-DBIl-f0z.js +1 -0
- package/dist/assets/lspLanguageFeatures-DK6tqicV.js +4 -0
- package/dist/assets/lua-prOwgJ4U.js +1 -0
- package/dist/assets/m3-DhrdTQ84.js +1 -0
- package/dist/assets/main-DTJwH51M.css +1 -0
- package/dist/assets/map-LZWWoeWa.js +1 -0
- package/dist/assets/markdown-DoI5HwSM.js +1 -0
- package/dist/assets/mdx-DS-bVtZN.js +1 -0
- package/dist/assets/mermaid-parser.core-BQtepoLd.js +4 -0
- package/dist/assets/mermaid.core-C7UT_EEL.js +11 -0
- package/dist/assets/mindmap-definition-LN4V7U3C-DplfjGms.js +96 -0
- package/dist/assets/mips-Dye5jsBU.js +1 -0
- package/dist/assets/monaco.contribution-CyLosfZI.js +2 -0
- package/dist/assets/msdax-TkNOyIL8.js +1 -0
- package/dist/assets/mysql-CEc-RKZF.js +1 -0
- package/dist/assets/objective-c-CqqCsQz0.js +1 -0
- package/dist/assets/ordinal-BLrOss5K.js +1 -0
- package/dist/assets/packet-7NZHBO7P-BDEPOPha.js +1 -0
- package/dist/assets/pascal-DO_pj8yg.js +1 -0
- package/dist/assets/pascaligo-1cbdZdF3.js +1 -0
- package/dist/assets/path-YdFzr2W6.js +1 -0
- package/dist/assets/pegDiagram-2B236MQR-lu3SdkQs.js +1 -0
- package/dist/assets/perl-B_N3PYfC.js +1 -0
- package/dist/assets/pgsql-9iYk1FQ7.js +1 -0
- package/dist/assets/php-rO2sgoUM.js +1 -0
- package/dist/assets/pie-RZYD4A2V-DWIrwPJQ.js +1 -0
- package/dist/assets/pieDiagram-ENE6RG2P-3fHbmDf9.js +39 -0
- package/dist/assets/pla-Cbgv60kq.js +1 -0
- package/dist/assets/postiats-y6xrhy_M.js +1 -0
- package/dist/assets/powerquery-BaD8gWNE.js +1 -0
- package/dist/assets/powershell-BMhG4JVe.js +1 -0
- package/dist/assets/preload-helper-mJowh_hw.js +1 -0
- package/dist/assets/protobuf-CdOP5kN3.js +2 -0
- package/dist/assets/pug-Ck60hNF9.js +1 -0
- package/dist/assets/python-C93q3xXz.js +1 -0
- package/dist/assets/qoder-C9LmmOSf.svg +1 -0
- package/dist/assets/qoder-Cf9gl0Y5.svg +1 -0
- package/dist/assets/qoder-gHCinseV.svg +1 -0
- package/dist/assets/qrcode-DV-_rjU8.js +3 -0
- package/dist/assets/qsharp-BcaKFfP-.js +1 -0
- package/dist/assets/quadrantDiagram-ABIIQ3AL-a5qoWz-X.js +7 -0
- package/dist/assets/r-rFNes0iK.js +1 -0
- package/dist/assets/radar-I7S5WNFK-B7OaD8vp.js +1 -0
- package/dist/assets/railroad-3IZDKUUU-BXW2nuCA.js +1 -0
- package/dist/assets/railroad-abnf-AHOZXSZD-Buw9QG2r.js +1 -0
- package/dist/assets/railroad-ebnf-EBAXGLYW-BjSi4sWJ.js +1 -0
- package/dist/assets/railroad-peg-LSFZ7HO6-DN1wIVhS.js +1 -0
- package/dist/assets/railroadDiagram-RFXS5EU6-D2TOuXW9.js +1 -0
- package/dist/assets/razor-CBifNGXO.js +1 -0
- package/dist/assets/react-dom-gtU6X-Hj.js +1 -0
- package/dist/assets/redis-DkpQ3K6G.js +1 -0
- package/dist/assets/redshift-Di9N5TI3.js +1 -0
- package/dist/assets/requirementDiagram-TGXJPOKE-LQBWpUvd.js +84 -0
- package/dist/assets/restructuredtext-By5ZzLt6.js +1 -0
- package/dist/assets/review-BcnwQtJv.css +1 -0
- package/dist/assets/rough.esm-c4PR5shF.js +1 -0
- package/dist/assets/ruby-fF5zGiLp.js +1 -0
- package/dist/assets/rust-C5b3AYY6.js +1 -0
- package/dist/assets/sankeyDiagram-HTMAVEWB-B1ohYoyK.js +40 -0
- package/dist/assets/sb-CD07UNRs.js +1 -0
- package/dist/assets/scala-jtn1ip9G.js +1 -0
- package/dist/assets/scheme-C4vqMkn_.js +1 -0
- package/dist/assets/scss-I4Glxjbz.js +3 -0
- package/dist/assets/sequenceDiagram-DBY2YBRQ-C3vM-hMq.js +162 -0
- package/dist/assets/shell-DubUcuns.js +1 -0
- package/dist/assets/sizeCapture-X5ZJPWSS-daTB6OcF.js +1 -0
- package/dist/assets/solidity-CNy2KSl2.js +1 -0
- package/dist/assets/sophia-D9_URt84.js +1 -0
- package/dist/assets/sparql-DviXpm4C.js +1 -0
- package/dist/assets/sql-vDRbAZgK.js +1 -0
- package/dist/assets/src-WBW1QxJt.js +1 -0
- package/dist/assets/st-CnpnRqA7.js +1 -0
- package/dist/assets/stateDiagram-2N3HPSRC-BvFhOryI.js +1 -0
- package/dist/assets/stateDiagram-v2-6OUMAXLB-WMrz4PNo.js +1 -0
- package/dist/assets/swift-D_g8Ganz.js +1 -0
- package/dist/assets/swimlanes-5IMT3BWC-DKF_b-2s.js +2 -0
- package/dist/assets/swimlanesDiagram-G3AALYLV-BGswC9oC.js +8 -0
- package/dist/assets/systemverilog-OuZa5h1M.js +1 -0
- package/dist/assets/tcl-DBm9ucsV.js +1 -0
- package/dist/assets/timeline-definition-FHXFAJF6-vAs11ux_.js +120 -0
- package/dist/assets/tokens-BwenNn5Q.css +1 -0
- package/dist/assets/treeView-QDETBFTQ-BVfICaZQ.js +1 -0
- package/dist/assets/treemap-6X3UGDF4-oWHPqo17.js +1 -0
- package/dist/assets/ts.worker-BwM5Ha3L.js +67719 -0
- package/dist/assets/tsMode-DGZTlTj8.js +11 -0
- package/dist/assets/twig-BRzEqX5Y.js +1 -0
- package/dist/assets/typescript-KJKfYO7K.js +1 -0
- package/dist/assets/typespec-D4ZgGeX8.js +1 -0
- package/dist/assets/useWorkspaceMenuKeyboard-CneKAZUJ.js +1 -0
- package/dist/assets/vb-DcP4s6WS.js +1 -0
- package/dist/assets/vennDiagram-L72KCM5P-BybTX_9B.js +34 -0
- package/dist/assets/wardley-OPB4EBWU-Dg8t5y3s.js +1 -0
- package/dist/assets/wardleyDiagram-EHGQE667-CicUmMBR.js +78 -0
- package/dist/assets/wgsl-CnRJ_QUf.js +298 -0
- package/dist/assets/workers-BSQRIkqa.js +1 -0
- package/dist/assets/workspace-view-state-CTyDzk2D.js +1 -0
- package/dist/assets/xml-C8FAcN6j.js +1 -0
- package/dist/assets/xychartDiagram-FW5EYKEG-BhWDyCX-.js +7 -0
- package/dist/assets/yaml-BQ4NrRhG.js +1 -0
- package/dist/assets/zsh-CLpveKlF.svg +1 -0
- package/dist/assets/zsh-FxSpMPbz.svg +1 -0
- package/dist/farming-2/app-icon-v2-180.png +0 -0
- package/dist/farming-2/app-icon-v2-192.png +0 -0
- package/dist/farming-2/app-icon-v2-512.png +0 -0
- package/dist/farming-2/app-icon-v2-maskable-512.png +0 -0
- package/dist/farming-2/app-icon-v2-master.png +0 -0
- package/dist/farming-2/favicon-v2-16.png +0 -0
- package/dist/farming-2/favicon-v2-32.png +0 -0
- package/dist/farming-2/favicon-v2-48.png +0 -0
- package/dist/farming-2/favicon-v2-64.png +0 -0
- package/dist/farming-2/favicon-v2.ico +0 -0
- package/dist/farming-2/favicon.ico +0 -0
- package/dist/farming-2/github-social-preview.png +0 -0
- package/dist/farming-2/images/avatar-watercolor-v1-bee-garden.png +0 -0
- package/dist/farming-2/images/avatar-watercolor-v2.png +0 -0
- package/dist/farming-2/images/avatar-watercolor-v3.png +0 -0
- package/dist/farming-2/legacy-icons-v1/favicon.ico +1 -0
- package/dist/farming-2/legacy-icons-v1/logo-16.png +0 -0
- package/dist/farming-2/legacy-icons-v1/logo-180.png +0 -0
- package/dist/farming-2/legacy-icons-v1/logo-192.png +0 -0
- package/dist/farming-2/legacy-icons-v1/logo-32.png +0 -0
- package/dist/farming-2/legacy-icons-v1/logo-48.png +0 -0
- package/dist/farming-2/legacy-icons-v1/logo-512.png +0 -0
- package/dist/farming-2/legacy-icons-v1/logo-64.png +0 -0
- package/dist/farming-2/logo-16.png +0 -0
- package/dist/farming-2/logo-180.png +0 -0
- package/dist/farming-2/logo-192.png +0 -0
- package/dist/farming-2/logo-32.png +0 -0
- package/dist/farming-2/logo-48.png +0 -0
- package/dist/farming-2/logo-512.png +0 -0
- package/dist/farming-2/logo-64.png +0 -0
- package/dist/farming-2/site.webmanifest +27 -0
- package/dist/farming-2/style_capsules/watercolor-pastoral.json +94 -0
- package/dist/index.html +27 -0
- package/frontend/ghostty-loader.js +24 -0
- package/frontend/runtime-paths.js +28 -0
- package/frontend/session-bridge.js +82 -0
- package/frontend/session-modal-bridge.js +331 -0
- package/frontend/skin-bridge.js +25 -0
- package/frontend/skins/crt/app.js +6940 -0
- package/frontend/skins/crt/assets/branding/farming-crt-icon.svg +11 -0
- package/frontend/skins/crt/assets/branding/farming-crt-logo-v1.png +0 -0
- package/frontend/skins/crt/assets/effects/phosphor-noise.svg +11 -0
- package/frontend/skins/crt/assets/fonts/departure-mono/DepartureMonoNerdFontMono-Regular.otf +0 -0
- package/frontend/skins/crt/assets/fonts/departure-mono/LICENSE +93 -0
- package/frontend/skins/crt/effects.js +8 -0
- package/frontend/skins/crt/index.html +1712 -0
- package/frontend/skins/crt/styles/billing.css +1003 -0
- package/frontend/skins/crt/styles/effects.css +171 -0
- package/frontend/skins/crt/styles/history.css +215 -0
- package/frontend/skins/crt/styles/monochrome-green.css +509 -0
- package/frontend/skins/crt/styles/search.css +271 -0
- package/frontend/terminal-bridge.js +192 -0
- package/frontend/theme-loader.js +84 -0
- package/frontend/themes/terminal/theme.json +24 -0
- package/frontend/vendor/ghostty-web/LICENSE +21 -0
- package/frontend/vendor/ghostty-web/__vite-browser-external-2447137e.js +4 -0
- package/frontend/vendor/ghostty-web/ghostty-vt.wasm +0 -0
- package/frontend/vendor/ghostty-web/ghostty-web.js +2963 -0
- package/frontend/vendor/ghostty-web/ghostty-web.umd.cjs +13 -0
- package/frontend/vendor/ghostty-web/index.d.ts +1842 -0
- package/index.html +23 -0
- package/package.json +144 -0
- package/scripts/sync-ghostty-vendor.js +66 -0
|
@@ -0,0 +1,2446 @@
|
|
|
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 ConfigManager = require('./config-manager');
|
|
13
|
+
const ThemeManager = require('./theme-manager');
|
|
14
|
+
const TokenAuth = require('./auth');
|
|
15
|
+
const { getLocalIPs, getPrimaryLocalIP } = require('./network');
|
|
16
|
+
const { listAvailableAgents, resolveCompatibleCodexExecutable } = require('./executable-discovery');
|
|
17
|
+
const { readClaudeSettingsSummary } = require('./claude-settings');
|
|
18
|
+
const { listCodexModelOptions } = require('./codex-models');
|
|
19
|
+
const { listCodexSessions } = require('./codex-session-history');
|
|
20
|
+
const {
|
|
21
|
+
buildAgentSessionResumeCommand,
|
|
22
|
+
findAgentSession,
|
|
23
|
+
isSafeSessionId,
|
|
24
|
+
listAgentSessions,
|
|
25
|
+
normalizeProvider,
|
|
26
|
+
paginateAgentSessions,
|
|
27
|
+
searchAgentSessions,
|
|
28
|
+
} = require('./agent-session-history');
|
|
29
|
+
const {
|
|
30
|
+
findActiveAgentClaimingSession,
|
|
31
|
+
mainPageAgentSessionKey,
|
|
32
|
+
mainPageAgentSessionsToAutoResume,
|
|
33
|
+
resumedAgentSource,
|
|
34
|
+
} = require('./main-page-session');
|
|
35
|
+
const { discoverAgentWorkspaces } = require('./workspace-discovery');
|
|
36
|
+
const { createControlRouter } = require('./control-api');
|
|
37
|
+
const { WorkspaceFileService, WorkspaceFileError } = require('./workspace-file-service');
|
|
38
|
+
const { createWorkspaceFileRouter } = require('./workspace-file-router');
|
|
39
|
+
const { UsageMonitor } = require('./usage-monitor');
|
|
40
|
+
const { CodexContextWindowReader } = require('./codex-context-window');
|
|
41
|
+
const { DEFAULT_MAX_TURNS: DEFAULT_CODEX_TRANSCRIPT_MAX_TURNS, readCodexTranscript } = require('./codex-transcript');
|
|
42
|
+
const { AsyncCache } = require('./async-cache');
|
|
43
|
+
const { getMainAgentSkillsCatalog } = require('./main-agent-skills');
|
|
44
|
+
const { discoverSlashCommands } = require('./slash-command-discovery');
|
|
45
|
+
const { FarmingUpdateService } = require('./update-service');
|
|
46
|
+
const { isRestartBlockingAgent } = require('./agent-activity');
|
|
47
|
+
const { inputPartsFromMessage } = require('./input-parts');
|
|
48
|
+
const { AppServerApiBridge, createAppServerApiRouter } = require('./app-server-api');
|
|
49
|
+
const { cleanupTerminalRuntime } = require('./terminal-runtime-cleanup');
|
|
50
|
+
const { QrShareTicketStore, SHARE_TICKET_TTL_MS } = require('./qr-share-tickets');
|
|
51
|
+
const { ReviewStateStore } = require('./review-state-store');
|
|
52
|
+
const { createReviewStateRouter } = require('./review-state-router');
|
|
53
|
+
const { ReviewDiffService } = require('./review-diff-service');
|
|
54
|
+
const { createReviewDiffRouter } = require('./review-diff-router');
|
|
55
|
+
const { ReviewSessionStore } = require('./review-session-store');
|
|
56
|
+
const { ReviewSessionService } = require('./review-session-service');
|
|
57
|
+
const { createReviewSessionRouter } = require('./review-session-router');
|
|
58
|
+
const {
|
|
59
|
+
normalizeBasePath,
|
|
60
|
+
routePath,
|
|
61
|
+
rewriteIndexHtmlForBasePath,
|
|
62
|
+
appendIndexHtmlAssetToken,
|
|
63
|
+
} = require('./index-html');
|
|
64
|
+
|
|
65
|
+
const execFileAsync = promisify(execFile);
|
|
66
|
+
|
|
67
|
+
const BASE_PATH = normalizeBasePath(process.env.FARMING_BASE_PATH || '/');
|
|
68
|
+
const PORT = process.env.PORT || 3000;
|
|
69
|
+
const tokenAuth = new TokenAuth({ basePath: BASE_PATH || '/' });
|
|
70
|
+
const authEnabled = tokenAuth.isEnabled();
|
|
71
|
+
const WS_PATH = routePath(BASE_PATH, '/ws');
|
|
72
|
+
const encodeCookieToken = TokenAuth.encodeCookieToken;
|
|
73
|
+
const MAX_CODEX_TRANSCRIPT_TURNS = 1000;
|
|
74
|
+
const INTERACTIVE_REFRESH_CACHE_MAX_AGE_MS = 3_000;
|
|
75
|
+
|
|
76
|
+
const app = express();
|
|
77
|
+
const server = http.createServer(app);
|
|
78
|
+
const wss = new WebSocket.Server({ server });
|
|
79
|
+
|
|
80
|
+
const configManager = new ConfigManager();
|
|
81
|
+
configManager.init();
|
|
82
|
+
|
|
83
|
+
function resolveCliBinDir() {
|
|
84
|
+
if (process.env.FARMING_CLI_BIN_DIR) {
|
|
85
|
+
return process.env.FARMING_CLI_BIN_DIR;
|
|
86
|
+
}
|
|
87
|
+
if (process.pkg || process.env.FARMING_PACKAGED_RUNTIME === '1') {
|
|
88
|
+
return path.dirname(process.execPath);
|
|
89
|
+
}
|
|
90
|
+
return path.join(__dirname, '..', 'bin');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const agentManager = new AgentManager(configManager, {
|
|
94
|
+
controlUrl: `http://127.0.0.1:${PORT}${BASE_PATH}`,
|
|
95
|
+
tokenFile: tokenAuth.getTokenFile(),
|
|
96
|
+
authDisabled: !authEnabled,
|
|
97
|
+
cliBinDir: resolveCliBinDir(),
|
|
98
|
+
});
|
|
99
|
+
const themeManager = new ThemeManager({ configDir: configManager.farmingDir });
|
|
100
|
+
const workspaceFileService = new WorkspaceFileService();
|
|
101
|
+
const updateService = new FarmingUpdateService({
|
|
102
|
+
rootDir: path.join(__dirname, '..'),
|
|
103
|
+
configDir: configManager.farmingDir,
|
|
104
|
+
platform: process.platform,
|
|
105
|
+
arch: process.arch,
|
|
106
|
+
packagedRuntime: Boolean(process.pkg || process.env.FARMING_PACKAGED_RUNTIME === '1'),
|
|
107
|
+
getUpdateUrl: () => configManager.getSettings().updateUrl || '',
|
|
108
|
+
});
|
|
109
|
+
const appServerApiBridge = new AppServerApiBridge();
|
|
110
|
+
const usageMonitor = new UsageMonitor({ agentManager, getProviderHomes: configuredProviderHomes });
|
|
111
|
+
const codexContextWindowReader = new CodexContextWindowReader();
|
|
112
|
+
const usageSummaryCache = new AsyncCache(() => usageMonitor.getUsageSummary(), {
|
|
113
|
+
ttlMs: 15_000,
|
|
114
|
+
staleMs: 2 * 60_000,
|
|
115
|
+
});
|
|
116
|
+
const codexModelOptionsCache = new AsyncCache(() => listCodexModelOptions(), {
|
|
117
|
+
ttlMs: 5 * 60_000,
|
|
118
|
+
staleMs: 30 * 60_000,
|
|
119
|
+
});
|
|
120
|
+
function configuredProviderHomes() {
|
|
121
|
+
const settings = configManager.getSettings();
|
|
122
|
+
const agentHomes = settings.agentHomes && typeof settings.agentHomes === 'object' ? settings.agentHomes : {};
|
|
123
|
+
const result = {};
|
|
124
|
+
for (const [provider, homes] of Object.entries(agentHomes)) {
|
|
125
|
+
if (!Array.isArray(homes)) continue;
|
|
126
|
+
result[provider] = homes.map(home => ({
|
|
127
|
+
id: String(home.id || 'default'),
|
|
128
|
+
path: configManager.expandWorkspacePath(String(home.path || '')),
|
|
129
|
+
})).filter(home => home.id && home.path);
|
|
130
|
+
}
|
|
131
|
+
return result;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const agentSessionsCache = new AsyncCache(() => {
|
|
135
|
+
return listAgentSessions({
|
|
136
|
+
limit: 5000,
|
|
137
|
+
providerLimit: 5000,
|
|
138
|
+
scanLimit: 5000,
|
|
139
|
+
providerHomes: configuredProviderHomes(),
|
|
140
|
+
});
|
|
141
|
+
}, {
|
|
142
|
+
ttlMs: 30_000,
|
|
143
|
+
staleMs: 5 * 60_000,
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
function withSearchTimeout(promise, timeoutMs) {
|
|
147
|
+
let timer;
|
|
148
|
+
const timeout = new Promise((_, reject) => {
|
|
149
|
+
timer = setTimeout(() => {
|
|
150
|
+
const error = new Error('Agent search timed out');
|
|
151
|
+
error.code = 'ETIMEDOUT';
|
|
152
|
+
reject(error);
|
|
153
|
+
}, timeoutMs);
|
|
154
|
+
});
|
|
155
|
+
return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const qrShareTickets = new QrShareTicketStore({ ttlMs: SHARE_TICKET_TTL_MS });
|
|
159
|
+
const reviewStateStore = new ReviewStateStore(configManager.farmingDir, {
|
|
160
|
+
seedReviews: {
|
|
161
|
+
'review-fixture-553987': {
|
|
162
|
+
patchsets: {
|
|
163
|
+
'Patchset 20': { reviewedPaths: ['clis/dataflow.py', 'clis/fetch_instance_log.py'], revision: 0 },
|
|
164
|
+
'Patchset 19': { reviewedPaths: ['clis/fetch_instance_log.py'], revision: 0 },
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
});
|
|
169
|
+
const reviewDiffService = new ReviewDiffService(agentManager, workspaceFileService);
|
|
170
|
+
const reviewSessionStore = new ReviewSessionStore(configManager.farmingDir);
|
|
171
|
+
const reviewSessionService = new ReviewSessionService(workspaceFileService, reviewSessionStore, reviewStateStore, {
|
|
172
|
+
resolveAgentRoot: agentId => agentManager.getAgentWorkspaceRoot(agentId),
|
|
173
|
+
resolveAcpReviewChanges: (agentId, itemIds) => agentManager.getAcpReviewChanges(agentId, itemIds),
|
|
174
|
+
});
|
|
175
|
+
const workspaceDiscoveryCache = new AsyncCache((key) => {
|
|
176
|
+
const request = JSON.parse(key);
|
|
177
|
+
return discoverAgentWorkspaces({
|
|
178
|
+
limit: request.limit,
|
|
179
|
+
agent: request.agent,
|
|
180
|
+
});
|
|
181
|
+
}, {
|
|
182
|
+
ttlMs: 30_000,
|
|
183
|
+
staleMs: 2 * 60_000,
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
const frontendDir = path.join(__dirname, '../frontend');
|
|
187
|
+
const crtFrontendDir = path.join(frontendDir, 'skins', 'crt');
|
|
188
|
+
const distDir = path.join(__dirname, '../dist');
|
|
189
|
+
const staticAppDir = fs.existsSync(distDir) ? distDir : frontendDir;
|
|
190
|
+
const xtermBrowserEntryPath = path.join(__dirname, '..', 'node_modules', '@xterm', 'xterm', 'lib', 'xterm.js');
|
|
191
|
+
const xtermFitEntryPath = path.join(__dirname, '..', 'node_modules', '@xterm', 'addon-fit', 'lib', 'addon-fit.js');
|
|
192
|
+
const xtermWebglEntryPath = path.join(__dirname, '..', 'node_modules', '@xterm', 'addon-webgl', 'lib', 'addon-webgl.js');
|
|
193
|
+
const xtermCssPath = path.join(__dirname, '..', 'node_modules', '@xterm', 'xterm', 'css', 'xterm.css');
|
|
194
|
+
const materialIconDir = path.join(__dirname, '..', 'node_modules', 'material-icon-theme', 'icons');
|
|
195
|
+
|
|
196
|
+
function getAvailableAgentsForRequest() {
|
|
197
|
+
if (process.env.FARMING_E2E_FAKE_EXECUTABLES === '1') {
|
|
198
|
+
return [
|
|
199
|
+
{
|
|
200
|
+
name: 'codex',
|
|
201
|
+
command: 'codex',
|
|
202
|
+
description: 'Codex CLI - OpenAI coding assistant',
|
|
203
|
+
category: 'coding',
|
|
204
|
+
supported: true,
|
|
205
|
+
interactive: true,
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
name: 'claude',
|
|
209
|
+
command: 'claude',
|
|
210
|
+
description: 'Claude CLI - Anthropic assistant',
|
|
211
|
+
category: 'coding',
|
|
212
|
+
supported: true,
|
|
213
|
+
interactive: true,
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
name: 'bash',
|
|
217
|
+
command: 'bash',
|
|
218
|
+
description: 'Shell session',
|
|
219
|
+
category: 'shell',
|
|
220
|
+
supported: true,
|
|
221
|
+
interactive: true,
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
name: 'zsh',
|
|
225
|
+
command: 'zsh',
|
|
226
|
+
description: 'Z shell',
|
|
227
|
+
category: 'shell',
|
|
228
|
+
supported: true,
|
|
229
|
+
interactive: true,
|
|
230
|
+
},
|
|
231
|
+
];
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const shellEnv = agentManager.resolveAgentShellEnv('', { maxAgeMs: INTERACTIVE_REFRESH_CACHE_MAX_AGE_MS });
|
|
235
|
+
const pathEnv = typeof shellEnv?.PATH === 'string' && shellEnv.PATH.trim()
|
|
236
|
+
? shellEnv.PATH
|
|
237
|
+
: (process.env.PATH || '');
|
|
238
|
+
return listAvailableAgents(pathEnv);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function normalizeWorkspaceCompletionInput(value) {
|
|
242
|
+
const raw = typeof value === 'string' ? value.trim() : '';
|
|
243
|
+
if (!raw) {
|
|
244
|
+
return { raw: '', parent: os.homedir(), prefix: '', displayParent: '~', explicitDirectory: false };
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const home = os.homedir();
|
|
248
|
+
const expanded = raw === '~' ? home : raw.startsWith('~/') ? path.join(home, raw.slice(2)) : raw;
|
|
249
|
+
const explicitDirectory = raw.endsWith(path.sep) || raw === '~';
|
|
250
|
+
const parent = explicitDirectory ? expanded : path.dirname(expanded);
|
|
251
|
+
const prefix = explicitDirectory ? '' : path.basename(expanded);
|
|
252
|
+
const trimTrailingSeparator = (input) => {
|
|
253
|
+
let next = input;
|
|
254
|
+
while (next.length > 1 && next.endsWith(path.sep)) next = next.slice(0, -1);
|
|
255
|
+
return next;
|
|
256
|
+
};
|
|
257
|
+
const displayParent = explicitDirectory
|
|
258
|
+
? trimTrailingSeparator(raw)
|
|
259
|
+
: trimTrailingSeparator(raw.slice(0, raw.length - prefix.length));
|
|
260
|
+
|
|
261
|
+
return {
|
|
262
|
+
raw,
|
|
263
|
+
parent: parent || path.sep,
|
|
264
|
+
prefix,
|
|
265
|
+
displayParent: displayParent || (path.isAbsolute(raw) ? path.sep : ''),
|
|
266
|
+
explicitDirectory,
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
async function listWorkspacePathCompletions(partialPath, limit = 12) {
|
|
271
|
+
const query = normalizeWorkspaceCompletionInput(partialPath);
|
|
272
|
+
const entries = await fs.promises.readdir(query.parent, { withFileTypes: true });
|
|
273
|
+
const normalizedPrefix = query.prefix.toLowerCase();
|
|
274
|
+
const maxResults = Math.max(1, Math.min(Number(limit) || 12, 100));
|
|
275
|
+
|
|
276
|
+
return entries
|
|
277
|
+
.filter(entry => entry.isDirectory())
|
|
278
|
+
.filter(entry => normalizedPrefix.startsWith('.') || !entry.name.startsWith('.'))
|
|
279
|
+
.filter(entry => !normalizedPrefix || entry.name.toLowerCase().startsWith(normalizedPrefix))
|
|
280
|
+
.sort((a, b) => a.name.localeCompare(b.name))
|
|
281
|
+
.slice(0, maxResults)
|
|
282
|
+
.map(entry => {
|
|
283
|
+
const fullPath = path.join(query.parent, entry.name);
|
|
284
|
+
const displayPath = query.raw.startsWith('~')
|
|
285
|
+
? path.join(query.displayParent || '~', entry.name)
|
|
286
|
+
: fullPath;
|
|
287
|
+
return {
|
|
288
|
+
name: entry.name,
|
|
289
|
+
path: `${displayPath}${path.sep}`,
|
|
290
|
+
};
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// iOS can fetch installed-web-app metadata outside the authenticated page
|
|
295
|
+
// request, without preserving its cookie or token query. These files contain
|
|
296
|
+
// only public product artwork and must remain available to that fetcher.
|
|
297
|
+
const publicProductAssetsDir = path.join(staticAppDir, 'farming-2');
|
|
298
|
+
app.use(routePath(BASE_PATH, '/farming-2'), express.static(publicProductAssetsDir, { index: false }));
|
|
299
|
+
|
|
300
|
+
const sendPublicProductAsset = (filename) => (_req, res) => {
|
|
301
|
+
res.sendFile(path.join(publicProductAssetsDir, filename));
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
app.get(['/favicon.ico', routePath(BASE_PATH, '/favicon.ico')], sendPublicProductAsset('favicon-v2.ico'));
|
|
305
|
+
app.get([
|
|
306
|
+
'/apple-touch-icon.png',
|
|
307
|
+
'/apple-touch-icon-precomposed.png',
|
|
308
|
+
'/apple-touch-icon-180x180.png',
|
|
309
|
+
routePath(BASE_PATH, '/apple-touch-icon.png'),
|
|
310
|
+
routePath(BASE_PATH, '/apple-touch-icon-precomposed.png'),
|
|
311
|
+
routePath(BASE_PATH, '/apple-touch-icon-180x180.png'),
|
|
312
|
+
], sendPublicProductAsset('app-icon-v2-180.png'));
|
|
313
|
+
|
|
314
|
+
app.get(routePath(BASE_PATH, '/j/:code'), (req, res) => {
|
|
315
|
+
if (req.method === 'HEAD') {
|
|
316
|
+
res.status(204).end();
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const ticket = qrShareTickets.consume(req.params.code);
|
|
321
|
+
if (!ticket || (authEnabled && !tokenAuth.verify(ticket.token))) {
|
|
322
|
+
res.status(410).send('Farming share link expired.');
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (authEnabled) {
|
|
327
|
+
res.setHeader(
|
|
328
|
+
'Set-Cookie',
|
|
329
|
+
`farming_token=${encodeCookieToken(ticket.token)}; Path=/; HttpOnly; SameSite=Lax`,
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
res.redirect(302, entryPathWithQuery(ticket.targetQuery));
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
// Token authentication middleware (before static files)
|
|
336
|
+
app.use(tokenAuth.middleware());
|
|
337
|
+
|
|
338
|
+
// Auth status endpoint (allowed without authentication via middleware)
|
|
339
|
+
app.get(routePath(BASE_PATH, '/api/auth/status'), (req, res) => {
|
|
340
|
+
res.json({ authRequired: authEnabled });
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
function absoluteClientUrl(req, urlPath) {
|
|
344
|
+
const forwardedProto = String(req.headers['x-forwarded-proto'] || '').split(',')[0].trim();
|
|
345
|
+
const protocol = forwardedProto || req.protocol || 'http';
|
|
346
|
+
const forwardedHost = String(req.headers['x-forwarded-host'] || '').split(',')[0].trim();
|
|
347
|
+
const host = forwardedHost || req.headers.host || `127.0.0.1:${PORT}`;
|
|
348
|
+
return `${protocol}://${host}${urlPath}`;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function shareTargetPositiveInteger(value) {
|
|
352
|
+
const number = Number(value);
|
|
353
|
+
return Number.isInteger(number) && number > 0 ? String(number) : '';
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function shareTargetString(value, maxLength) {
|
|
357
|
+
const string = String(value || '').trim();
|
|
358
|
+
if (!string || string.length > maxLength || string.includes('\0')) return '';
|
|
359
|
+
return string;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function shareTargetQueryFromBody(body) {
|
|
363
|
+
const target = body && typeof body === 'object' ? body.target : null;
|
|
364
|
+
if (!target || typeof target !== 'object') return '';
|
|
365
|
+
|
|
366
|
+
const kind = target.kind === 'file' ? 'file' : target.kind === 'folder' ? 'folder' : target.kind === 'agent' ? 'agent' : '';
|
|
367
|
+
const agentId = shareTargetString(target.agentId, 160);
|
|
368
|
+
const absolutePath = shareTargetString(target.absolutePath, 2048);
|
|
369
|
+
const projectLabel = shareTargetString(target.projectLabel, 160);
|
|
370
|
+
if (!kind || kind === 'agent' && !agentId || kind !== 'agent' && !absolutePath && !agentId && !projectLabel) return '';
|
|
371
|
+
|
|
372
|
+
const params = new URLSearchParams();
|
|
373
|
+
params.set('ftarget', kind);
|
|
374
|
+
if (agentId) params.set('agent', agentId);
|
|
375
|
+
if (absolutePath) params.set('path', absolutePath);
|
|
376
|
+
if (projectLabel) params.set('project', projectLabel);
|
|
377
|
+
|
|
378
|
+
if (kind === 'folder') {
|
|
379
|
+
const folderPath = shareTargetString(target.folderPath, 2048);
|
|
380
|
+
if (!absolutePath && !folderPath) return '';
|
|
381
|
+
if (folderPath) params.set('folder', folderPath);
|
|
382
|
+
} else if (kind === 'file') {
|
|
383
|
+
const filePath = shareTargetString(target.filePath, 2048);
|
|
384
|
+
if (!absolutePath && !filePath) return '';
|
|
385
|
+
if (filePath) params.set('file', filePath);
|
|
386
|
+
if (target.view === 'diff') params.set('view', 'diff');
|
|
387
|
+
const line = shareTargetPositiveInteger(target.lineNumber);
|
|
388
|
+
const column = shareTargetPositiveInteger(target.column);
|
|
389
|
+
const endColumn = shareTargetPositiveInteger(target.endColumn);
|
|
390
|
+
if (line) params.set('line', line);
|
|
391
|
+
if (column) params.set('column', column);
|
|
392
|
+
if (endColumn) params.set('endColumn', endColumn);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
if (absolutePath) {
|
|
396
|
+
const absoluteParams = new URLSearchParams(params);
|
|
397
|
+
absoluteParams.delete('agent');
|
|
398
|
+
absoluteParams.delete(kind === 'folder' ? 'folder' : 'file');
|
|
399
|
+
if (absoluteParams.toString().length <= 1800) return absoluteParams.toString();
|
|
400
|
+
params.delete('path');
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
return params.toString();
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function entryPathWithQuery(query = '', options = {}) {
|
|
407
|
+
const entryPath = BASE_PATH || '/';
|
|
408
|
+
const params = new URLSearchParams(query || '');
|
|
409
|
+
if (options.includeToken && authEnabled) {
|
|
410
|
+
params.set('token', tokenAuth.getToken());
|
|
411
|
+
}
|
|
412
|
+
const queryString = params.toString();
|
|
413
|
+
return queryString ? `${entryPath}?${queryString}` : entryPath;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function entryPathWithToken(targetQuery = '') {
|
|
417
|
+
return entryPathWithQuery(targetQuery, { includeToken: true });
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
app.post(routePath(BASE_PATH, '/api/share/qr-ticket'), express.json({ limit: '8kb' }), (req, res) => {
|
|
421
|
+
try {
|
|
422
|
+
const targetQuery = shareTargetQueryFromBody(req.body);
|
|
423
|
+
const ticket = qrShareTickets.create(authEnabled ? tokenAuth.getToken() : '', { targetQuery });
|
|
424
|
+
const shortPath = routePath(BASE_PATH, `/j/${ticket.code}`);
|
|
425
|
+
const longPath = entryPathWithToken(ticket.targetQuery);
|
|
426
|
+
res.json({
|
|
427
|
+
code: ticket.code,
|
|
428
|
+
expiresAt: ticket.expiresAt,
|
|
429
|
+
ttlMs: SHARE_TICKET_TTL_MS,
|
|
430
|
+
shortPath,
|
|
431
|
+
shortUrl: absoluteClientUrl(req, shortPath),
|
|
432
|
+
longUrl: absoluteClientUrl(req, longPath),
|
|
433
|
+
tokenLabel: authEnabled ? tokenAuth.getToken() : '',
|
|
434
|
+
});
|
|
435
|
+
} catch (error) {
|
|
436
|
+
res.status(500).json({ error: error.message || 'Failed to create share ticket' });
|
|
437
|
+
}
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
app.delete(routePath(BASE_PATH, '/api/share/qr-ticket/:code'), (req, res) => {
|
|
441
|
+
res.json({ revoked: qrShareTickets.revoke(req.params.code) });
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
// Terminal assets remain available to the standalone CRT skin when React is served from dist.
|
|
445
|
+
app.use(routePath(BASE_PATH, '/vendor'), express.static(path.join(frontendDir, 'vendor')));
|
|
446
|
+
app.get(routePath(BASE_PATH, '/vendor/xterm/xterm.js'), (_req, res) => {
|
|
447
|
+
res.sendFile(xtermBrowserEntryPath);
|
|
448
|
+
});
|
|
449
|
+
app.get(routePath(BASE_PATH, '/vendor/xterm/addon-fit.js'), (_req, res) => {
|
|
450
|
+
res.sendFile(xtermFitEntryPath);
|
|
451
|
+
});
|
|
452
|
+
app.get(routePath(BASE_PATH, '/vendor/xterm/addon-webgl.js'), (_req, res) => {
|
|
453
|
+
res.sendFile(xtermWebglEntryPath);
|
|
454
|
+
});
|
|
455
|
+
app.get(routePath(BASE_PATH, '/vendor/xterm/xterm.css'), (_req, res) => {
|
|
456
|
+
res.sendFile(xtermCssPath);
|
|
457
|
+
});
|
|
458
|
+
app.use(routePath(BASE_PATH, '/vendor/material-icons'), express.static(materialIconDir));
|
|
459
|
+
app.get(routePath(BASE_PATH, '/vendor/material-icons/:iconId.svg'), (req, res) => {
|
|
460
|
+
const fallbackIcon = String(req.params.iconId || '').startsWith('folder-') ? 'folder.svg' : 'file.svg';
|
|
461
|
+
res.sendFile(path.join(materialIconDir, fallbackIcon));
|
|
462
|
+
});
|
|
463
|
+
if (BASE_PATH) {
|
|
464
|
+
app.use('/assets', express.static(path.join(staticAppDir, 'assets'), { index: false }));
|
|
465
|
+
app.use('/farming-2', express.static(path.join(staticAppDir, 'farming-2'), { index: false }));
|
|
466
|
+
}
|
|
467
|
+
const crtEntryPath = routePath(BASE_PATH, '/crt');
|
|
468
|
+
app.get(crtEntryPath, (req, res) => {
|
|
469
|
+
if (req.path.endsWith('/')) {
|
|
470
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
471
|
+
res.sendFile(path.join(crtFrontendDir, 'index.html'));
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
const requestUrl = new URL(req.url || '/', `http://${req.headers.host || 'localhost'}`);
|
|
475
|
+
res.redirect(308, `${crtEntryPath}/${requestUrl.search}`);
|
|
476
|
+
});
|
|
477
|
+
app.use(`${crtEntryPath}/shared`, express.static(frontendDir, { index: false }));
|
|
478
|
+
app.use(`${crtEntryPath}/`, express.static(crtFrontendDir, { index: false }));
|
|
479
|
+
app.use(BASE_PATH || '/', express.static(staticAppDir, { index: false }));
|
|
480
|
+
|
|
481
|
+
app.use(routePath(BASE_PATH, '/api/files'), createWorkspaceFileRouter(agentManager, workspaceFileService));
|
|
482
|
+
|
|
483
|
+
app.use(routePath(BASE_PATH, '/api/review-sessions'), createReviewSessionRouter(reviewSessionService));
|
|
484
|
+
app.use(routePath(BASE_PATH, '/api/reviews'), createReviewDiffRouter(reviewDiffService, reviewSessionService));
|
|
485
|
+
app.use(routePath(BASE_PATH, '/api/reviews'), createReviewStateRouter(reviewStateStore));
|
|
486
|
+
|
|
487
|
+
app.use(routePath(BASE_PATH, '/api/control'), createControlRouter(agentManager, {
|
|
488
|
+
notifyUpdate: broadcastState,
|
|
489
|
+
}));
|
|
490
|
+
|
|
491
|
+
app.use(routePath(BASE_PATH, '/api/app-server'), createAppServerApiRouter({
|
|
492
|
+
bridge: appServerApiBridge,
|
|
493
|
+
}));
|
|
494
|
+
|
|
495
|
+
app.get([
|
|
496
|
+
BASE_PATH || '/',
|
|
497
|
+
`${BASE_PATH || ''}/`,
|
|
498
|
+
routePath(BASE_PATH, '/code'),
|
|
499
|
+
routePath(BASE_PATH, '/code/'),
|
|
500
|
+
routePath(BASE_PATH, '/error-preview'),
|
|
501
|
+
routePath(BASE_PATH, '/review'),
|
|
502
|
+
].filter(Boolean), (req, res) => {
|
|
503
|
+
const indexPath = path.join(staticAppDir, 'index.html');
|
|
504
|
+
fs.readFile(indexPath, 'utf8', (error, html) => {
|
|
505
|
+
if (error) {
|
|
506
|
+
res.status(500).send('Farming frontend is not built');
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
510
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
511
|
+
const rewrittenHtml = rewriteIndexHtmlForBasePath(html, BASE_PATH);
|
|
512
|
+
const requestUrl = new URL(req.url || '/', `http://${req.headers.host || 'localhost'}`);
|
|
513
|
+
const requestToken = requestUrl.searchParams.has('token') ? tokenAuth.extractToken(req) : '';
|
|
514
|
+
const htmlWithAssetToken = authEnabled && requestToken && tokenAuth.verify(requestToken)
|
|
515
|
+
? appendIndexHtmlAssetToken(rewrittenHtml, requestToken)
|
|
516
|
+
: rewrittenHtml;
|
|
517
|
+
res.send(htmlWithAssetToken);
|
|
518
|
+
});
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
app.get(routePath(BASE_PATH, '/api/executables'), (req, res) => {
|
|
522
|
+
const availableAgents = getAvailableAgentsForRequest();
|
|
523
|
+
res.setHeader('Cache-Control', 'no-store');
|
|
524
|
+
res.json({
|
|
525
|
+
agents: availableAgents,
|
|
526
|
+
total: availableAgents.length
|
|
527
|
+
});
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
app.get(routePath(BASE_PATH, '/api/workspaces/complete'), async (req, res) => {
|
|
531
|
+
try {
|
|
532
|
+
const partialPath = typeof req.query.path === 'string' ? req.query.path : '';
|
|
533
|
+
const requestedLimit = Number(req.query.limit);
|
|
534
|
+
const suggestions = await listWorkspacePathCompletions(partialPath, requestedLimit);
|
|
535
|
+
res.json({ suggestions });
|
|
536
|
+
} catch (error) {
|
|
537
|
+
res.status(200).json({
|
|
538
|
+
suggestions: [],
|
|
539
|
+
error: error.message || 'Failed to read directory',
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
app.get(routePath(BASE_PATH, '/api/skills'), (_req, res) => {
|
|
545
|
+
res.json({ skills: getMainAgentSkillsCatalog() });
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
app.get(routePath(BASE_PATH, '/api/slash-commands'), (req, res) => {
|
|
549
|
+
const provider = typeof req.query.provider === 'string' ? req.query.provider : '';
|
|
550
|
+
const workspace = typeof req.query.workspace === 'string' ? req.query.workspace : '';
|
|
551
|
+
res.json({ commands: discoverSlashCommands({ provider, workspace }) });
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
const IMAGE_ATTACHMENT_EXTENSIONS = {
|
|
555
|
+
'image/png': 'png',
|
|
556
|
+
'image/jpeg': 'jpg',
|
|
557
|
+
'image/gif': 'gif',
|
|
558
|
+
'image/webp': 'webp',
|
|
559
|
+
};
|
|
560
|
+
const IMAGE_ATTACHMENT_RETENTION_MS = 7 * 24 * 60 * 60 * 1000;
|
|
561
|
+
const IMAGE_ATTACHMENT_GC_INTERVAL_MS = 60 * 60 * 1000;
|
|
562
|
+
const IMAGE_ATTACHMENT_FILENAME_RE = /^pasted-image-\d+-[a-f0-9]{8}\.(?:png|jpg|gif|webp)$/;
|
|
563
|
+
let lastImageAttachmentGcAt = 0;
|
|
564
|
+
|
|
565
|
+
function imageAttachmentExtension(contentType) {
|
|
566
|
+
const normalized = String(contentType || '').split(';')[0].trim().toLowerCase();
|
|
567
|
+
return IMAGE_ATTACHMENT_EXTENSIONS[normalized] || '';
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
function imageAttachmentsDir() {
|
|
571
|
+
return path.join(configManager.farmingDir, 'attachments');
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
async function cleanupExpiredImageAttachments(options = {}) {
|
|
575
|
+
const now = Date.now();
|
|
576
|
+
if (!options.force && now - lastImageAttachmentGcAt < IMAGE_ATTACHMENT_GC_INTERVAL_MS) return;
|
|
577
|
+
lastImageAttachmentGcAt = now;
|
|
578
|
+
|
|
579
|
+
const attachmentsDir = imageAttachmentsDir();
|
|
580
|
+
let entries = [];
|
|
581
|
+
try {
|
|
582
|
+
entries = await fs.promises.readdir(attachmentsDir, { withFileTypes: true });
|
|
583
|
+
} catch (error) {
|
|
584
|
+
if (error && error.code !== 'ENOENT') {
|
|
585
|
+
console.warn('Failed to scan image attachments:', error.message || error);
|
|
586
|
+
}
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
const cutoff = now - IMAGE_ATTACHMENT_RETENTION_MS;
|
|
591
|
+
await Promise.all(entries.map(async (entry) => {
|
|
592
|
+
if (!entry.isFile() || !IMAGE_ATTACHMENT_FILENAME_RE.test(entry.name)) return;
|
|
593
|
+
|
|
594
|
+
const filePath = path.join(attachmentsDir, entry.name);
|
|
595
|
+
try {
|
|
596
|
+
const stat = await fs.promises.stat(filePath);
|
|
597
|
+
if (stat.mtimeMs < cutoff) {
|
|
598
|
+
await fs.promises.unlink(filePath);
|
|
599
|
+
}
|
|
600
|
+
} catch (error) {
|
|
601
|
+
if (!error || error.code !== 'ENOENT') {
|
|
602
|
+
console.warn('Failed to remove expired image attachment:', error && (error.message || error));
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
}));
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
void cleanupExpiredImageAttachments({ force: true });
|
|
609
|
+
|
|
610
|
+
app.post(
|
|
611
|
+
routePath(BASE_PATH, '/api/attachments/image'),
|
|
612
|
+
express.raw({ type: 'image/*', limit: '12mb' }),
|
|
613
|
+
(req, res) => {
|
|
614
|
+
const contentType = String(req.headers['content-type'] || '').split(';')[0].trim().toLowerCase();
|
|
615
|
+
const extension = imageAttachmentExtension(contentType);
|
|
616
|
+
if (!extension) {
|
|
617
|
+
res.status(415).json({ error: 'unsupported image type' });
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
if (!Buffer.isBuffer(req.body) || req.body.length === 0) {
|
|
622
|
+
res.status(400).json({ error: 'empty image attachment' });
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
const attachmentsDir = imageAttachmentsDir();
|
|
627
|
+
fs.mkdirSync(attachmentsDir, { recursive: true });
|
|
628
|
+
const filename = `pasted-image-${Date.now()}-${crypto.randomBytes(4).toString('hex')}.${extension}`;
|
|
629
|
+
const filePath = path.join(attachmentsDir, filename);
|
|
630
|
+
fs.writeFileSync(filePath, req.body);
|
|
631
|
+
void cleanupExpiredImageAttachments();
|
|
632
|
+
|
|
633
|
+
res.status(201).json({
|
|
634
|
+
path: filePath,
|
|
635
|
+
name: filename,
|
|
636
|
+
type: contentType,
|
|
637
|
+
size: req.body.length,
|
|
638
|
+
});
|
|
639
|
+
}
|
|
640
|
+
);
|
|
641
|
+
|
|
642
|
+
app.get(routePath(BASE_PATH, '/api/codex/models'), async (req, res) => {
|
|
643
|
+
const catalog = await codexModelOptionsCache.get('catalog');
|
|
644
|
+
res.json(catalog);
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
app.get(routePath(BASE_PATH, '/api/claude/settings'), (req, res) => {
|
|
648
|
+
res.json({ settings: readClaudeSettingsSummary() });
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
app.get(routePath(BASE_PATH, '/api/usage'), async (req, res) => {
|
|
652
|
+
try {
|
|
653
|
+
const fresh = req.query.fresh === '1';
|
|
654
|
+
if (fresh) usageMonitor.invalidateDailyCache();
|
|
655
|
+
const usage = await usageSummaryCache.get('summary', fresh ? { force: true } : {});
|
|
656
|
+
res.json({ usage });
|
|
657
|
+
} catch (error) {
|
|
658
|
+
res.status(500).json({ error: error.message || 'Failed to read usage information' });
|
|
659
|
+
}
|
|
660
|
+
});
|
|
661
|
+
|
|
662
|
+
app.get(routePath(BASE_PATH, '/api/usage/day'), async (req, res) => {
|
|
663
|
+
try {
|
|
664
|
+
const date = String(req.query.date || '').trim();
|
|
665
|
+
const detail = await usageMonitor.getUsageDay(date, { fresh: req.query.fresh === '1' });
|
|
666
|
+
res.json({ detail });
|
|
667
|
+
} catch (error) {
|
|
668
|
+
const invalidDate = error instanceof RangeError;
|
|
669
|
+
res.status(invalidDate ? 400 : 500).json({
|
|
670
|
+
error: invalidDate
|
|
671
|
+
? error.message
|
|
672
|
+
: error.message || 'Failed to read usage day information',
|
|
673
|
+
});
|
|
674
|
+
}
|
|
675
|
+
});
|
|
676
|
+
|
|
677
|
+
app.post(routePath(BASE_PATH, '/api/codex/context-windows'), express.json(), async (req, res) => {
|
|
678
|
+
try {
|
|
679
|
+
const requestedIds = Array.isArray(req.body?.agentIds)
|
|
680
|
+
? req.body.agentIds
|
|
681
|
+
.map(value => String(value || '').trim())
|
|
682
|
+
.filter(Boolean)
|
|
683
|
+
.slice(0, 20)
|
|
684
|
+
: [];
|
|
685
|
+
const requestedIdSet = new Set(requestedIds);
|
|
686
|
+
const agents = agentManager.getState().agents.filter(agent => requestedIdSet.has(agent.id));
|
|
687
|
+
const contextWindows = await codexContextWindowReader.readForAgents(agents);
|
|
688
|
+
res.json({ contextWindows });
|
|
689
|
+
} catch (error) {
|
|
690
|
+
res.status(500).json({ error: error.message || 'Failed to read Codex context windows' });
|
|
691
|
+
}
|
|
692
|
+
});
|
|
693
|
+
|
|
694
|
+
function blockingUpdateAgents() {
|
|
695
|
+
return agentManager.getState().agents
|
|
696
|
+
.filter(isRestartBlockingAgent)
|
|
697
|
+
.map(agent => ({
|
|
698
|
+
id: agent.id,
|
|
699
|
+
command: agent.command,
|
|
700
|
+
task: agent.task || agent.customTitle || agent.sessionTitle || '',
|
|
701
|
+
cwd: agent.cwd,
|
|
702
|
+
}));
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
app.get(routePath(BASE_PATH, '/api/update'), async (req, res) => {
|
|
706
|
+
try {
|
|
707
|
+
const update = await updateService.check({ force: req.query.force === '1' });
|
|
708
|
+
res.json({
|
|
709
|
+
update: {
|
|
710
|
+
...update,
|
|
711
|
+
blockingAgents: blockingUpdateAgents(),
|
|
712
|
+
},
|
|
713
|
+
});
|
|
714
|
+
} catch (error) {
|
|
715
|
+
res.status(502).json({ error: error.message || 'Failed to check for updates' });
|
|
716
|
+
}
|
|
717
|
+
});
|
|
718
|
+
|
|
719
|
+
app.post(routePath(BASE_PATH, '/api/update/install'), express.json(), async (req, res) => {
|
|
720
|
+
const blockers = blockingUpdateAgents();
|
|
721
|
+
const force = req.body && req.body.force === true;
|
|
722
|
+
if (blockers.length > 0 && !force) {
|
|
723
|
+
res.status(409).json({
|
|
724
|
+
error: 'Cannot upgrade while non-recoverable project agents are running',
|
|
725
|
+
blockingAgents: blockers,
|
|
726
|
+
});
|
|
727
|
+
return;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
try {
|
|
731
|
+
const state = await updateService.startInstall({
|
|
732
|
+
assetName: req.body && typeof req.body.assetName === 'string' ? req.body.assetName : '',
|
|
733
|
+
});
|
|
734
|
+
res.status(202).json({
|
|
735
|
+
update: {
|
|
736
|
+
state,
|
|
737
|
+
blockingAgents: blockers,
|
|
738
|
+
},
|
|
739
|
+
});
|
|
740
|
+
} catch (error) {
|
|
741
|
+
res.status(500).json({ error: error.message || 'Failed to start update' });
|
|
742
|
+
}
|
|
743
|
+
});
|
|
744
|
+
|
|
745
|
+
function warmCodexExecutableVersionCache() {
|
|
746
|
+
const startedAt = Date.now();
|
|
747
|
+
try {
|
|
748
|
+
const result = resolveCompatibleCodexExecutable('');
|
|
749
|
+
if (result.path) {
|
|
750
|
+
console.log(`Codex executable ready: ${result.version || 'unknown version'} (${Date.now() - startedAt}ms)`);
|
|
751
|
+
}
|
|
752
|
+
} catch (error) {
|
|
753
|
+
console.warn('Failed to warm Codex executable version cache:', error.message || error);
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
app.get(routePath(BASE_PATH, '/api/codex/sessions'), async (req, res) => {
|
|
758
|
+
const requestedLimit = Number(req.query.limit);
|
|
759
|
+
const limit = Number.isFinite(requestedLimit) ? Math.max(0, Math.min(1000, requestedLimit)) : 40;
|
|
760
|
+
const requestedScanLimit = Number(req.query.scanLimit);
|
|
761
|
+
const scanLimit = Number.isFinite(requestedScanLimit) ? Math.max(limit, Math.min(5000, requestedScanLimit)) : undefined;
|
|
762
|
+
const sessions = await listCodexSessions({ limit, scanLimit });
|
|
763
|
+
res.json({ sessions });
|
|
764
|
+
});
|
|
765
|
+
|
|
766
|
+
app.get(routePath(BASE_PATH, '/api/agent-sessions'), async (req, res) => {
|
|
767
|
+
try {
|
|
768
|
+
const requestedLimit = Number(req.query.limit);
|
|
769
|
+
const limit = Number.isFinite(requestedLimit) ? Math.max(0, Math.min(1000, requestedLimit)) : 60;
|
|
770
|
+
const cursor = typeof req.query.cursor === 'string' ? req.query.cursor : '';
|
|
771
|
+
const cacheOptions = req.query.force === '1'
|
|
772
|
+
? { force: true }
|
|
773
|
+
: (req.query.fresh === '1' ? { maxAgeMs: INTERACTIVE_REFRESH_CACHE_MAX_AGE_MS } : {});
|
|
774
|
+
const sessions = await agentSessionsCache.get(
|
|
775
|
+
JSON.stringify(configManager.getSettings().agentHomes || {}),
|
|
776
|
+
cacheOptions
|
|
777
|
+
);
|
|
778
|
+
const page = paginateAgentSessions(sessions, { limit: Math.max(1, limit), cursor });
|
|
779
|
+
if (page.invalidCursor) {
|
|
780
|
+
res.status(400).json({ error: 'Invalid Agent session cursor' });
|
|
781
|
+
return;
|
|
782
|
+
}
|
|
783
|
+
const displayStateByKey = new Map(configManager.listAgentSessionRecords()
|
|
784
|
+
.filter(record => record && record.providerSessionKey)
|
|
785
|
+
.map(record => [record.providerSessionKey, record]));
|
|
786
|
+
res.json({
|
|
787
|
+
sessions: page.sessions.map(session => {
|
|
788
|
+
const key = mainPageAgentSessionKey(session.provider, session.id, session.providerHomeId);
|
|
789
|
+
const displayState = displayStateByKey.get(key);
|
|
790
|
+
return typeof displayState?.displayPinned === 'boolean'
|
|
791
|
+
? { ...session, pinned: displayState.displayPinned }
|
|
792
|
+
: session;
|
|
793
|
+
}),
|
|
794
|
+
nextCursor: page.nextCursor,
|
|
795
|
+
hasMore: page.hasMore,
|
|
796
|
+
total: sessions.length,
|
|
797
|
+
});
|
|
798
|
+
} catch (error) {
|
|
799
|
+
console.error('Failed to read agent sessions:', error);
|
|
800
|
+
res.status(500).json({ error: error.message || 'Failed to read agent sessions' });
|
|
801
|
+
}
|
|
802
|
+
});
|
|
803
|
+
|
|
804
|
+
app.get(routePath(BASE_PATH, '/api/agent-sessions/search'), async (req, res) => {
|
|
805
|
+
try {
|
|
806
|
+
const query = typeof req.query.q === 'string' ? req.query.q : '';
|
|
807
|
+
const requestedLimit = Number(req.query.limit);
|
|
808
|
+
const limit = Number.isFinite(requestedLimit) ? Math.max(1, Math.min(1000, requestedLimit)) : 100;
|
|
809
|
+
const settings = configManager.getSettings();
|
|
810
|
+
const cacheOptions = req.query.force === '1'
|
|
811
|
+
? { force: true }
|
|
812
|
+
: (req.query.fresh === '1' ? { maxAgeMs: INTERACTIVE_REFRESH_CACHE_MAX_AGE_MS } : {});
|
|
813
|
+
const sessions = await withSearchTimeout(
|
|
814
|
+
agentSessionsCache.get(
|
|
815
|
+
JSON.stringify(settings.agentHomes || {}),
|
|
816
|
+
cacheOptions
|
|
817
|
+
),
|
|
818
|
+
settings.searchTimeoutMs
|
|
819
|
+
);
|
|
820
|
+
const result = searchAgentSessions(sessions, query, {
|
|
821
|
+
limit,
|
|
822
|
+
projectNames: settings.projectNames,
|
|
823
|
+
});
|
|
824
|
+
const displayStateByKey = new Map(configManager.listAgentSessionRecords()
|
|
825
|
+
.filter(record => record && record.providerSessionKey)
|
|
826
|
+
.map(record => [record.providerSessionKey, record]));
|
|
827
|
+
res.json({
|
|
828
|
+
...result,
|
|
829
|
+
sessions: result.sessions.map(session => {
|
|
830
|
+
const key = mainPageAgentSessionKey(session.provider, session.id, session.providerHomeId);
|
|
831
|
+
const displayState = displayStateByKey.get(key);
|
|
832
|
+
return typeof displayState?.displayPinned === 'boolean'
|
|
833
|
+
? { ...session, pinned: displayState.displayPinned }
|
|
834
|
+
: session;
|
|
835
|
+
}),
|
|
836
|
+
});
|
|
837
|
+
} catch (error) {
|
|
838
|
+
if (error?.code === 'ETIMEDOUT') {
|
|
839
|
+
res.status(504).json({ error: error.message });
|
|
840
|
+
return;
|
|
841
|
+
}
|
|
842
|
+
console.error('Failed to search agent sessions:', error);
|
|
843
|
+
res.status(500).json({ error: error.message || 'Failed to search Agent sessions' });
|
|
844
|
+
}
|
|
845
|
+
});
|
|
846
|
+
|
|
847
|
+
app.patch(routePath(BASE_PATH, '/api/agent-sessions/:provider/:sessionId'), express.json(), (req, res) => {
|
|
848
|
+
const provider = normalizeProvider(req.params.provider);
|
|
849
|
+
const sessionId = String(req.params.sessionId || '').trim();
|
|
850
|
+
const providerHomeId = String(req.body?.providerHomeId || 'default').trim() || 'default';
|
|
851
|
+
if (!provider || !isSafeSessionId(sessionId) || !/^[A-Za-z0-9._-]+$/.test(providerHomeId)) {
|
|
852
|
+
res.status(400).json({ error: 'Invalid Agent session' });
|
|
853
|
+
return;
|
|
854
|
+
}
|
|
855
|
+
if (typeof req.body?.pinned !== 'boolean') {
|
|
856
|
+
res.status(400).json({ error: 'Pinned state is required' });
|
|
857
|
+
return;
|
|
858
|
+
}
|
|
859
|
+
const sessionKey = mainPageAgentSessionKey(provider, sessionId, providerHomeId);
|
|
860
|
+
configManager.setProviderSessionDisplayState(sessionKey, { pinned: req.body.pinned });
|
|
861
|
+
res.json({ sessionKey, pinned: req.body.pinned });
|
|
862
|
+
});
|
|
863
|
+
|
|
864
|
+
app.get(routePath(BASE_PATH, '/api/themes'), (req, res) => {
|
|
865
|
+
const currentTheme = configManager.getSettings().theme || 'terminal';
|
|
866
|
+
res.json({
|
|
867
|
+
themes: themeManager.getAllThemes(),
|
|
868
|
+
current: currentTheme
|
|
869
|
+
});
|
|
870
|
+
});
|
|
871
|
+
|
|
872
|
+
app.get(routePath(BASE_PATH, '/api/settings'), (req, res) => {
|
|
873
|
+
res.json({
|
|
874
|
+
settings: configManager.getSettings()
|
|
875
|
+
});
|
|
876
|
+
});
|
|
877
|
+
|
|
878
|
+
app.get(routePath(BASE_PATH, '/api/workspaces/discovered'), (req, res) => {
|
|
879
|
+
const requestedLimit = Number(req.query.limit);
|
|
880
|
+
const limit = Number.isFinite(requestedLimit) ? Math.max(0, Math.min(20, requestedLimit)) : 12;
|
|
881
|
+
const agent = typeof req.query.agent === 'string' ? req.query.agent : '';
|
|
882
|
+
const cacheToken = JSON.stringify({ limit, agent });
|
|
883
|
+
workspaceDiscoveryCache.get(cacheToken)
|
|
884
|
+
.then(workspaces => {
|
|
885
|
+
res.json({ workspaces });
|
|
886
|
+
})
|
|
887
|
+
.catch(error => {
|
|
888
|
+
console.error('Failed to discover workspaces:', error);
|
|
889
|
+
res.status(500).json({ error: error.message || 'Failed to discover workspaces' });
|
|
890
|
+
});
|
|
891
|
+
});
|
|
892
|
+
|
|
893
|
+
app.get(routePath(BASE_PATH, '/api/agents/:agentId/session-text'), async (req, res) => {
|
|
894
|
+
const text = await agentManager.getAgentSessionText(req.params.agentId);
|
|
895
|
+
if (text === null) {
|
|
896
|
+
res.status(404).json({ error: 'Agent not found' });
|
|
897
|
+
return;
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
res.type('text/plain');
|
|
901
|
+
res.send(text);
|
|
902
|
+
});
|
|
903
|
+
|
|
904
|
+
app.get(routePath(BASE_PATH, '/api/agents/:agentId/session-view'), async (req, res) => {
|
|
905
|
+
const sessionView = await agentManager.getAgentSessionView(req.params.agentId);
|
|
906
|
+
if (!sessionView) {
|
|
907
|
+
res.status(404).json({ error: 'Agent not found' });
|
|
908
|
+
return;
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
res.json({ session: sessionView });
|
|
912
|
+
});
|
|
913
|
+
|
|
914
|
+
app.get(routePath(BASE_PATH, '/api/agents/:agentId/codex-transcript'), async (req, res) => {
|
|
915
|
+
// Legacy JSONL transcript reader. New App Server Agents use the dedicated
|
|
916
|
+
// structured endpoint below; Terminal Agents stay in their terminal UI.
|
|
917
|
+
const providerSession = agentManager.getAgentProviderSession(req.params.agentId);
|
|
918
|
+
if (!providerSession) {
|
|
919
|
+
res.status(404).json({ error: 'Agent not found' });
|
|
920
|
+
return;
|
|
921
|
+
}
|
|
922
|
+
if (
|
|
923
|
+
providerSession.provider !== 'codex'
|
|
924
|
+
|| providerSession.temporary
|
|
925
|
+
|| !providerSession.sessionId
|
|
926
|
+
|| providerSession.sessionId.startsWith('tmp_uuid')
|
|
927
|
+
) {
|
|
928
|
+
res.json({
|
|
929
|
+
transcript: {
|
|
930
|
+
available: false,
|
|
931
|
+
reason: 'not-codex-provider-session',
|
|
932
|
+
sessionId: providerSession.sessionId || '',
|
|
933
|
+
turns: [],
|
|
934
|
+
},
|
|
935
|
+
});
|
|
936
|
+
return;
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
try {
|
|
940
|
+
const requestedMaxTurns = Number.parseInt(String(req.query.maxTurns || ''), 10);
|
|
941
|
+
const maxTurns = Number.isFinite(requestedMaxTurns)
|
|
942
|
+
? Math.min(MAX_CODEX_TRANSCRIPT_TURNS, Math.max(20, requestedMaxTurns))
|
|
943
|
+
: DEFAULT_CODEX_TRANSCRIPT_MAX_TURNS;
|
|
944
|
+
const transcript = await readCodexTranscript(providerSession.sessionId, {
|
|
945
|
+
maxTurns,
|
|
946
|
+
codexHome: providerSession.codexRuntimeMode === 'app-server'
|
|
947
|
+
? (providerSession.codexAppServerHomePath || providerSession.providerHomePath || '')
|
|
948
|
+
: (providerSession.providerHomePath || ''),
|
|
949
|
+
});
|
|
950
|
+
res.json({ transcript });
|
|
951
|
+
} catch (error) {
|
|
952
|
+
console.error('Failed to read Codex transcript:', error);
|
|
953
|
+
res.status(500).json({ error: error.message || 'Failed to read Codex transcript' });
|
|
954
|
+
}
|
|
955
|
+
});
|
|
956
|
+
|
|
957
|
+
app.get(routePath(BASE_PATH, '/api/agents/:agentId/codex-app-server-transcript'), async (req, res) => {
|
|
958
|
+
try {
|
|
959
|
+
const requestedMaxTurns = Number.parseInt(String(req.query.maxTurns || ''), 10);
|
|
960
|
+
const maxTurns = Number.isFinite(requestedMaxTurns)
|
|
961
|
+
? Math.min(MAX_CODEX_TRANSCRIPT_TURNS, Math.max(20, requestedMaxTurns))
|
|
962
|
+
: DEFAULT_CODEX_TRANSCRIPT_MAX_TURNS;
|
|
963
|
+
const transcript = agentManager.getCodexAppServerTranscript(req.params.agentId, { maxTurns });
|
|
964
|
+
res.json({ transcript });
|
|
965
|
+
} catch (error) {
|
|
966
|
+
const message = error && error.message ? error.message : 'Failed to read Codex App Server transcript';
|
|
967
|
+
res.status(/not using the Codex App Server runtime/i.test(message) ? 409 : 500).json({ error: message });
|
|
968
|
+
}
|
|
969
|
+
});
|
|
970
|
+
|
|
971
|
+
app.get(routePath(BASE_PATH, '/api/agents/:agentId/json-cli-transcript'), async (req, res) => {
|
|
972
|
+
try {
|
|
973
|
+
const transcript = agentManager.getJsonCliTranscript(req.params.agentId, {
|
|
974
|
+
maxTurns: DEFAULT_CODEX_TRANSCRIPT_MAX_TURNS,
|
|
975
|
+
});
|
|
976
|
+
res.json({ transcript });
|
|
977
|
+
} catch (error) {
|
|
978
|
+
const message = error && error.message ? error.message : 'Failed to read JSON CLI transcript';
|
|
979
|
+
res.status(message === 'Agent not found' ? 404 : 409).json({ error: message });
|
|
980
|
+
}
|
|
981
|
+
});
|
|
982
|
+
|
|
983
|
+
app.get(routePath(BASE_PATH, '/api/agents/:agentId/acp-session'), async (req, res) => {
|
|
984
|
+
try {
|
|
985
|
+
res.json({
|
|
986
|
+
session: agentManager.getAcpSession(req.params.agentId, {
|
|
987
|
+
includeUpdates: req.query.includeUpdates === '1',
|
|
988
|
+
includeEntries: req.query.includeEntries !== '0',
|
|
989
|
+
}),
|
|
990
|
+
});
|
|
991
|
+
} catch (error) {
|
|
992
|
+
const message = error && error.message ? error.message : 'Failed to read ACP session';
|
|
993
|
+
res.status(message === 'Agent not found' ? 404 : 409).json({ error: message });
|
|
994
|
+
}
|
|
995
|
+
});
|
|
996
|
+
|
|
997
|
+
app.get(routePath(BASE_PATH, '/api/agents/:agentId/acp-transcript'), async (req, res) => {
|
|
998
|
+
try {
|
|
999
|
+
const requestedMaxTurns = Number.parseInt(String(req.query.maxTurns || ''), 10);
|
|
1000
|
+
const maxTurns = Number.isFinite(requestedMaxTurns)
|
|
1001
|
+
? Math.min(MAX_CODEX_TRANSCRIPT_TURNS, Math.max(20, requestedMaxTurns))
|
|
1002
|
+
: DEFAULT_CODEX_TRANSCRIPT_MAX_TURNS;
|
|
1003
|
+
const requestedRevision = Number.parseInt(String(req.query.sinceRevision || ''), 10);
|
|
1004
|
+
res.json({ transcript: agentManager.getAcpTranscript(req.params.agentId, {
|
|
1005
|
+
maxTurns,
|
|
1006
|
+
...(Number.isFinite(requestedRevision) && requestedRevision >= 0
|
|
1007
|
+
? { sinceRevision: requestedRevision }
|
|
1008
|
+
: {}),
|
|
1009
|
+
}) });
|
|
1010
|
+
} catch (error) {
|
|
1011
|
+
const message = error && error.message ? error.message : 'Failed to read ACP transcript';
|
|
1012
|
+
res.status(message === 'Agent not found' ? 404 : 409).json({ error: message });
|
|
1013
|
+
}
|
|
1014
|
+
});
|
|
1015
|
+
|
|
1016
|
+
app.get(routePath(BASE_PATH, '/api/agents/:agentId/acp-tool-details/:toolCallId'), async (req, res) => {
|
|
1017
|
+
try {
|
|
1018
|
+
res.json(agentManager.getAcpToolDetail(req.params.agentId, req.params.toolCallId));
|
|
1019
|
+
} catch (error) {
|
|
1020
|
+
const message = error && error.message ? error.message : 'Failed to read ACP tool details';
|
|
1021
|
+
const status = message === 'Agent not found' || message === 'ACP tool call not found' ? 404 : 409;
|
|
1022
|
+
res.status(status).json({ error: message });
|
|
1023
|
+
}
|
|
1024
|
+
});
|
|
1025
|
+
|
|
1026
|
+
app.post(routePath(BASE_PATH, '/api/agents/:agentId/acp-terminals/:terminalId/kill'), (req, res) => {
|
|
1027
|
+
try {
|
|
1028
|
+
res.json(agentManager.killAcpTerminal(req.params.agentId, req.params.terminalId));
|
|
1029
|
+
} catch (error) {
|
|
1030
|
+
const message = error && error.message ? error.message : 'Failed to stop ACP terminal';
|
|
1031
|
+
const status = message === 'Agent not found' || message === 'Unknown ACP terminal' ? 404 : 409;
|
|
1032
|
+
res.status(status).json({ error: message });
|
|
1033
|
+
}
|
|
1034
|
+
});
|
|
1035
|
+
|
|
1036
|
+
app.post(routePath(BASE_PATH, '/api/agents/:agentId/acp-terminals/:terminalId/input'), express.json({ limit: '96kb' }), (req, res) => {
|
|
1037
|
+
try {
|
|
1038
|
+
res.json(agentManager.inputAcpTerminal(req.params.agentId, req.params.terminalId, req.body?.input));
|
|
1039
|
+
} catch (error) {
|
|
1040
|
+
const message = error && error.message ? error.message : 'Failed to write ACP terminal input';
|
|
1041
|
+
const status = message === 'Agent not found' || message === 'Unknown ACP terminal' ? 404 : 409;
|
|
1042
|
+
res.status(status).json({ error: message });
|
|
1043
|
+
}
|
|
1044
|
+
});
|
|
1045
|
+
|
|
1046
|
+
app.post(routePath(BASE_PATH, '/api/agents/:agentId/acp-terminals/:terminalId/resize'), express.json(), (req, res) => {
|
|
1047
|
+
try {
|
|
1048
|
+
res.json(agentManager.resizeAcpTerminal(
|
|
1049
|
+
req.params.agentId,
|
|
1050
|
+
req.params.terminalId,
|
|
1051
|
+
req.body?.cols,
|
|
1052
|
+
req.body?.rows,
|
|
1053
|
+
));
|
|
1054
|
+
} catch (error) {
|
|
1055
|
+
const message = error && error.message ? error.message : 'Failed to resize ACP terminal';
|
|
1056
|
+
const status = message === 'Agent not found' || message === 'Unknown ACP terminal' ? 404 : 409;
|
|
1057
|
+
res.status(status).json({ error: message });
|
|
1058
|
+
}
|
|
1059
|
+
});
|
|
1060
|
+
|
|
1061
|
+
app.post(routePath(BASE_PATH, '/api/agents/:agentId/acp-subagents/:sessionId/cancel'), async (req, res) => {
|
|
1062
|
+
try {
|
|
1063
|
+
res.json(await agentManager.cancelAcpSubagent(req.params.agentId, req.params.sessionId));
|
|
1064
|
+
} catch (error) {
|
|
1065
|
+
const message = error && error.message ? error.message : 'Failed to stop ACP subagent';
|
|
1066
|
+
const status = message === 'Agent not found' || message === 'ACP subagent session not found' ? 404 : 409;
|
|
1067
|
+
res.status(status).json({ error: message });
|
|
1068
|
+
}
|
|
1069
|
+
});
|
|
1070
|
+
|
|
1071
|
+
app.post(routePath(BASE_PATH, '/api/agents/:agentId/acp-patches/:toolCallId/decision'), express.json({ limit: '16kb' }), async (req, res) => {
|
|
1072
|
+
try {
|
|
1073
|
+
res.json(await agentManager.decideAcpPatch(
|
|
1074
|
+
req.params.agentId,
|
|
1075
|
+
req.params.toolCallId,
|
|
1076
|
+
req.body?.path,
|
|
1077
|
+
req.body?.decision,
|
|
1078
|
+
));
|
|
1079
|
+
} catch (error) {
|
|
1080
|
+
const message = error && error.message ? error.message : 'Failed to decide ACP patch';
|
|
1081
|
+
const status = Number(error?.statusCode) || (message === 'Agent not found' || message === 'ACP tool call not found' ? 404 : 409);
|
|
1082
|
+
res.status(status).json({ error: message });
|
|
1083
|
+
}
|
|
1084
|
+
});
|
|
1085
|
+
|
|
1086
|
+
app.get(routePath(BASE_PATH, '/api/agents/:agentId/acp-sessions'), async (req, res) => {
|
|
1087
|
+
try {
|
|
1088
|
+
const result = await agentManager.listAcpSessions(req.params.agentId, {
|
|
1089
|
+
cwd: typeof req.query.cwd === 'string' ? req.query.cwd : '',
|
|
1090
|
+
cursor: typeof req.query.cursor === 'string' ? req.query.cursor : '',
|
|
1091
|
+
});
|
|
1092
|
+
res.json(result);
|
|
1093
|
+
} catch (error) {
|
|
1094
|
+
const message = error && error.message ? error.message : 'Failed to list ACP sessions';
|
|
1095
|
+
res.status(message === 'Agent not found' ? 404 : 409).json({ error: message });
|
|
1096
|
+
}
|
|
1097
|
+
});
|
|
1098
|
+
|
|
1099
|
+
app.post(routePath(BASE_PATH, '/api/agents/:agentId/acp-permission'), express.json(), (req, res) => {
|
|
1100
|
+
try {
|
|
1101
|
+
const result = agentManager.respondToAcpPermission(
|
|
1102
|
+
req.params.agentId,
|
|
1103
|
+
req.body?.requestId,
|
|
1104
|
+
req.body?.optionId,
|
|
1105
|
+
req.body?.cancelled === true
|
|
1106
|
+
);
|
|
1107
|
+
res.json(result);
|
|
1108
|
+
} catch (error) {
|
|
1109
|
+
const message = error && error.message ? error.message : 'Failed to respond to ACP permission';
|
|
1110
|
+
res.status(message === 'Agent not found' ? 404 : 409).json({ error: message });
|
|
1111
|
+
}
|
|
1112
|
+
});
|
|
1113
|
+
|
|
1114
|
+
app.post(routePath(BASE_PATH, '/api/agents/:agentId/acp-elicitation'), express.json(), (req, res) => {
|
|
1115
|
+
try {
|
|
1116
|
+
const result = agentManager.respondToAcpElicitation(
|
|
1117
|
+
req.params.agentId,
|
|
1118
|
+
req.body?.requestId,
|
|
1119
|
+
req.body?.action,
|
|
1120
|
+
req.body?.content
|
|
1121
|
+
);
|
|
1122
|
+
res.json(result);
|
|
1123
|
+
} catch (error) {
|
|
1124
|
+
const message = error && error.message ? error.message : 'Failed to respond to ACP input request';
|
|
1125
|
+
res.status(message === 'Agent not found' ? 404 : 409).json({ error: message });
|
|
1126
|
+
}
|
|
1127
|
+
});
|
|
1128
|
+
|
|
1129
|
+
app.post(routePath(BASE_PATH, '/api/agents/:agentId/acp-session/authenticate'), express.json(), async (req, res) => {
|
|
1130
|
+
try {
|
|
1131
|
+
res.json(await agentManager.authenticateAcpAgent(req.params.agentId, req.body?.methodId));
|
|
1132
|
+
} catch (error) {
|
|
1133
|
+
const message = error && error.message ? error.message : 'Failed to authenticate ACP Agent';
|
|
1134
|
+
res.status(message === 'Agent not found' ? 404 : 409).json({ error: message });
|
|
1135
|
+
}
|
|
1136
|
+
});
|
|
1137
|
+
|
|
1138
|
+
app.post(routePath(BASE_PATH, '/api/agents/:agentId/acp-session/fork'), express.json(), async (req, res) => {
|
|
1139
|
+
try {
|
|
1140
|
+
res.json(await agentManager.forkAcpSession(req.params.agentId, req.body || {}));
|
|
1141
|
+
} catch (error) {
|
|
1142
|
+
const message = error && error.message ? error.message : 'Failed to fork ACP session';
|
|
1143
|
+
res.status(message === 'Agent not found' ? 404 : 409).json({ error: message });
|
|
1144
|
+
}
|
|
1145
|
+
});
|
|
1146
|
+
|
|
1147
|
+
app.delete(routePath(BASE_PATH, '/api/agents/:agentId/acp-sessions/:sessionId'), async (req, res) => {
|
|
1148
|
+
try {
|
|
1149
|
+
res.json(await agentManager.deleteAcpSession(req.params.agentId, req.params.sessionId));
|
|
1150
|
+
} catch (error) {
|
|
1151
|
+
const message = error && error.message ? error.message : 'Failed to delete ACP session';
|
|
1152
|
+
res.status(message === 'Agent not found' ? 404 : 409).json({ error: message });
|
|
1153
|
+
}
|
|
1154
|
+
});
|
|
1155
|
+
|
|
1156
|
+
app.post(routePath(BASE_PATH, '/api/agents/:agentId/acp-session/close'), async (req, res) => {
|
|
1157
|
+
try {
|
|
1158
|
+
res.json(await agentManager.closeAcpSession(req.params.agentId));
|
|
1159
|
+
} catch (error) {
|
|
1160
|
+
const message = error && error.message ? error.message : 'Failed to close ACP session';
|
|
1161
|
+
res.status(message === 'Agent not found' ? 404 : 409).json({ error: message });
|
|
1162
|
+
}
|
|
1163
|
+
});
|
|
1164
|
+
|
|
1165
|
+
app.patch(routePath(BASE_PATH, '/api/agents/:agentId/acp-session'), express.json(), async (req, res) => {
|
|
1166
|
+
try {
|
|
1167
|
+
if (typeof req.body?.modeId === 'string') {
|
|
1168
|
+
res.json(await agentManager.setAcpSessionMode(req.params.agentId, req.body.modeId));
|
|
1169
|
+
return;
|
|
1170
|
+
}
|
|
1171
|
+
if (typeof req.body?.configId === 'string' && Object.prototype.hasOwnProperty.call(req.body, 'value')) {
|
|
1172
|
+
res.json(await agentManager.setAcpSessionConfigOption(
|
|
1173
|
+
req.params.agentId,
|
|
1174
|
+
req.body.configId,
|
|
1175
|
+
req.body.value
|
|
1176
|
+
));
|
|
1177
|
+
return;
|
|
1178
|
+
}
|
|
1179
|
+
res.status(400).json({ error: 'ACP modeId or configId/value is required' });
|
|
1180
|
+
} catch (error) {
|
|
1181
|
+
const message = error && error.message ? error.message : 'Failed to update ACP session';
|
|
1182
|
+
res.status(message === 'Agent not found' ? 404 : 409).json({ error: message });
|
|
1183
|
+
}
|
|
1184
|
+
});
|
|
1185
|
+
|
|
1186
|
+
app.get(routePath(BASE_PATH, '/api/agents/:agentId/codex-goal'), async (req, res) => {
|
|
1187
|
+
try {
|
|
1188
|
+
const goal = await agentManager.getCodexAppServerGoal(req.params.agentId);
|
|
1189
|
+
res.json({ goal });
|
|
1190
|
+
} catch (error) {
|
|
1191
|
+
const message = error && error.message ? error.message : 'Failed to read Codex goal';
|
|
1192
|
+
res.status(message === 'Agent not found' ? 404 : 400).json({ error: message });
|
|
1193
|
+
}
|
|
1194
|
+
});
|
|
1195
|
+
|
|
1196
|
+
app.patch(routePath(BASE_PATH, '/api/agents/:agentId/codex-goal'), express.json(), async (req, res) => {
|
|
1197
|
+
try {
|
|
1198
|
+
const body = req.body || {};
|
|
1199
|
+
const goal = await agentManager.setCodexAppServerGoal(req.params.agentId, {
|
|
1200
|
+
objective: typeof body.objective === 'string' ? body.objective : undefined,
|
|
1201
|
+
status: typeof body.status === 'string' ? body.status : undefined,
|
|
1202
|
+
...(Object.prototype.hasOwnProperty.call(body, 'tokenBudget') ? { tokenBudget: body.tokenBudget } : {}),
|
|
1203
|
+
});
|
|
1204
|
+
res.json({ goal });
|
|
1205
|
+
} catch (error) {
|
|
1206
|
+
const message = error && error.message ? error.message : 'Failed to update Codex goal';
|
|
1207
|
+
res.status(message === 'Agent not found' ? 404 : 400).json({ error: message });
|
|
1208
|
+
}
|
|
1209
|
+
});
|
|
1210
|
+
|
|
1211
|
+
app.delete(routePath(BASE_PATH, '/api/agents/:agentId/codex-goal'), async (req, res) => {
|
|
1212
|
+
try {
|
|
1213
|
+
await agentManager.clearCodexAppServerGoal(req.params.agentId);
|
|
1214
|
+
res.json({ goal: null });
|
|
1215
|
+
} catch (error) {
|
|
1216
|
+
const message = error && error.message ? error.message : 'Failed to clear Codex goal';
|
|
1217
|
+
res.status(message === 'Agent not found' ? 404 : 400).json({ error: message });
|
|
1218
|
+
}
|
|
1219
|
+
});
|
|
1220
|
+
|
|
1221
|
+
app.patch(routePath(BASE_PATH, '/api/agents/:agentId'), express.json(), async (req, res) => {
|
|
1222
|
+
const body = req.body || {};
|
|
1223
|
+
const updates = {};
|
|
1224
|
+
|
|
1225
|
+
if (typeof body.customTitle === 'string') {
|
|
1226
|
+
const result = agentManager.renameAgent(req.params.agentId, body.customTitle);
|
|
1227
|
+
if (result.error) {
|
|
1228
|
+
res.status(404).json({ error: result.error });
|
|
1229
|
+
return;
|
|
1230
|
+
}
|
|
1231
|
+
updates.customTitle = result.customTitle;
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
if (typeof body.task === 'string') {
|
|
1235
|
+
const result = agentManager.setAgentTask(req.params.agentId, body.task);
|
|
1236
|
+
if (result.error) {
|
|
1237
|
+
res.status(404).json({ error: result.error });
|
|
1238
|
+
return;
|
|
1239
|
+
}
|
|
1240
|
+
updates.task = result.task;
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
const flagPatch = {};
|
|
1244
|
+
['pinned', 'unread', 'archived'].forEach((flagName) => {
|
|
1245
|
+
if (typeof body[flagName] === 'boolean') {
|
|
1246
|
+
flagPatch[flagName] = body[flagName];
|
|
1247
|
+
}
|
|
1248
|
+
});
|
|
1249
|
+
if (typeof body.readAttentionSeq === 'number' && Number.isFinite(body.readAttentionSeq)) {
|
|
1250
|
+
flagPatch.readAttentionSeq = body.readAttentionSeq;
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
if (flagPatch.archived === true) {
|
|
1254
|
+
const result = await agentManager.archiveAgent(req.params.agentId);
|
|
1255
|
+
if (result.error) {
|
|
1256
|
+
const status = result.error === 'Agent not found' ? 404 : 400;
|
|
1257
|
+
res.status(status).json({ error: result.error });
|
|
1258
|
+
return;
|
|
1259
|
+
}
|
|
1260
|
+
Object.assign(updates, result);
|
|
1261
|
+
delete updates.agentId;
|
|
1262
|
+
delete flagPatch.archived;
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
if (Object.keys(flagPatch).length > 0) {
|
|
1266
|
+
const result = agentManager.updateAgentFlags(req.params.agentId, flagPatch);
|
|
1267
|
+
if (result.error) {
|
|
1268
|
+
const status = result.error === 'Agent not found' ? 404 : 400;
|
|
1269
|
+
res.status(status).json({ error: result.error });
|
|
1270
|
+
return;
|
|
1271
|
+
}
|
|
1272
|
+
Object.assign(updates, result);
|
|
1273
|
+
delete updates.agentId;
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
if (typeof body.launchPermissionMode === 'string') {
|
|
1277
|
+
const result = await agentManager.syncCodexTerminalPermissionMode(req.params.agentId, body.launchPermissionMode);
|
|
1278
|
+
if (result.error) {
|
|
1279
|
+
const status = result.error === 'Agent not found' ? 404 : 400;
|
|
1280
|
+
res.status(status).json({ error: result.error });
|
|
1281
|
+
return;
|
|
1282
|
+
}
|
|
1283
|
+
updates.launchPermissionMode = result.launchPermissionMode;
|
|
1284
|
+
if (result.restarted === true) updates.restarted = true;
|
|
1285
|
+
if (result.restartedAgentId) updates.restartedAgentId = result.restartedAgentId;
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
if (typeof body.agentRuntimeMode === 'string') {
|
|
1289
|
+
const result = await agentManager.restartAgentRuntimeMode(req.params.agentId, body.agentRuntimeMode);
|
|
1290
|
+
if (result.error) {
|
|
1291
|
+
const status = result.error === 'Agent not found' ? 404 : 400;
|
|
1292
|
+
res.status(status).json({ error: result.error });
|
|
1293
|
+
return;
|
|
1294
|
+
}
|
|
1295
|
+
updates.agentRuntimeMode = result.agentRuntimeMode;
|
|
1296
|
+
if (result.restarted === true) updates.restarted = true;
|
|
1297
|
+
if (result.restartedAgentId) updates.restartedAgentId = result.restartedAgentId;
|
|
1298
|
+
if (result.switchFailed === true) updates.switchFailed = true;
|
|
1299
|
+
if (result.warning) updates.warning = result.warning;
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
if (Object.keys(updates).length === 0) {
|
|
1303
|
+
res.status(400).json({ error: 'customTitle, task, pinned, unread, archived, readAttentionSeq, launchPermissionMode, or agentRuntimeMode is required' });
|
|
1304
|
+
return;
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
scheduleBroadcastState();
|
|
1308
|
+
res.json({ agentId: req.params.agentId, ...updates });
|
|
1309
|
+
});
|
|
1310
|
+
|
|
1311
|
+
app.post(routePath(BASE_PATH, '/api/agents/:agentId/reorder'), express.json(), (req, res) => {
|
|
1312
|
+
const result = agentManager.reorderAgent(req.params.agentId, {
|
|
1313
|
+
beforeAgentId: req.body?.beforeAgentId,
|
|
1314
|
+
afterAgentId: req.body?.afterAgentId,
|
|
1315
|
+
});
|
|
1316
|
+
if (result.error) {
|
|
1317
|
+
const status = result.error === 'Agent not found' ? 404 : 400;
|
|
1318
|
+
res.status(status).json({ error: result.error });
|
|
1319
|
+
return;
|
|
1320
|
+
}
|
|
1321
|
+
scheduleBroadcastState();
|
|
1322
|
+
res.json(result);
|
|
1323
|
+
});
|
|
1324
|
+
|
|
1325
|
+
app.post(routePath(BASE_PATH, '/api/agents/:agentId/fork'), express.json(), async (req, res) => {
|
|
1326
|
+
const mode = req.body && typeof req.body.mode === 'string' ? req.body.mode : 'same-worktree';
|
|
1327
|
+
const result = await agentManager.forkAgent(req.params.agentId, mode);
|
|
1328
|
+
if (result.error) {
|
|
1329
|
+
const status = result.error === 'Agent not found' ? 404 : 400;
|
|
1330
|
+
res.status(status).json({ error: result.error });
|
|
1331
|
+
return;
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
broadcastState();
|
|
1335
|
+
res.status(201).json(result);
|
|
1336
|
+
});
|
|
1337
|
+
|
|
1338
|
+
app.post(routePath(BASE_PATH, '/api/projects/delete-worktree'), express.json(), async (req, res) => {
|
|
1339
|
+
const body = req.body || {};
|
|
1340
|
+
const workspace = typeof body.workspace === 'string' ? body.workspace : '';
|
|
1341
|
+
const result = await agentManager.deleteForkWorktreeProject(workspace, { force: body.force === true });
|
|
1342
|
+
if (result.error) {
|
|
1343
|
+
if (result.requiresForce) {
|
|
1344
|
+
res.status(409).json(result);
|
|
1345
|
+
return;
|
|
1346
|
+
}
|
|
1347
|
+
const status = result.error === 'Workspace not found' || result.error === 'Workspace is required' ? 404 : 400;
|
|
1348
|
+
res.status(status).json(result);
|
|
1349
|
+
return;
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
broadcastState();
|
|
1353
|
+
res.json(result);
|
|
1354
|
+
});
|
|
1355
|
+
|
|
1356
|
+
app.post(routePath(BASE_PATH, '/api/codex/sessions/:sessionId/resume'), express.json(), async (req, res) => {
|
|
1357
|
+
await startResumedAgentSession(req, res, 'codex', req.params.sessionId);
|
|
1358
|
+
});
|
|
1359
|
+
|
|
1360
|
+
app.post(routePath(BASE_PATH, '/api/agent-sessions/:provider/:sessionId/resume'), express.json(), async (req, res) => {
|
|
1361
|
+
await startResumedAgentSession(req, res, req.params.provider, req.params.sessionId);
|
|
1362
|
+
});
|
|
1363
|
+
|
|
1364
|
+
const pendingResumeStarts = new Map();
|
|
1365
|
+
|
|
1366
|
+
function resumedAgentStartKey(provider, sessionId, options = {}) {
|
|
1367
|
+
return [
|
|
1368
|
+
provider,
|
|
1369
|
+
options.providerHomeId || 'default',
|
|
1370
|
+
sessionId,
|
|
1371
|
+
options.fork === true ? 'fork' : 'resume',
|
|
1372
|
+
options.asMain === true ? 'main' : 'agent',
|
|
1373
|
+
].join(':');
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
function findResumedAgent(provider, sessionId, providerHomeId = '') {
|
|
1377
|
+
return findActiveAgentClaimingSession(agentManager.getState().agents, provider, { id: sessionId, providerHomeId });
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
function isMainAgentSessionWorkspace(session) {
|
|
1381
|
+
const values = [session && session.cwd, session && session.workspace];
|
|
1382
|
+
return values.some(value => {
|
|
1383
|
+
const normalized = String(value || '').trim().replace(/[\\/]+$/, '');
|
|
1384
|
+
return normalized === '~/.farming' || /(^|[/\\])\.farming$/.test(normalized);
|
|
1385
|
+
});
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
function rememberMainPageAgentSession(provider, sessionId, providerHomeId = '') {
|
|
1389
|
+
const sessionKey = mainPageAgentSessionKey(provider, sessionId, providerHomeId);
|
|
1390
|
+
if (typeof configManager.rememberMainPageSessionKey === 'function') {
|
|
1391
|
+
configManager.rememberMainPageSessionKey(sessionKey, {
|
|
1392
|
+
provider,
|
|
1393
|
+
providerSessionId: sessionId,
|
|
1394
|
+
providerSessionKey: sessionKey,
|
|
1395
|
+
providerHomeId: providerHomeId || 'default',
|
|
1396
|
+
source: 'resume',
|
|
1397
|
+
});
|
|
1398
|
+
return;
|
|
1399
|
+
}
|
|
1400
|
+
const currentKeys = typeof configManager.getMainPageSessionKeys === 'function'
|
|
1401
|
+
? configManager.getMainPageSessionKeys()
|
|
1402
|
+
: (Array.isArray(configManager.getSettings().mainPageSessionKeys) ? configManager.getSettings().mainPageSessionKeys : []);
|
|
1403
|
+
configManager.updateSettings({
|
|
1404
|
+
mainPageSessionKeys: [
|
|
1405
|
+
sessionKey,
|
|
1406
|
+
...currentKeys.filter(key => key !== sessionKey),
|
|
1407
|
+
],
|
|
1408
|
+
});
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
function forgetMainPageAgentSession(provider, sessionId, providerHomeId = '') {
|
|
1412
|
+
const sessionKey = mainPageAgentSessionKey(provider, sessionId, providerHomeId);
|
|
1413
|
+
if (typeof configManager.removeMainPageSessionKey === 'function') {
|
|
1414
|
+
configManager.removeMainPageSessionKey(sessionKey);
|
|
1415
|
+
return;
|
|
1416
|
+
}
|
|
1417
|
+
const currentKeys = typeof configManager.getMainPageSessionKeys === 'function'
|
|
1418
|
+
? configManager.getMainPageSessionKeys()
|
|
1419
|
+
: (Array.isArray(configManager.getSettings().mainPageSessionKeys) ? configManager.getSettings().mainPageSessionKeys : []);
|
|
1420
|
+
if (!currentKeys.includes(sessionKey)) return;
|
|
1421
|
+
configManager.updateSettings({
|
|
1422
|
+
mainPageSessionKeys: currentKeys.filter(key => key !== sessionKey),
|
|
1423
|
+
});
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
async function unarchiveCodexSession(sessionId, session = {}) {
|
|
1427
|
+
const codexResolution = resolveCompatibleCodexExecutable(session.cliVersion || '');
|
|
1428
|
+
if (!codexResolution.compatible) {
|
|
1429
|
+
return {
|
|
1430
|
+
error: codexResolution.error || 'Codex CLI is not compatible with this session',
|
|
1431
|
+
status: 400,
|
|
1432
|
+
};
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
try {
|
|
1436
|
+
await execFileAsync(codexResolution.path || 'codex', ['unarchive', sessionId], {
|
|
1437
|
+
cwd: session.cwd || session.workspace || os.homedir(),
|
|
1438
|
+
env: session.providerHomePath
|
|
1439
|
+
? { ...process.env, CODEX_HOME: session.providerHomePath }
|
|
1440
|
+
: process.env,
|
|
1441
|
+
timeout: 30_000,
|
|
1442
|
+
maxBuffer: 1024 * 1024,
|
|
1443
|
+
});
|
|
1444
|
+
return { unarchived: true };
|
|
1445
|
+
} catch (error) {
|
|
1446
|
+
const message = [
|
|
1447
|
+
error && error.stdout ? String(error.stdout).trim() : '',
|
|
1448
|
+
error && error.stderr ? String(error.stderr).trim() : '',
|
|
1449
|
+
error && error.message ? String(error.message).trim() : '',
|
|
1450
|
+
].filter(Boolean).join('\n') || 'failed to unarchive Codex session';
|
|
1451
|
+
return {
|
|
1452
|
+
error: message,
|
|
1453
|
+
status: 409,
|
|
1454
|
+
};
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
async function resumeAgentSessionById(provider, rawSessionId, options = {}) {
|
|
1459
|
+
const normalizedProvider = normalizeProvider(provider);
|
|
1460
|
+
const sessionId = String(rawSessionId || '').trim();
|
|
1461
|
+
const providerHomeId = typeof options.providerHomeId === 'string' && options.providerHomeId.trim() ? options.providerHomeId.trim() : 'default';
|
|
1462
|
+
if (!normalizedProvider || !isSafeSessionId(sessionId)) {
|
|
1463
|
+
return { error: 'invalid session id', status: 400 };
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
const shouldFork = options.fork === true;
|
|
1467
|
+
const requestedAsMain = options.asMain === true && !shouldFork;
|
|
1468
|
+
const shouldRememberMainPageSession = options.rememberMainPageSession !== false && !shouldFork && !requestedAsMain;
|
|
1469
|
+
const pendingResumeId = resumedAgentStartKey(normalizedProvider, sessionId, {
|
|
1470
|
+
fork: shouldFork,
|
|
1471
|
+
asMain: requestedAsMain,
|
|
1472
|
+
providerHomeId,
|
|
1473
|
+
});
|
|
1474
|
+
if (!shouldFork) {
|
|
1475
|
+
const existingAgent = findResumedAgent(normalizedProvider, sessionId, providerHomeId);
|
|
1476
|
+
if (existingAgent) {
|
|
1477
|
+
if (shouldRememberMainPageSession) rememberMainPageAgentSession(normalizedProvider, sessionId, providerHomeId);
|
|
1478
|
+
return { agentId: existingAgent.id, reused: true };
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
const pendingStart = pendingResumeStarts.get(pendingResumeId);
|
|
1482
|
+
if (pendingStart) {
|
|
1483
|
+
const result = await pendingStart;
|
|
1484
|
+
if (result.error) {
|
|
1485
|
+
return result;
|
|
1486
|
+
}
|
|
1487
|
+
if (shouldRememberMainPageSession) rememberMainPageAgentSession(normalizedProvider, sessionId, providerHomeId);
|
|
1488
|
+
return {
|
|
1489
|
+
agentId: result.agentId,
|
|
1490
|
+
reused: true,
|
|
1491
|
+
pending: true,
|
|
1492
|
+
...(result.claimed ? { claimed: true } : {}),
|
|
1493
|
+
};
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
const startPromise = (async () => {
|
|
1497
|
+
let session = await findAgentSession(normalizedProvider, sessionId, {
|
|
1498
|
+
limit: 1000,
|
|
1499
|
+
providerLimit: 1000,
|
|
1500
|
+
scanLimit: 5000,
|
|
1501
|
+
providerHomeId,
|
|
1502
|
+
providerHomes: configuredProviderHomes(),
|
|
1503
|
+
});
|
|
1504
|
+
if (session && session.archived && !shouldFork) {
|
|
1505
|
+
if (options.allowUnarchiveArchived === true && normalizedProvider === 'codex' && !requestedAsMain) {
|
|
1506
|
+
const unarchiveResult = await unarchiveCodexSession(sessionId, session);
|
|
1507
|
+
if (unarchiveResult.error) {
|
|
1508
|
+
return unarchiveResult;
|
|
1509
|
+
}
|
|
1510
|
+
session = await findAgentSession(normalizedProvider, sessionId, {
|
|
1511
|
+
limit: 1000,
|
|
1512
|
+
providerLimit: 1000,
|
|
1513
|
+
scanLimit: 5000,
|
|
1514
|
+
providerHomeId,
|
|
1515
|
+
providerHomes: configuredProviderHomes(),
|
|
1516
|
+
}) || {
|
|
1517
|
+
...session,
|
|
1518
|
+
archived: false,
|
|
1519
|
+
};
|
|
1520
|
+
} else {
|
|
1521
|
+
forgetMainPageAgentSession(normalizedProvider, sessionId, providerHomeId);
|
|
1522
|
+
return {
|
|
1523
|
+
error: `${session.providerName || normalizedProvider} session is archived. Unarchive it before resuming.`,
|
|
1524
|
+
status: 409,
|
|
1525
|
+
archived: true,
|
|
1526
|
+
};
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
if (!shouldFork && !requestedAsMain) {
|
|
1530
|
+
const claimingAgent = findActiveAgentClaimingSession(agentManager.getState().agents, normalizedProvider, {
|
|
1531
|
+
id: sessionId,
|
|
1532
|
+
providerHomeId,
|
|
1533
|
+
...(session || {}),
|
|
1534
|
+
});
|
|
1535
|
+
if (claimingAgent) {
|
|
1536
|
+
if (shouldRememberMainPageSession) rememberMainPageAgentSession(normalizedProvider, sessionId, providerHomeId);
|
|
1537
|
+
return { agentId: claimingAgent.id, reused: true, claimed: true };
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
const resumeAsMain = requestedAsMain && isMainAgentSessionWorkspace(session);
|
|
1542
|
+
if (requestedAsMain && !resumeAsMain) {
|
|
1543
|
+
return { error: 'session is not a Main Agent session', status: 400 };
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
const workingDirectory = session && (session.cwd || session.workspace) ? (session.cwd || session.workspace) : null;
|
|
1547
|
+
const command = buildAgentSessionResumeCommand(normalizedProvider, sessionId, {
|
|
1548
|
+
fork: shouldFork,
|
|
1549
|
+
cwd: workingDirectory,
|
|
1550
|
+
});
|
|
1551
|
+
|
|
1552
|
+
if (!command) {
|
|
1553
|
+
return { error: 'invalid session id', status: 400 };
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
return new Promise((resolve) => {
|
|
1557
|
+
const resolvedProviderHomeId = session ? (session.providerHomeId || providerHomeId) : providerHomeId;
|
|
1558
|
+
const resumeSource = resumedAgentSource(normalizedProvider, sessionId, resolvedProviderHomeId);
|
|
1559
|
+
const startResult = agentManager.startAgent(command, workingDirectory, (agentId, error) => {
|
|
1560
|
+
if (error) {
|
|
1561
|
+
resolve({ error, status: 400 });
|
|
1562
|
+
return;
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
if (!agentId) {
|
|
1566
|
+
resolve({ error: 'failed to resume agent session', status: 500 });
|
|
1567
|
+
return;
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
resolve({ agentId });
|
|
1571
|
+
}, {
|
|
1572
|
+
wantsMain: resumeAsMain,
|
|
1573
|
+
task: session ? session.title : '',
|
|
1574
|
+
customTitle: typeof options.customTitle === 'string' ? options.customTitle : '',
|
|
1575
|
+
requiredCliVersion: normalizedProvider === 'codex' && session ? session.cliVersion : '',
|
|
1576
|
+
projectWorkspace: session ? (session.workspace || session.cwd || '') : '',
|
|
1577
|
+
source: shouldFork ? resumeSource.replace('-history:', '-history-fork:') : resumeSource,
|
|
1578
|
+
agentRuntimeMode: options.agentRuntimeMode === 'acp' ? 'acp' : 'terminal',
|
|
1579
|
+
acpHistoryMode: options.acpHistoryMode === 'resume' ? 'resume' : 'load',
|
|
1580
|
+
providerHomeId: resolvedProviderHomeId,
|
|
1581
|
+
providerHomePath: session ? (session.providerHomePath || '') : '',
|
|
1582
|
+
autoReadInitialAttention: options.autoReadInitialAttention === true,
|
|
1583
|
+
});
|
|
1584
|
+
Promise.resolve(startResult).catch((error) => {
|
|
1585
|
+
resolve({ error: error.message || 'failed to resume agent session', status: 500 });
|
|
1586
|
+
});
|
|
1587
|
+
});
|
|
1588
|
+
})();
|
|
1589
|
+
pendingResumeStarts.set(pendingResumeId, startPromise);
|
|
1590
|
+
|
|
1591
|
+
const result = await startPromise;
|
|
1592
|
+
if (pendingResumeStarts.get(pendingResumeId) === startPromise) {
|
|
1593
|
+
pendingResumeStarts.delete(pendingResumeId);
|
|
1594
|
+
}
|
|
1595
|
+
|
|
1596
|
+
if (result.error) {
|
|
1597
|
+
return result;
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1600
|
+
if (shouldRememberMainPageSession) rememberMainPageAgentSession(normalizedProvider, sessionId, providerHomeId);
|
|
1601
|
+
if (result.reused) {
|
|
1602
|
+
return {
|
|
1603
|
+
agentId: result.agentId,
|
|
1604
|
+
reused: true,
|
|
1605
|
+
...(result.claimed ? { claimed: true } : {}),
|
|
1606
|
+
};
|
|
1607
|
+
}
|
|
1608
|
+
return { agentId: result.agentId };
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
async function startResumedAgentSession(req, res, provider, rawSessionId) {
|
|
1612
|
+
const shouldFork = req.body && req.body.fork === true;
|
|
1613
|
+
const requestedAsMain = req.body && req.body.asMain === true && !shouldFork;
|
|
1614
|
+
const allowUnarchiveArchived = req.body && req.body.unarchiveArchived === true && !shouldFork && !requestedAsMain;
|
|
1615
|
+
const result = await resumeAgentSessionById(provider, rawSessionId, {
|
|
1616
|
+
fork: shouldFork,
|
|
1617
|
+
asMain: requestedAsMain,
|
|
1618
|
+
allowUnarchiveArchived,
|
|
1619
|
+
providerHomeId: req.body && typeof req.body.providerHomeId === 'string' ? req.body.providerHomeId : '',
|
|
1620
|
+
customTitle: req.body && typeof req.body.customTitle === 'string' ? req.body.customTitle : '',
|
|
1621
|
+
agentRuntimeMode: req.body && req.body.agentRuntimeMode === 'acp' ? 'acp' : 'terminal',
|
|
1622
|
+
acpHistoryMode: req.body && req.body.acpHistoryMode === 'resume' ? 'resume' : 'load',
|
|
1623
|
+
});
|
|
1624
|
+
|
|
1625
|
+
if (result.error) {
|
|
1626
|
+
res.status(result.status || 400).json({ error: result.error });
|
|
1627
|
+
return;
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
broadcastState();
|
|
1631
|
+
if (result.reused) {
|
|
1632
|
+
res.status(200).json({
|
|
1633
|
+
agentId: result.agentId,
|
|
1634
|
+
reused: true,
|
|
1635
|
+
...(result.claimed ? { claimed: true } : {}),
|
|
1636
|
+
...(result.pending ? { pending: true } : {}),
|
|
1637
|
+
});
|
|
1638
|
+
return;
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
res.status(201).json({ agentId: result.agentId });
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1644
|
+
async function autoResumeMainPageAgentSessions() {
|
|
1645
|
+
if (typeof agentManager.whenRecovered === 'function') {
|
|
1646
|
+
await agentManager.whenRecovered();
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
const sessions = mainPageAgentSessionsToAutoResume(configManager.getSettings());
|
|
1650
|
+
if (sessions.length === 0) return;
|
|
1651
|
+
|
|
1652
|
+
let resumedCount = 0;
|
|
1653
|
+
for (const session of sessions) {
|
|
1654
|
+
try {
|
|
1655
|
+
const sessionDetails = await findAgentSession(session.provider, session.sessionId, { limit: 200, providerHomeId: session.providerHomeId || 'default', providerHomes: configuredProviderHomes() });
|
|
1656
|
+
if (!sessionDetails) {
|
|
1657
|
+
console.warn('Dropping stale main-page session from auto-resume:', session.provider, session.sessionId);
|
|
1658
|
+
forgetMainPageAgentSession(session.provider, session.sessionId, session.providerHomeId || 'default');
|
|
1659
|
+
continue;
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
const claimingAgent = findActiveAgentClaimingSession(agentManager.getState().agents, session.provider, {
|
|
1663
|
+
id: session.sessionId,
|
|
1664
|
+
...(sessionDetails || {}),
|
|
1665
|
+
});
|
|
1666
|
+
if (claimingAgent) {
|
|
1667
|
+
continue;
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1670
|
+
const result = await resumeAgentSessionById(session.provider, session.sessionId, {
|
|
1671
|
+
rememberMainPageSession: false,
|
|
1672
|
+
providerHomeId: session.providerHomeId || 'default',
|
|
1673
|
+
autoReadInitialAttention: true,
|
|
1674
|
+
});
|
|
1675
|
+
if (result.error) {
|
|
1676
|
+
const message = String(result.error || '').toLowerCase();
|
|
1677
|
+
if (session.provider === 'qoder' && message.includes('invalid session identifier')) {
|
|
1678
|
+
console.warn('Dropping stale qoder session from auto-resume:', session.provider, session.sessionId, result.error);
|
|
1679
|
+
forgetMainPageAgentSession(session.provider, session.sessionId, session.providerHomeId || 'default');
|
|
1680
|
+
continue;
|
|
1681
|
+
}
|
|
1682
|
+
console.warn('Failed to auto-resume main page agent session:', session.provider, session.sessionId, result.error);
|
|
1683
|
+
} else {
|
|
1684
|
+
resumedCount += 1;
|
|
1685
|
+
}
|
|
1686
|
+
} catch (error) {
|
|
1687
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1688
|
+
console.warn('Failed to auto-resume main page agent session:', session.provider, session.sessionId, message);
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
1691
|
+
|
|
1692
|
+
if (resumedCount > 0) {
|
|
1693
|
+
broadcastState();
|
|
1694
|
+
}
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1697
|
+
app.post(routePath(BASE_PATH, '/api/settings'), express.json(), (req, res) => {
|
|
1698
|
+
configManager.updateSettings(req.body || {});
|
|
1699
|
+
agentSessionsCache.invalidate();
|
|
1700
|
+
res.json({
|
|
1701
|
+
success: true,
|
|
1702
|
+
settings: configManager.getSettings()
|
|
1703
|
+
});
|
|
1704
|
+
broadcastState();
|
|
1705
|
+
});
|
|
1706
|
+
|
|
1707
|
+
app.post(routePath(BASE_PATH, '/api/themes/:themeId/set'), express.json(), (req, res) => {
|
|
1708
|
+
const theme = themeManager.getTheme(req.params.themeId);
|
|
1709
|
+
if (!theme) {
|
|
1710
|
+
res.status(404).json({ error: 'Theme not found' });
|
|
1711
|
+
return;
|
|
1712
|
+
}
|
|
1713
|
+
|
|
1714
|
+
configManager.updateSettings({ theme: req.params.themeId });
|
|
1715
|
+
res.json({ success: true, theme: req.params.themeId });
|
|
1716
|
+
});
|
|
1717
|
+
|
|
1718
|
+
app.get(routePath(BASE_PATH, '/api/themes/:themeId/settings'), (req, res) => {
|
|
1719
|
+
const theme = themeManager.getTheme(req.params.themeId);
|
|
1720
|
+
if (!theme) {
|
|
1721
|
+
res.status(404).json({ error: 'Theme not found' });
|
|
1722
|
+
return;
|
|
1723
|
+
}
|
|
1724
|
+
|
|
1725
|
+
const settings = themeManager.getThemeSettings(req.params.themeId);
|
|
1726
|
+
res.json({ settings });
|
|
1727
|
+
});
|
|
1728
|
+
|
|
1729
|
+
app.post(routePath(BASE_PATH, '/api/themes/:themeId/settings'), express.json(), (req, res) => {
|
|
1730
|
+
const theme = themeManager.getTheme(req.params.themeId);
|
|
1731
|
+
if (!theme) {
|
|
1732
|
+
res.status(404).json({ error: 'Theme not found' });
|
|
1733
|
+
return;
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1736
|
+
const success = themeManager.updateThemeSettings(req.params.themeId, req.body);
|
|
1737
|
+
if (success) {
|
|
1738
|
+
res.json({ success: true, settings: themeManager.getThemeSettings(req.params.themeId) });
|
|
1739
|
+
} else {
|
|
1740
|
+
res.status(500).json({ error: 'Failed to update theme settings' });
|
|
1741
|
+
}
|
|
1742
|
+
});
|
|
1743
|
+
|
|
1744
|
+
app.get(routePath(BASE_PATH, '/api/themes/:themeId'), (req, res) => {
|
|
1745
|
+
const theme = themeManager.getTheme(req.params.themeId);
|
|
1746
|
+
if (!theme) {
|
|
1747
|
+
res.status(404).json({ error: 'Theme not found' });
|
|
1748
|
+
return;
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1751
|
+
const css = themeManager.getThemeCSS(req.params.themeId);
|
|
1752
|
+
res.json({
|
|
1753
|
+
theme,
|
|
1754
|
+
css
|
|
1755
|
+
});
|
|
1756
|
+
});
|
|
1757
|
+
|
|
1758
|
+
wss.on('connection', (ws, req) => {
|
|
1759
|
+
const url = new URL(req.url || '/', `http://${req.headers.host || 'localhost'}`);
|
|
1760
|
+
if (url.pathname !== WS_PATH) {
|
|
1761
|
+
ws.close(1008, 'Invalid path');
|
|
1762
|
+
return;
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1765
|
+
// Verify token for WebSocket connections when auth is enabled.
|
|
1766
|
+
if (authEnabled && !tokenAuth.verifyWebSocket(req)) {
|
|
1767
|
+
ws.close(4001, 'Authentication required');
|
|
1768
|
+
return;
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1771
|
+
console.log('Client connected');
|
|
1772
|
+
|
|
1773
|
+
ws.on('message', (message) => {
|
|
1774
|
+
try {
|
|
1775
|
+
const data = JSON.parse(message);
|
|
1776
|
+
handleMessage(ws, data);
|
|
1777
|
+
} catch (e) {
|
|
1778
|
+
console.error('Failed to parse message:', e);
|
|
1779
|
+
}
|
|
1780
|
+
});
|
|
1781
|
+
|
|
1782
|
+
ws.on('close', () => {
|
|
1783
|
+
clearWorkspaceFileWatch(ws);
|
|
1784
|
+
console.log('Client disconnected');
|
|
1785
|
+
});
|
|
1786
|
+
|
|
1787
|
+
sendState(ws);
|
|
1788
|
+
});
|
|
1789
|
+
|
|
1790
|
+
const MAIN_AGENT_RESTART_COMMANDS = new Set(['codex', 'claude', 'opencode', 'qoder', 'bash', 'zsh']);
|
|
1791
|
+
|
|
1792
|
+
function normalizeMainAgentRestartCommand(command) {
|
|
1793
|
+
const normalized = String(command || '').trim();
|
|
1794
|
+
return MAIN_AGENT_RESTART_COMMANDS.has(normalized) ? normalized : '';
|
|
1795
|
+
}
|
|
1796
|
+
|
|
1797
|
+
function restartMainAgent(ws, command) {
|
|
1798
|
+
const normalizedCommand = normalizeMainAgentRestartCommand(command);
|
|
1799
|
+
if (!normalizedCommand) {
|
|
1800
|
+
ws.send(JSON.stringify({ type: 'error', message: 'Unsupported Main Agent restart command' }));
|
|
1801
|
+
return;
|
|
1802
|
+
}
|
|
1803
|
+
|
|
1804
|
+
void (async () => {
|
|
1805
|
+
try {
|
|
1806
|
+
const state = agentManager.getState();
|
|
1807
|
+
const currentMain = state.agents.find(agent => (
|
|
1808
|
+
agent.id === state.mainAgentId || agent.isMain === true
|
|
1809
|
+
));
|
|
1810
|
+
if (currentMain) {
|
|
1811
|
+
await agentManager.killAgent(currentMain.id);
|
|
1812
|
+
}
|
|
1813
|
+
|
|
1814
|
+
await agentManager.startAgent(normalizedCommand, null, (agentId, error) => {
|
|
1815
|
+
if (error) {
|
|
1816
|
+
ws.send(JSON.stringify({ type: 'error', message: error }));
|
|
1817
|
+
} else if (agentId) {
|
|
1818
|
+
ws.agentId = agentId;
|
|
1819
|
+
broadcastState();
|
|
1820
|
+
ws.send(JSON.stringify({ type: 'agent-started', agentId }));
|
|
1821
|
+
}
|
|
1822
|
+
}, {
|
|
1823
|
+
wantsMain: true
|
|
1824
|
+
});
|
|
1825
|
+
} catch (error) {
|
|
1826
|
+
const message = error instanceof Error ? error.message : 'Failed to restart Main Agent';
|
|
1827
|
+
ws.send(JSON.stringify({ type: 'error', message }));
|
|
1828
|
+
broadcastState();
|
|
1829
|
+
}
|
|
1830
|
+
})();
|
|
1831
|
+
}
|
|
1832
|
+
|
|
1833
|
+
async function sendInputMessage(ws, data) {
|
|
1834
|
+
const targetAgentId = resolveInputTargetAgentId(ws, data);
|
|
1835
|
+
if (!targetAgentId) return;
|
|
1836
|
+
|
|
1837
|
+
const inputParts = inputPartsFromMessage(data);
|
|
1838
|
+
if (inputParts.length === 0) return;
|
|
1839
|
+
await agentManager.sendInput(targetAgentId, inputParts);
|
|
1840
|
+
}
|
|
1841
|
+
|
|
1842
|
+
async function sendComposerInputMessage(ws, data) {
|
|
1843
|
+
const targetAgentId = resolveInputTargetAgentId(ws, data);
|
|
1844
|
+
const message = typeof data.message === 'string' ? data.message : '';
|
|
1845
|
+
const content = [];
|
|
1846
|
+
if (message.trim()) content.push({ type: 'text', text: message });
|
|
1847
|
+
const attachmentsRoot = path.resolve(imageAttachmentsDir());
|
|
1848
|
+
const attachments = Array.isArray(data.attachments) ? data.attachments.slice(0, 8) : [];
|
|
1849
|
+
for (const attachment of attachments) {
|
|
1850
|
+
if (attachment?.kind !== 'image' || typeof attachment.path !== 'string') continue;
|
|
1851
|
+
const filePath = path.resolve(attachment.path);
|
|
1852
|
+
if (!filePath.startsWith(`${attachmentsRoot}${path.sep}`)) continue;
|
|
1853
|
+
const mimeType = typeof attachment.type === 'string' && /^image\/(?:png|jpe?g|gif|webp)$/i.test(attachment.type)
|
|
1854
|
+
? attachment.type.toLowerCase()
|
|
1855
|
+
: '';
|
|
1856
|
+
if (!mimeType) continue;
|
|
1857
|
+
try {
|
|
1858
|
+
const data = await fs.promises.readFile(filePath);
|
|
1859
|
+
if (data.length === 0 || data.length > 12 * 1024 * 1024) continue;
|
|
1860
|
+
content.push({
|
|
1861
|
+
type: 'image',
|
|
1862
|
+
data: data.toString('base64'),
|
|
1863
|
+
mimeType,
|
|
1864
|
+
uri: pathToFileURL(filePath).href,
|
|
1865
|
+
});
|
|
1866
|
+
} catch {
|
|
1867
|
+
// The text fallback already explains failed or unavailable uploads.
|
|
1868
|
+
}
|
|
1869
|
+
}
|
|
1870
|
+
if (!targetAgentId || content.length === 0) return;
|
|
1871
|
+
try {
|
|
1872
|
+
await agentManager.sendComposerMessage(targetAgentId, content);
|
|
1873
|
+
} catch (error) {
|
|
1874
|
+
ws.send(JSON.stringify({
|
|
1875
|
+
type: 'error',
|
|
1876
|
+
message: error && error.message ? error.message : 'Failed to send Composer message',
|
|
1877
|
+
}));
|
|
1878
|
+
}
|
|
1879
|
+
}
|
|
1880
|
+
|
|
1881
|
+
async function respondToAppServerRequest(ws, data) {
|
|
1882
|
+
const targetAgentId = resolveInputTargetAgentId(ws, data);
|
|
1883
|
+
const requestId = typeof data.requestId === 'string' ? data.requestId : '';
|
|
1884
|
+
if (!targetAgentId || !requestId) return;
|
|
1885
|
+
try {
|
|
1886
|
+
agentManager.respondToCodexAppServerRequest(targetAgentId, requestId, data.result, {
|
|
1887
|
+
reject: data.reject === true,
|
|
1888
|
+
reason: typeof data.reason === 'string' ? data.reason : '',
|
|
1889
|
+
});
|
|
1890
|
+
} catch (error) {
|
|
1891
|
+
ws.send(JSON.stringify({
|
|
1892
|
+
type: 'error',
|
|
1893
|
+
message: error && error.message ? error.message : 'Failed to respond to Codex App Server request',
|
|
1894
|
+
}));
|
|
1895
|
+
}
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1898
|
+
function handleMessage(ws, data) {
|
|
1899
|
+
switch (data.type) {
|
|
1900
|
+
case 'start-agent': {
|
|
1901
|
+
const workspace = data.workspace || null;
|
|
1902
|
+
agentManager.startAgent(data.command, workspace, (agentId, error) => {
|
|
1903
|
+
if (error) {
|
|
1904
|
+
ws.send(JSON.stringify({ type: 'error', message: error }));
|
|
1905
|
+
} else if (agentId) {
|
|
1906
|
+
ws.agentId = agentId;
|
|
1907
|
+
broadcastState();
|
|
1908
|
+
ws.send(JSON.stringify({ type: 'agent-started', agentId }));
|
|
1909
|
+
}
|
|
1910
|
+
}, {
|
|
1911
|
+
wantsMain: data.asMain === true,
|
|
1912
|
+
projectWorkspace: typeof data.projectWorkspace === 'string' ? data.projectWorkspace : '',
|
|
1913
|
+
task: typeof data.task === 'string' ? data.task : '',
|
|
1914
|
+
workflowTemplate: typeof data.workflowTemplate === 'string' ? data.workflowTemplate : '',
|
|
1915
|
+
customTitle: typeof data.customTitle === 'string' ? data.customTitle : '',
|
|
1916
|
+
codexApprovalMode: typeof data.codexApprovalMode === 'string' ? data.codexApprovalMode : undefined,
|
|
1917
|
+
codexRuntimeMode: data.codexRuntimeMode === 'app-server' || data.codexRuntimeMode === 'cli'
|
|
1918
|
+
? data.codexRuntimeMode
|
|
1919
|
+
: undefined,
|
|
1920
|
+
agentRuntimeMode: ['json', 'acp'].includes(data.agentRuntimeMode) ? data.agentRuntimeMode : 'terminal',
|
|
1921
|
+
acpHistoryMode: data.acpHistoryMode === 'resume' ? 'resume' : 'load',
|
|
1922
|
+
providerHomeId: typeof data.providerHomeId === 'string' ? data.providerHomeId : '',
|
|
1923
|
+
...(data.dangerouslySkipPermissions === true ? { dangerouslySkipPermissions: true } : {}),
|
|
1924
|
+
});
|
|
1925
|
+
break;
|
|
1926
|
+
}
|
|
1927
|
+
case 'input':
|
|
1928
|
+
{
|
|
1929
|
+
void sendInputMessage(ws, data);
|
|
1930
|
+
}
|
|
1931
|
+
break;
|
|
1932
|
+
|
|
1933
|
+
case 'composer-input':
|
|
1934
|
+
{
|
|
1935
|
+
void sendComposerInputMessage(ws, data);
|
|
1936
|
+
}
|
|
1937
|
+
break;
|
|
1938
|
+
|
|
1939
|
+
case 'app-server-request-response':
|
|
1940
|
+
{
|
|
1941
|
+
void respondToAppServerRequest(ws, data);
|
|
1942
|
+
}
|
|
1943
|
+
break;
|
|
1944
|
+
|
|
1945
|
+
case 'acp-permission-response':
|
|
1946
|
+
try {
|
|
1947
|
+
agentManager.respondToAcpPermission(
|
|
1948
|
+
data.agentId,
|
|
1949
|
+
data.requestId,
|
|
1950
|
+
data.optionId,
|
|
1951
|
+
data.cancelled === true
|
|
1952
|
+
);
|
|
1953
|
+
} catch (error) {
|
|
1954
|
+
ws.send(JSON.stringify({
|
|
1955
|
+
type: 'error',
|
|
1956
|
+
message: error && error.message ? error.message : 'Failed to respond to ACP permission',
|
|
1957
|
+
}));
|
|
1958
|
+
}
|
|
1959
|
+
break;
|
|
1960
|
+
|
|
1961
|
+
case 'interrupt-agent':
|
|
1962
|
+
if (data.agentId) {
|
|
1963
|
+
agentManager.interruptAgent(data.agentId);
|
|
1964
|
+
}
|
|
1965
|
+
break;
|
|
1966
|
+
|
|
1967
|
+
case 'focus-agent':
|
|
1968
|
+
ws.focusedAgentId = data.agentId;
|
|
1969
|
+
if (data.streamScope === 'focused' || data.streamScope === 'all') {
|
|
1970
|
+
ws.streamScope = data.streamScope;
|
|
1971
|
+
}
|
|
1972
|
+
if (data.previewScope === 'none' || data.previewScope === 'focused' || data.previewScope === 'all') {
|
|
1973
|
+
ws.previewScope = data.previewScope;
|
|
1974
|
+
}
|
|
1975
|
+
if (data.refreshState === true) {
|
|
1976
|
+
sendState(ws);
|
|
1977
|
+
}
|
|
1978
|
+
break;
|
|
1979
|
+
|
|
1980
|
+
case 'resize-agent':
|
|
1981
|
+
if (data.agentId && Number.isFinite(data.cols) && Number.isFinite(data.rows)) {
|
|
1982
|
+
agentManager.resizeAgentSession(data.agentId, data.cols, data.rows);
|
|
1983
|
+
}
|
|
1984
|
+
break;
|
|
1985
|
+
|
|
1986
|
+
case 'watch-workspace-files':
|
|
1987
|
+
watchWorkspaceFiles(ws, data);
|
|
1988
|
+
break;
|
|
1989
|
+
|
|
1990
|
+
case 'unwatch-workspace-files':
|
|
1991
|
+
clearWorkspaceFileWatch(ws, data.agentId);
|
|
1992
|
+
break;
|
|
1993
|
+
|
|
1994
|
+
case 'kill-agent':
|
|
1995
|
+
agentManager.killAgent(data.agentId);
|
|
1996
|
+
broadcastState();
|
|
1997
|
+
break;
|
|
1998
|
+
|
|
1999
|
+
case 'restart-main-agent':
|
|
2000
|
+
restartMainAgent(ws, data.command);
|
|
2001
|
+
break;
|
|
2002
|
+
|
|
2003
|
+
default:
|
|
2004
|
+
console.log('Unknown message type:', data.type);
|
|
2005
|
+
}
|
|
2006
|
+
}
|
|
2007
|
+
|
|
2008
|
+
const { resolveInputTargetAgentId } = require('./input-routing');
|
|
2009
|
+
|
|
2010
|
+
function sendWorkspaceFileWatchError(ws, error) {
|
|
2011
|
+
const message = error instanceof WorkspaceFileError ? error.message : 'failed to watch workspace files';
|
|
2012
|
+
ws.send(JSON.stringify({ type: 'error', message }));
|
|
2013
|
+
}
|
|
2014
|
+
|
|
2015
|
+
function clearWorkspaceFileWatch(ws, agentId = null) {
|
|
2016
|
+
const watches = ws.workspaceFileUnsubscribes;
|
|
2017
|
+
if (!watches) return;
|
|
2018
|
+
|
|
2019
|
+
const entries = agentId
|
|
2020
|
+
? [[agentId, watches.get(agentId)]]
|
|
2021
|
+
: Array.from(watches.entries());
|
|
2022
|
+
|
|
2023
|
+
entries.forEach(([watchedAgentId, unsubscribe]) => {
|
|
2024
|
+
if (!unsubscribe) return;
|
|
2025
|
+
watches.delete(watchedAgentId);
|
|
2026
|
+
Promise.resolve(unsubscribe()).catch((error) => {
|
|
2027
|
+
console.error('Failed to clear workspace file watch:', error);
|
|
2028
|
+
});
|
|
2029
|
+
});
|
|
2030
|
+
|
|
2031
|
+
if (watches.size === 0) {
|
|
2032
|
+
ws.workspaceFileUnsubscribes = null;
|
|
2033
|
+
}
|
|
2034
|
+
}
|
|
2035
|
+
|
|
2036
|
+
async function watchWorkspaceFiles(ws, data) {
|
|
2037
|
+
try {
|
|
2038
|
+
if (!data.agentId) {
|
|
2039
|
+
throw new WorkspaceFileError('agentId is required', 400);
|
|
2040
|
+
}
|
|
2041
|
+
if (!ws.workspaceFileUnsubscribes) {
|
|
2042
|
+
ws.workspaceFileUnsubscribes = new Map();
|
|
2043
|
+
}
|
|
2044
|
+
if (ws.workspaceFileUnsubscribes.has(data.agentId)) {
|
|
2045
|
+
ws.send(JSON.stringify({
|
|
2046
|
+
type: 'workspace-file-watch',
|
|
2047
|
+
agentId: data.agentId,
|
|
2048
|
+
watching: true,
|
|
2049
|
+
}));
|
|
2050
|
+
return;
|
|
2051
|
+
}
|
|
2052
|
+
|
|
2053
|
+
const root = agentManager.getAgentWorkspaceRoot(data.agentId);
|
|
2054
|
+
if (!root) {
|
|
2055
|
+
throw new WorkspaceFileError('agent not found', 404);
|
|
2056
|
+
}
|
|
2057
|
+
|
|
2058
|
+
const unsubscribe = await workspaceFileService.subscribe(root, (event) => {
|
|
2059
|
+
if (ws.readyState !== WebSocket.OPEN) return;
|
|
2060
|
+
ws.send(JSON.stringify({
|
|
2061
|
+
type: 'workspace-file-event',
|
|
2062
|
+
event: {
|
|
2063
|
+
agentId: data.agentId,
|
|
2064
|
+
...event,
|
|
2065
|
+
},
|
|
2066
|
+
}));
|
|
2067
|
+
});
|
|
2068
|
+
ws.workspaceFileUnsubscribes.set(data.agentId, unsubscribe);
|
|
2069
|
+
ws.send(JSON.stringify({
|
|
2070
|
+
type: 'workspace-file-watch',
|
|
2071
|
+
agentId: data.agentId,
|
|
2072
|
+
watching: true,
|
|
2073
|
+
}));
|
|
2074
|
+
} catch (error) {
|
|
2075
|
+
sendWorkspaceFileWatchError(ws, error);
|
|
2076
|
+
}
|
|
2077
|
+
}
|
|
2078
|
+
|
|
2079
|
+
function buildStatePayload() {
|
|
2080
|
+
return {
|
|
2081
|
+
...agentManager.getState(),
|
|
2082
|
+
mainPageSessionKeys: typeof configManager.getMainPageSessionKeys === 'function'
|
|
2083
|
+
? configManager.getMainPageSessionKeys()
|
|
2084
|
+
: (Array.isArray(configManager.getSettings().mainPageSessionKeys) ? configManager.getSettings().mainPageSessionKeys : []),
|
|
2085
|
+
};
|
|
2086
|
+
}
|
|
2087
|
+
|
|
2088
|
+
function sendState(ws) {
|
|
2089
|
+
const state = buildStatePayload();
|
|
2090
|
+
ws.send(JSON.stringify({ type: 'state', state }));
|
|
2091
|
+
agentManager.getPreviewPayloads().forEach((preview) => {
|
|
2092
|
+
sendPreview(ws, preview);
|
|
2093
|
+
});
|
|
2094
|
+
}
|
|
2095
|
+
|
|
2096
|
+
function sendPreview(ws, preview) {
|
|
2097
|
+
ws.send(JSON.stringify({
|
|
2098
|
+
type: 'session-preview',
|
|
2099
|
+
preview,
|
|
2100
|
+
}));
|
|
2101
|
+
}
|
|
2102
|
+
|
|
2103
|
+
const STATE_BROADCAST_INTERVAL_MS = 120;
|
|
2104
|
+
const PREVIEW_BROADCAST_INTERVAL_MS = 500;
|
|
2105
|
+
const SESSION_STREAM_BROADCAST_INTERVAL_MS = 33;
|
|
2106
|
+
const MAX_SESSION_STREAM_CLIENT_BUFFERED_AMOUNT = 4 * 1024 * 1024;
|
|
2107
|
+
let stateBroadcastTimer = null;
|
|
2108
|
+
let lastStateBroadcastAt = 0;
|
|
2109
|
+
const pendingPreviewBroadcasts = new Map();
|
|
2110
|
+
const pendingSessionStreams = new Map();
|
|
2111
|
+
let sessionStreamBroadcastTimer = null;
|
|
2112
|
+
let shutdownStarted = false;
|
|
2113
|
+
|
|
2114
|
+
function broadcastState() {
|
|
2115
|
+
lastStateBroadcastAt = Date.now();
|
|
2116
|
+
stateBroadcastTimer = null;
|
|
2117
|
+
const state = buildStatePayload();
|
|
2118
|
+
const message = JSON.stringify({ type: 'state', state });
|
|
2119
|
+
wss.clients.forEach(client => {
|
|
2120
|
+
if (client.readyState === WebSocket.OPEN) {
|
|
2121
|
+
client.send(message);
|
|
2122
|
+
}
|
|
2123
|
+
});
|
|
2124
|
+
}
|
|
2125
|
+
|
|
2126
|
+
function scheduleBroadcastState() {
|
|
2127
|
+
const now = Date.now();
|
|
2128
|
+
const elapsed = now - lastStateBroadcastAt;
|
|
2129
|
+
|
|
2130
|
+
if (elapsed >= STATE_BROADCAST_INTERVAL_MS) {
|
|
2131
|
+
broadcastState();
|
|
2132
|
+
return;
|
|
2133
|
+
}
|
|
2134
|
+
|
|
2135
|
+
if (stateBroadcastTimer) {
|
|
2136
|
+
return;
|
|
2137
|
+
}
|
|
2138
|
+
|
|
2139
|
+
stateBroadcastTimer = setTimeout(() => {
|
|
2140
|
+
broadcastState();
|
|
2141
|
+
}, STATE_BROADCAST_INTERVAL_MS - elapsed);
|
|
2142
|
+
}
|
|
2143
|
+
|
|
2144
|
+
function broadcastSessionPreview(preview) {
|
|
2145
|
+
const message = JSON.stringify({
|
|
2146
|
+
type: 'session-preview',
|
|
2147
|
+
preview,
|
|
2148
|
+
});
|
|
2149
|
+
|
|
2150
|
+
wss.clients.forEach((client) => {
|
|
2151
|
+
const previewAllowed = client.previewScope !== 'none'
|
|
2152
|
+
&& (client.previewScope !== 'focused' || client.focusedAgentId === preview.agentId);
|
|
2153
|
+
if (client.readyState === WebSocket.OPEN && previewAllowed) {
|
|
2154
|
+
client.send(message);
|
|
2155
|
+
}
|
|
2156
|
+
});
|
|
2157
|
+
}
|
|
2158
|
+
|
|
2159
|
+
function schedulePreviewBroadcast(preview) {
|
|
2160
|
+
const agentId = preview && preview.agentId;
|
|
2161
|
+
if (!agentId) {
|
|
2162
|
+
broadcastSessionPreview(preview);
|
|
2163
|
+
return;
|
|
2164
|
+
}
|
|
2165
|
+
|
|
2166
|
+
const now = Date.now();
|
|
2167
|
+
const entry = pendingPreviewBroadcasts.get(agentId) || {
|
|
2168
|
+
lastAt: 0,
|
|
2169
|
+
timer: null,
|
|
2170
|
+
preview: null,
|
|
2171
|
+
};
|
|
2172
|
+
entry.preview = preview;
|
|
2173
|
+
|
|
2174
|
+
const elapsed = now - entry.lastAt;
|
|
2175
|
+
if (elapsed >= PREVIEW_BROADCAST_INTERVAL_MS) {
|
|
2176
|
+
if (entry.timer) {
|
|
2177
|
+
clearTimeout(entry.timer);
|
|
2178
|
+
entry.timer = null;
|
|
2179
|
+
}
|
|
2180
|
+
entry.lastAt = now;
|
|
2181
|
+
const latest = entry.preview;
|
|
2182
|
+
entry.preview = null;
|
|
2183
|
+
pendingPreviewBroadcasts.set(agentId, entry);
|
|
2184
|
+
broadcastSessionPreview(latest);
|
|
2185
|
+
return;
|
|
2186
|
+
}
|
|
2187
|
+
|
|
2188
|
+
if (!entry.timer) {
|
|
2189
|
+
entry.timer = setTimeout(() => {
|
|
2190
|
+
entry.timer = null;
|
|
2191
|
+
entry.lastAt = Date.now();
|
|
2192
|
+
const latest = entry.preview;
|
|
2193
|
+
entry.preview = null;
|
|
2194
|
+
pendingPreviewBroadcasts.set(agentId, entry);
|
|
2195
|
+
if (latest) {
|
|
2196
|
+
broadcastSessionPreview(latest);
|
|
2197
|
+
}
|
|
2198
|
+
}, PREVIEW_BROADCAST_INTERVAL_MS - elapsed);
|
|
2199
|
+
}
|
|
2200
|
+
|
|
2201
|
+
pendingPreviewBroadcasts.set(agentId, entry);
|
|
2202
|
+
}
|
|
2203
|
+
|
|
2204
|
+
agentManager.onUpdate(() => {
|
|
2205
|
+
scheduleBroadcastState();
|
|
2206
|
+
});
|
|
2207
|
+
|
|
2208
|
+
agentManager.on('provider-session-updated', () => {
|
|
2209
|
+
agentSessionsCache.invalidate();
|
|
2210
|
+
scheduleBroadcastState();
|
|
2211
|
+
});
|
|
2212
|
+
|
|
2213
|
+
function sessionStreamKey(stream) {
|
|
2214
|
+
return `${stream.agentId || ''}\0${stream.sessionSource || ''}`;
|
|
2215
|
+
}
|
|
2216
|
+
|
|
2217
|
+
function broadcastSessionStream(stream) {
|
|
2218
|
+
const message = JSON.stringify({
|
|
2219
|
+
type: 'session-output',
|
|
2220
|
+
stream
|
|
2221
|
+
});
|
|
2222
|
+
|
|
2223
|
+
wss.clients.forEach((client) => {
|
|
2224
|
+
if (client.readyState === WebSocket.OPEN) {
|
|
2225
|
+
if (client.streamScope === 'focused' && client.focusedAgentId !== stream.agentId) return;
|
|
2226
|
+
if (client.bufferedAmount > MAX_SESSION_STREAM_CLIENT_BUFFERED_AMOUNT) return;
|
|
2227
|
+
client.send(message);
|
|
2228
|
+
}
|
|
2229
|
+
});
|
|
2230
|
+
}
|
|
2231
|
+
|
|
2232
|
+
function flushSessionStreams() {
|
|
2233
|
+
sessionStreamBroadcastTimer = null;
|
|
2234
|
+
const streams = Array.from(pendingSessionStreams.values());
|
|
2235
|
+
pendingSessionStreams.clear();
|
|
2236
|
+
streams.forEach(broadcastSessionStream);
|
|
2237
|
+
}
|
|
2238
|
+
|
|
2239
|
+
function scheduleSessionStreamBroadcast(stream) {
|
|
2240
|
+
if (!stream || !stream.agentId) return;
|
|
2241
|
+
const key = sessionStreamKey(stream);
|
|
2242
|
+
const existing = pendingSessionStreams.get(key);
|
|
2243
|
+
const data = typeof stream.data === 'string' ? stream.data : String(stream.data || '');
|
|
2244
|
+
const outputSeq = Number.isFinite(stream.outputSeq) ? stream.outputSeq : undefined;
|
|
2245
|
+
|
|
2246
|
+
if (existing) {
|
|
2247
|
+
pendingSessionStreams.set(key, {
|
|
2248
|
+
...stream,
|
|
2249
|
+
data: stream.replace === true ? data : `${existing.data || ''}${data}`,
|
|
2250
|
+
replace: existing.replace || stream.replace === true,
|
|
2251
|
+
outputSeq: outputSeq ?? existing.outputSeq,
|
|
2252
|
+
});
|
|
2253
|
+
} else {
|
|
2254
|
+
pendingSessionStreams.set(key, {
|
|
2255
|
+
...stream,
|
|
2256
|
+
data,
|
|
2257
|
+
replace: stream.replace === true,
|
|
2258
|
+
outputSeq,
|
|
2259
|
+
});
|
|
2260
|
+
}
|
|
2261
|
+
|
|
2262
|
+
if (!sessionStreamBroadcastTimer) {
|
|
2263
|
+
sessionStreamBroadcastTimer = setTimeout(flushSessionStreams, SESSION_STREAM_BROADCAST_INTERVAL_MS);
|
|
2264
|
+
if (typeof sessionStreamBroadcastTimer.unref === 'function') sessionStreamBroadcastTimer.unref();
|
|
2265
|
+
}
|
|
2266
|
+
}
|
|
2267
|
+
|
|
2268
|
+
function clearBroadcastTimers() {
|
|
2269
|
+
if (stateBroadcastTimer) {
|
|
2270
|
+
clearTimeout(stateBroadcastTimer);
|
|
2271
|
+
stateBroadcastTimer = null;
|
|
2272
|
+
}
|
|
2273
|
+
for (const entry of pendingPreviewBroadcasts.values()) {
|
|
2274
|
+
if (entry && entry.timer) {
|
|
2275
|
+
clearTimeout(entry.timer);
|
|
2276
|
+
}
|
|
2277
|
+
}
|
|
2278
|
+
pendingPreviewBroadcasts.clear();
|
|
2279
|
+
if (sessionStreamBroadcastTimer) {
|
|
2280
|
+
clearTimeout(sessionStreamBroadcastTimer);
|
|
2281
|
+
sessionStreamBroadcastTimer = null;
|
|
2282
|
+
}
|
|
2283
|
+
pendingSessionStreams.clear();
|
|
2284
|
+
}
|
|
2285
|
+
|
|
2286
|
+
function closeHttpServer() {
|
|
2287
|
+
return new Promise(resolve => {
|
|
2288
|
+
if (!server.listening) {
|
|
2289
|
+
resolve();
|
|
2290
|
+
return;
|
|
2291
|
+
}
|
|
2292
|
+
server.close(error => {
|
|
2293
|
+
if (error && error.code !== 'ERR_SERVER_NOT_RUNNING') {
|
|
2294
|
+
console.warn('Failed to close HTTP server:', error.message || error);
|
|
2295
|
+
}
|
|
2296
|
+
resolve();
|
|
2297
|
+
});
|
|
2298
|
+
});
|
|
2299
|
+
}
|
|
2300
|
+
|
|
2301
|
+
function closeWebSocketServer() {
|
|
2302
|
+
for (const client of wss.clients) {
|
|
2303
|
+
clearWorkspaceFileWatch(client);
|
|
2304
|
+
try {
|
|
2305
|
+
client.close(1001, 'Farming server shutting down');
|
|
2306
|
+
} catch {
|
|
2307
|
+
// ignore shutdown races
|
|
2308
|
+
}
|
|
2309
|
+
}
|
|
2310
|
+
return new Promise(resolve => {
|
|
2311
|
+
wss.close(() => resolve());
|
|
2312
|
+
setTimeout(resolve, 250).unref?.();
|
|
2313
|
+
});
|
|
2314
|
+
}
|
|
2315
|
+
|
|
2316
|
+
async function shutdownServer(options = {}) {
|
|
2317
|
+
if (shutdownStarted) return;
|
|
2318
|
+
shutdownStarted = true;
|
|
2319
|
+
const preserveTerminalHost = options.preserveTerminalHost !== undefined
|
|
2320
|
+
? options.preserveTerminalHost === true
|
|
2321
|
+
: process.env.FARMING_NATIVE_PTY_HOST_PERSIST !== '0';
|
|
2322
|
+
|
|
2323
|
+
clearBroadcastTimers();
|
|
2324
|
+
tokenAuth.cleanup();
|
|
2325
|
+
await Promise.allSettled([
|
|
2326
|
+
closeWebSocketServer(),
|
|
2327
|
+
workspaceFileService.dispose(),
|
|
2328
|
+
agentManager.dispose({ preserveTerminalHost }),
|
|
2329
|
+
closeHttpServer(),
|
|
2330
|
+
]);
|
|
2331
|
+
|
|
2332
|
+
if (options.exit === true) {
|
|
2333
|
+
process.exit(options.exitCode || 0);
|
|
2334
|
+
}
|
|
2335
|
+
}
|
|
2336
|
+
|
|
2337
|
+
agentManager.onSessionStream((stream) => {
|
|
2338
|
+
scheduleSessionStreamBroadcast(stream);
|
|
2339
|
+
});
|
|
2340
|
+
|
|
2341
|
+
agentManager.onSessionPreview((preview) => {
|
|
2342
|
+
schedulePreviewBroadcast(preview);
|
|
2343
|
+
});
|
|
2344
|
+
|
|
2345
|
+
agentManager.onSystemStats((systemStats) => {
|
|
2346
|
+
const usageSnapshot = agentManager.getAgentUsageSnapshots();
|
|
2347
|
+
const message = JSON.stringify({
|
|
2348
|
+
type: 'system-stats',
|
|
2349
|
+
stats: {
|
|
2350
|
+
...systemStats,
|
|
2351
|
+
ip: getPrimaryLocalIP(),
|
|
2352
|
+
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone
|
|
2353
|
+
},
|
|
2354
|
+
uptime: agentManager.getUptime(),
|
|
2355
|
+
usageRate: {
|
|
2356
|
+
windowMs: usageSnapshot.windowMs,
|
|
2357
|
+
estimatedTokensPerMinute: usageSnapshot.estimatedTokensPerMinute,
|
|
2358
|
+
source: usageSnapshot.source,
|
|
2359
|
+
}
|
|
2360
|
+
});
|
|
2361
|
+
wss.clients.forEach(client => {
|
|
2362
|
+
if (client.readyState === WebSocket.OPEN) {
|
|
2363
|
+
client.send(message);
|
|
2364
|
+
}
|
|
2365
|
+
});
|
|
2366
|
+
});
|
|
2367
|
+
|
|
2368
|
+
let serverStarted = false;
|
|
2369
|
+
let terminalRuntimeCleanupPromise = null;
|
|
2370
|
+
|
|
2371
|
+
function runTerminalRuntimeStartupCleanup() {
|
|
2372
|
+
if (!terminalRuntimeCleanupPromise) {
|
|
2373
|
+
terminalRuntimeCleanupPromise = cleanupTerminalRuntime({ configDir: configManager.farmingDir })
|
|
2374
|
+
.catch(error => {
|
|
2375
|
+
console.warn('Failed to clean terminal runtime leftovers:', error && (error.message || error));
|
|
2376
|
+
return null;
|
|
2377
|
+
});
|
|
2378
|
+
}
|
|
2379
|
+
return terminalRuntimeCleanupPromise;
|
|
2380
|
+
}
|
|
2381
|
+
|
|
2382
|
+
function startServer() {
|
|
2383
|
+
if (serverStarted) return server;
|
|
2384
|
+
serverStarted = true;
|
|
2385
|
+
|
|
2386
|
+
void runTerminalRuntimeStartupCleanup().finally(() => {
|
|
2387
|
+
if (shutdownStarted) return;
|
|
2388
|
+
server.listen(PORT, () => {
|
|
2389
|
+
const token = tokenAuth.getToken();
|
|
2390
|
+
const localIPs = getLocalIPs();
|
|
2391
|
+
const entryPath = BASE_PATH || '/';
|
|
2392
|
+
const entrySuffix = authEnabled ? `${entryPath}?token=${token}` : entryPath;
|
|
2393
|
+
|
|
2394
|
+
console.log('');
|
|
2395
|
+
console.log(' Farming server running on:');
|
|
2396
|
+
console.log('');
|
|
2397
|
+
console.log(` Local: http://localhost:${PORT}${entrySuffix}`);
|
|
2398
|
+
localIPs.forEach(ip => {
|
|
2399
|
+
console.log(` Network: http://${ip}:${PORT}${entrySuffix}`);
|
|
2400
|
+
});
|
|
2401
|
+
console.log('');
|
|
2402
|
+
if (authEnabled) {
|
|
2403
|
+
console.log(` Token: ${token}`);
|
|
2404
|
+
const tokenInfo = tokenAuth.getTokenInfo();
|
|
2405
|
+
if (tokenInfo) {
|
|
2406
|
+
console.log(` Token style: ${tokenInfo.style} (${tokenInfo.source}, ~${tokenInfo.entropyBits} bits)`);
|
|
2407
|
+
}
|
|
2408
|
+
} else {
|
|
2409
|
+
console.log(' Token auth: disabled');
|
|
2410
|
+
}
|
|
2411
|
+
console.log('');
|
|
2412
|
+
setTimeout(warmCodexExecutableVersionCache, 100);
|
|
2413
|
+
void autoResumeMainPageAgentSessions();
|
|
2414
|
+
});
|
|
2415
|
+
});
|
|
2416
|
+
|
|
2417
|
+
process.on('SIGINT', () => {
|
|
2418
|
+
void shutdownServer({ exit: true });
|
|
2419
|
+
});
|
|
2420
|
+
process.on('SIGTERM', () => {
|
|
2421
|
+
void shutdownServer({ exit: true });
|
|
2422
|
+
});
|
|
2423
|
+
|
|
2424
|
+
return server;
|
|
2425
|
+
}
|
|
2426
|
+
|
|
2427
|
+
if (require.main === module) {
|
|
2428
|
+
startServer();
|
|
2429
|
+
}
|
|
2430
|
+
|
|
2431
|
+
module.exports = {
|
|
2432
|
+
app,
|
|
2433
|
+
server,
|
|
2434
|
+
wss,
|
|
2435
|
+
agentManager,
|
|
2436
|
+
appServerApiBridge,
|
|
2437
|
+
workspaceFileService,
|
|
2438
|
+
handleMessage,
|
|
2439
|
+
resolveCliBinDir,
|
|
2440
|
+
resolveInputTargetAgentId,
|
|
2441
|
+
rewriteIndexHtmlForBasePath,
|
|
2442
|
+
appendIndexHtmlAssetToken,
|
|
2443
|
+
startServer,
|
|
2444
|
+
shutdownServer,
|
|
2445
|
+
runTerminalRuntimeStartupCleanup,
|
|
2446
|
+
};
|