zidane 7.2.4 → 8.0.4
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 +34 -6
- package/dist/{acp-D-tVyUjw.js → acp-DQDhi2dj.js} +230 -77
- package/dist/acp-DQDhi2dj.js.map +1 -0
- package/dist/acp-cli.d.ts +1 -1
- package/dist/acp-cli.js +9 -66
- package/dist/acp-cli.js.map +1 -1
- package/dist/acp.d.ts +10 -590
- package/dist/acp.d.ts.map +1 -1
- package/dist/acp.js +3 -3
- package/dist/{agent-D0zZnGbt.js → agent-DSLNmBp0.js} +14401 -13214
- package/dist/agent-DSLNmBp0.js.map +1 -0
- package/dist/agent.d.ts +1 -1
- package/dist/agent.js +3 -2
- package/dist/aliasing-lVFbm81n.js +217 -0
- package/dist/aliasing-lVFbm81n.js.map +1 -0
- package/dist/{anthropic-C2aole0G.js → anthropic-CD00LhUP.js} +1442 -1264
- package/dist/anthropic-CD00LhUP.js.map +1 -0
- package/dist/artifacts-CV2hUKnL.js.map +1 -1
- package/dist/{atomic-write-B7zr1ddF.js → atomic-write-Ijgf2Mhc.js} +13 -3
- package/dist/atomic-write-Ijgf2Mhc.js.map +1 -0
- package/dist/call-tool-dHlbGOIY.js +70 -0
- package/dist/call-tool-dHlbGOIY.js.map +1 -0
- package/dist/chat/pure.d.ts +372 -6
- package/dist/chat/pure.d.ts.map +1 -1
- package/dist/chat/pure.js +35 -244
- package/dist/chat/pure.js.map +1 -1
- package/dist/{approvals-DKaJZYlm.js → chat-JPJJ8uu5.js} +15879 -14631
- package/dist/chat-JPJJ8uu5.js.map +1 -0
- package/dist/chat.d.ts +4 -6
- package/dist/chat.js +23 -12
- package/dist/cli-shared-C-YCaglL.js +159 -0
- package/dist/cli-shared-C-YCaglL.js.map +1 -0
- package/dist/client-core-BQSyDALT.d.ts +2115 -0
- package/dist/client-core-BQSyDALT.d.ts.map +1 -0
- package/dist/codemode-worker.d.ts +1 -0
- package/dist/codemode-worker.js +155 -0
- package/dist/codemode-worker.js.map +1 -0
- package/dist/codemode.d.ts +352 -0
- package/dist/codemode.d.ts.map +1 -0
- package/dist/codemode.js +5 -0
- package/dist/connection-Dr-Js4yZ.d.ts +28 -0
- package/dist/connection-Dr-Js4yZ.d.ts.map +1 -0
- package/dist/connection-loop-gdh22LTI.js +112 -0
- package/dist/connection-loop-gdh22LTI.js.map +1 -0
- package/dist/context-segments-BHKlagL6.js +1016 -0
- package/dist/context-segments-BHKlagL6.js.map +1 -0
- package/dist/{context-transport-T_6AYIvy.js → context-transport-DSMTUsK1.js} +3 -3
- package/dist/{context-transport-T_6AYIvy.js.map → context-transport-DSMTUsK1.js.map} +1 -1
- package/dist/contexts/daytona.d.ts +19 -8
- package/dist/contexts/daytona.d.ts.map +1 -1
- package/dist/contexts/daytona.js +1 -440
- package/dist/contexts/docker.d.ts +36 -7
- package/dist/contexts/docker.d.ts.map +1 -1
- package/dist/contexts/docker.js +1 -456
- package/dist/contexts/e2b.d.ts +68 -13
- package/dist/contexts/e2b.d.ts.map +1 -1
- package/dist/contexts/e2b.js +1 -382
- package/dist/contexts/sandbox.d.ts +1 -1
- package/dist/contexts/sandbox.js +5 -0
- package/dist/contexts/sandbox.js.map +1 -1
- package/dist/{contexts-CRE9pw07.js → contexts-d7UolYot.js} +1059 -524
- package/dist/contexts-d7UolYot.js.map +1 -0
- package/dist/contexts.d.ts +3 -3
- package/dist/contexts.js +1 -1
- package/dist/cred-keys-GpBg71cP.js +105 -0
- package/dist/cred-keys-GpBg71cP.js.map +1 -0
- package/dist/{auth-BG2rVKSt.js → credentials-JttEuRON.js} +55 -206
- package/dist/credentials-JttEuRON.js.map +1 -0
- package/dist/{cursor-iojRXHF9.js → cursor-CdjaQTBW.js} +3 -3
- package/dist/{cursor-iojRXHF9.js.map → cursor-CdjaQTBW.js.map} +1 -1
- package/dist/daemon-cli.d.ts +1 -1
- package/dist/daemon-cli.js +3 -2
- package/dist/daemon-cli.js.map +1 -1
- package/dist/daytona-DG3omipZ.js +513 -0
- package/dist/daytona-DG3omipZ.js.map +1 -0
- package/dist/defaults-Cx3zvDOV.js +23 -0
- package/dist/defaults-Cx3zvDOV.js.map +1 -0
- package/dist/dispatcher-BMlu2BmZ.js +4941 -0
- package/dist/dispatcher-BMlu2BmZ.js.map +1 -0
- package/dist/docker-BZhMMJUp.js +727 -0
- package/dist/docker-BZhMMJUp.js.map +1 -0
- package/dist/e2b-CEVOdRib.js +480 -0
- package/dist/e2b-CEVOdRib.js.map +1 -0
- package/dist/edit-utils-EGosADZq.js +234 -0
- package/dist/edit-utils-EGosADZq.js.map +1 -0
- package/dist/environment-8wsKE2Ln.js +214 -0
- package/dist/environment-8wsKE2Ln.js.map +1 -0
- package/dist/eval-BNcvle_J.js +1595 -0
- package/dist/eval-BNcvle_J.js.map +1 -0
- package/dist/eval.d.ts +2 -2
- package/dist/eval.js +1 -1489
- package/dist/executor-Cx5-8yqi.js +621 -0
- package/dist/executor-Cx5-8yqi.js.map +1 -0
- package/dist/extension-authoring-DyGGmq9V.js +385 -0
- package/dist/extension-authoring-DyGGmq9V.js.map +1 -0
- package/dist/extensions.d.ts +2 -2
- package/dist/extensions.js +3 -3
- package/dist/{fetch-url-DX75gpL0.js → fetch-url-Ct2TjDWt.js} +4 -4
- package/dist/{fetch-url-DX75gpL0.js.map → fetch-url-Ct2TjDWt.js.map} +1 -1
- package/dist/{format-BF745pLL.js → format-C2NQEVUH.js} +39 -2
- package/dist/format-C2NQEVUH.js.map +1 -0
- package/dist/format-bytes-C7tfc3Zm.js +55 -0
- package/dist/format-bytes-C7tfc3Zm.js.map +1 -0
- package/dist/headless-C_rNUuYC.js +960 -0
- package/dist/headless-C_rNUuYC.js.map +1 -0
- package/dist/headless.d.ts +1 -1
- package/dist/headless.js +1 -413
- package/dist/{history-search-C4bk1Wz_.js → history-search-CcYzF8-0.js} +7 -3
- package/dist/history-search-CcYzF8-0.js.map +1 -0
- package/dist/history-search-worker.d.ts +1 -1
- package/dist/history-search-worker.js +1 -1
- package/dist/host-D7NrI03V.js +378 -0
- package/dist/host-D7NrI03V.js.map +1 -0
- package/dist/host-signals-BcP5QKUD.js +118 -0
- package/dist/host-signals-BcP5QKUD.js.map +1 -0
- package/dist/identity-CwfO8K-t.js +26 -0
- package/dist/identity-CwfO8K-t.js.map +1 -0
- package/dist/{index-BgN1T3lk.d.ts → index-BGe1fQT7.d.ts} +10987 -9320
- package/dist/index-BGe1fQT7.d.ts.map +1 -0
- package/dist/index-BVQsIKGn.d.ts +448 -0
- package/dist/index-BVQsIKGn.d.ts.map +1 -0
- package/dist/{index-C6NEd5y_.d.ts → index-CHABJd-L.d.ts} +3386 -502
- package/dist/index-CHABJd-L.d.ts.map +1 -0
- package/dist/{index-C37_ppWv.d.ts → index-D6a7qXpg.d.ts} +2 -3
- package/dist/index-D6a7qXpg.d.ts.map +1 -0
- package/dist/index.d.ts +6 -6
- package/dist/index.js +27 -23
- package/dist/{completion-core-BRo86R_L.js → inline-ghost-BVRQeGoL.js} +50 -2
- package/dist/inline-ghost-BVRQeGoL.js.map +1 -0
- package/dist/ipc/remote.d.ts +313 -0
- package/dist/ipc/remote.d.ts.map +1 -0
- package/dist/ipc/remote.js +4 -0
- package/dist/ipc.d.ts +6 -964
- package/dist/ipc.js +403 -4
- package/dist/ipc.js.map +1 -0
- package/dist/{isolation-Kfoof0MM.d.ts → isolation-D9L650W5.d.ts} +100 -15
- package/dist/isolation-D9L650W5.d.ts.map +1 -0
- package/dist/json-rpc--HXQti1E.js +350 -0
- package/dist/json-rpc--HXQti1E.js.map +1 -0
- package/dist/links-j8rED1sR.js +603 -0
- package/dist/links-j8rED1sR.js.map +1 -0
- package/dist/local-terminal-D8QThwo_.js +252 -0
- package/dist/local-terminal-D8QThwo_.js.map +1 -0
- package/dist/{logger--C1VQjwJ.js → logger-Gtqlayx8.js} +199 -173
- package/dist/logger-Gtqlayx8.js.map +1 -0
- package/dist/logs-view-CWG8aUAG.js.map +1 -1
- package/dist/{mcp-CgNd-f3F.js → mcp-_wecH9Qe.js} +608 -562
- package/dist/mcp-_wecH9Qe.js.map +1 -0
- package/dist/mcp.d.ts +2 -2
- package/dist/mcp.js +2 -2
- package/dist/media-sniff-UKijEaiQ.js.map +1 -1
- package/dist/{messages-2jcOA_5R.js → messages-BqiwcD2Q.js} +1017 -1014
- package/dist/messages-BqiwcD2Q.js.map +1 -0
- package/dist/{oauth-DxHO17JE.js → oauth-Px_pBB9W.js} +5 -5
- package/dist/{oauth-DxHO17JE.js.map → oauth-Px_pBB9W.js.map} +1 -1
- package/dist/{openai-compat-Dy56weK8.js → openai-compat-CFrpH7uq.js} +432 -589
- package/dist/openai-compat-CFrpH7uq.js.map +1 -0
- package/dist/orchestration-BCGmB8Ps.js +511 -0
- package/dist/orchestration-BCGmB8Ps.js.map +1 -0
- package/dist/output/stream-json.d.ts +1 -2
- package/dist/output/stream-json.d.ts.map +1 -1
- package/dist/output/stream-json.js +1 -1
- package/dist/output/terminal.d.ts +1 -2
- package/dist/output/terminal.d.ts.map +1 -1
- package/dist/output/terminal.js +13 -7
- package/dist/output/terminal.js.map +1 -1
- package/dist/{plugins-gbI7SBA6.js → plugins-DS7mahzZ.js} +20 -101
- package/dist/plugins-DS7mahzZ.js.map +1 -0
- package/dist/plugins.d.ts +1 -1
- package/dist/plugins.js +1 -2
- package/dist/prelude-Cxo4t74B.js +517 -0
- package/dist/prelude-Cxo4t74B.js.map +1 -0
- package/dist/presets.d.ts +1 -1
- package/dist/presets.js +1 -2
- package/dist/project-root-B8zADr4o.js +167 -0
- package/dist/project-root-B8zADr4o.js.map +1 -0
- package/dist/project-user-paths-BSgtrRxl.js +58 -0
- package/dist/project-user-paths-BSgtrRxl.js.map +1 -0
- package/dist/{prompts-Pqc3JdW_.d.ts → prompts-DYcvqShf.d.ts} +14 -11
- package/dist/prompts-DYcvqShf.d.ts.map +1 -0
- package/dist/protocol-DnhqqSmE.js +13 -0
- package/dist/protocol-DnhqqSmE.js.map +1 -0
- package/dist/providers/anthropic.d.ts +2 -2
- package/dist/providers/anthropic.js +1 -2
- package/dist/providers/arcee.d.ts +1 -1
- package/dist/providers/arcee.js +1 -1
- package/dist/providers/baseten.d.ts +1 -1
- package/dist/providers/baseten.js +1 -1
- package/dist/providers/cerebras.d.ts +1 -1
- package/dist/providers/cerebras.js +3 -2
- package/dist/providers/cerebras.js.map +1 -1
- package/dist/providers/local.d.ts +1 -1
- package/dist/providers/local.js +1 -1
- package/dist/providers/openai-compat.d.ts +1 -1
- package/dist/providers/openai-compat.js +1 -2
- package/dist/providers/openai.d.ts +1 -1
- package/dist/providers/openai.js +15 -12
- package/dist/providers/openai.js.map +1 -1
- package/dist/providers/openrouter.d.ts +1 -1
- package/dist/providers/openrouter.js +3 -2
- package/dist/providers/openrouter.js.map +1 -1
- package/dist/providers/xai.d.ts +1 -1
- package/dist/providers/xai.js +3 -3
- package/dist/{providers-BVhnFyiY.js → providers-XUR7F4Hi.js} +4 -4
- package/dist/{providers-BVhnFyiY.js.map → providers-XUR7F4Hi.js.map} +1 -1
- package/dist/providers.d.ts +1 -1
- package/dist/providers.js +4 -4
- package/dist/registry-D6ShBWJU.js +5234 -0
- package/dist/registry-D6ShBWJU.js.map +1 -0
- package/dist/remote-hosts-BtA_9ZGp.js +97 -0
- package/dist/remote-hosts-BtA_9ZGp.js.map +1 -0
- package/dist/restate.d.ts +2 -3
- package/dist/restate.d.ts.map +1 -1
- package/dist/restate.js +631 -572
- package/dist/restate.js.map +1 -1
- package/dist/run-summary-CklLiZ4y.js +288 -0
- package/dist/run-summary-CklLiZ4y.js.map +1 -0
- package/dist/{sandbox-BH0WQkXn.d.ts → sandbox-C5dR5DwQ.d.ts} +6 -3
- package/dist/sandbox-C5dR5DwQ.d.ts.map +1 -0
- package/dist/session/sqlite.d.ts +1 -2
- package/dist/session/sqlite.d.ts.map +1 -1
- package/dist/session/sqlite.js +1 -616
- package/dist/session-factory-BUGY5P40.js +1576 -0
- package/dist/session-factory-BUGY5P40.js.map +1 -0
- package/dist/{session-BqRP1hS7.js → session-xh9JXu6i.js} +94 -52
- package/dist/session-xh9JXu6i.js.map +1 -0
- package/dist/session.d.ts +1 -1
- package/dist/session.js +2 -2
- package/dist/settings-keys-Ckf29NeM.js +677 -0
- package/dist/settings-keys-Ckf29NeM.js.map +1 -0
- package/dist/shell-path-Dqaoow2J.js +412 -0
- package/dist/shell-path-Dqaoow2J.js.map +1 -0
- package/dist/{skills-BC3dodWp.js → skills-DKqZJm4X.js} +430 -470
- package/dist/skills-DKqZJm4X.js.map +1 -0
- package/dist/skills.d.ts +2 -2
- package/dist/skills.js +1 -2
- package/dist/source-mPExaUm8.js +381 -0
- package/dist/source-mPExaUm8.js.map +1 -0
- package/dist/sqlite-DV1DQzN2.js +698 -0
- package/dist/sqlite-DV1DQzN2.js.map +1 -0
- package/dist/{src-CkNQKFKr.js → src-BRMYRJib.js} +903 -1121
- package/dist/src-BRMYRJib.js.map +1 -0
- package/dist/{stdio-DPpBDPx5.js → stdio-C6YyoW3I.js} +4 -4
- package/dist/{stdio-DPpBDPx5.js.map → stdio-C6YyoW3I.js.map} +1 -1
- package/dist/{stdio-loader-Bbx19dMZ.js → stdio-loader-C830Vg-Z.js} +4 -4
- package/dist/{stdio-loader-Bbx19dMZ.js.map → stdio-loader-C830Vg-Z.js.map} +1 -1
- package/dist/system-prompt-MNVHatSD.d.ts.map +1 -1
- package/dist/timeout-DEaqslLO.d.ts.map +1 -1
- package/dist/todo-edits-Dn251YUP.js +1790 -0
- package/dist/todo-edits-Dn251YUP.js.map +1 -0
- package/dist/{turn-operations-DrbJlpq9.js → tool-formatters-IQFDrdrs.js} +1456 -2120
- package/dist/tool-formatters-IQFDrdrs.js.map +1 -0
- package/dist/tool-state-Dku6YqAB.js +989 -0
- package/dist/tool-state-Dku6YqAB.js.map +1 -0
- package/dist/tools/fetch-url.d.ts +1 -2
- package/dist/tools/fetch-url.d.ts.map +1 -1
- package/dist/tools/fetch-url.js +1 -2
- package/dist/tools/prompts.d.ts +1 -1
- package/dist/tools/prompts.js +7 -7
- package/dist/tools/prompts.js.map +1 -1
- package/dist/tools/web-search.d.ts +1 -2
- package/dist/tools/web-search.d.ts.map +1 -1
- package/dist/tools/web-search.js +2 -2
- package/dist/tools/web-search.js.map +1 -1
- package/dist/tools-_DnT3AGM.js +578 -0
- package/dist/tools-_DnT3AGM.js.map +1 -0
- package/dist/tools.d.ts +3 -3
- package/dist/tools.js +4 -2
- package/dist/tui.d.ts +534 -517
- package/dist/tui.d.ts.map +1 -1
- package/dist/tui.js +34933 -27242
- package/dist/tui.js.map +1 -1
- package/dist/turn-operations-B8sokwi5.js +333 -0
- package/dist/turn-operations-B8sokwi5.js.map +1 -0
- package/dist/turn-operations-DQX5ZZni.d.ts +3089 -0
- package/dist/turn-operations-DQX5ZZni.d.ts.map +1 -0
- package/dist/types-C7ZFOztE.d.ts +132 -0
- package/dist/types-C7ZFOztE.d.ts.map +1 -0
- package/dist/types-CH5XpIHw.js +114 -0
- package/dist/types-CH5XpIHw.js.map +1 -0
- package/dist/{types-CWgENVLY.js → types-Cu0ZGDu9.js} +10 -6
- package/dist/types-Cu0ZGDu9.js.map +1 -0
- package/dist/types-IqegbJEE.d.ts +624 -0
- package/dist/types-IqegbJEE.d.ts.map +1 -0
- package/dist/types.d.ts +4 -4
- package/dist/types.js +1 -1
- package/dist/url-D8HCl65b.js +1259 -0
- package/dist/url-D8HCl65b.js.map +1 -0
- package/dist/wasm-BVT-j5cl.js +33 -0
- package/dist/wasm-BVT-j5cl.js.map +1 -0
- package/dist/workspace-exec-stream-SV2R9d5S.js +355 -0
- package/dist/workspace-exec-stream-SV2R9d5S.js.map +1 -0
- package/dist/ws-CIHMvlSh.js +952 -0
- package/dist/ws-CIHMvlSh.js.map +1 -0
- package/dist/ws-client-N7NxzvQp.js +230 -0
- package/dist/ws-client-N7NxzvQp.js.map +1 -0
- package/dist/{xai-CazjStOK.js → xai-CvJTeLrI.js} +165 -5
- package/dist/xai-CvJTeLrI.js.map +1 -0
- package/dist/xdg-zweed0-t.js +117 -0
- package/dist/xdg-zweed0-t.js.map +1 -0
- package/docs/ACP.md +52 -0
- package/docs/ARCHITECTURE.md +103 -6
- package/docs/ARTIFACTS.md +8 -4
- package/docs/CHAT.md +165 -14
- package/docs/EXECUTION_CONTEXT.md +8 -0
- package/docs/EXTENSIONS.md +123 -2
- package/docs/HEADLESS.md +2 -0
- package/docs/IPC.md +775 -64
- package/docs/MACHINES.md +456 -0
- package/docs/PANELS.md +3 -0
- package/docs/README.md +4 -0
- package/docs/RESTATE.md +3 -2
- package/docs/RUN_IN_BACKGROUND.md +10 -2
- package/docs/SERVER.md +1085 -0
- package/docs/SKILL.md +6 -1
- package/docs/TERMINAL.md +75 -0
- package/docs/TUI.md +65 -14
- package/package.json +58 -26
- package/scripts/eval.ts +4 -0
- package/dist/acp-D-tVyUjw.js.map +0 -1
- package/dist/agent-D0zZnGbt.js.map +0 -1
- package/dist/anthropic-C2aole0G.js.map +0 -1
- package/dist/approvals-DKaJZYlm.js.map +0 -1
- package/dist/atomic-write-B7zr1ddF.js.map +0 -1
- package/dist/auth-BG2rVKSt.js.map +0 -1
- package/dist/completion-core-BRo86R_L.js.map +0 -1
- package/dist/contexts/daytona.js.map +0 -1
- package/dist/contexts/docker.js.map +0 -1
- package/dist/contexts/e2b.js.map +0 -1
- package/dist/contexts-CRE9pw07.js.map +0 -1
- package/dist/eval.js.map +0 -1
- package/dist/format-BF745pLL.js.map +0 -1
- package/dist/glob-DluQFSw1.js +0 -270
- package/dist/glob-DluQFSw1.js.map +0 -1
- package/dist/headless.js.map +0 -1
- package/dist/history-search-C4bk1Wz_.js.map +0 -1
- package/dist/index-BgN1T3lk.d.ts.map +0 -1
- package/dist/index-C37_ppWv.d.ts.map +0 -1
- package/dist/index-C6NEd5y_.d.ts.map +0 -1
- package/dist/ipc-zVSyo6K1.js +0 -331
- package/dist/ipc-zVSyo6K1.js.map +0 -1
- package/dist/ipc.d.ts.map +0 -1
- package/dist/isolation-Kfoof0MM.d.ts.map +0 -1
- package/dist/json-rpc-rSWMUh9b.js +0 -192
- package/dist/json-rpc-rSWMUh9b.js.map +0 -1
- package/dist/logger--C1VQjwJ.js.map +0 -1
- package/dist/mcp-CgNd-f3F.js.map +0 -1
- package/dist/messages-2jcOA_5R.js.map +0 -1
- package/dist/openai-compat-Dy56weK8.js.map +0 -1
- package/dist/output-cap-C9zukcCj.js +0 -100
- package/dist/output-cap-C9zukcCj.js.map +0 -1
- package/dist/panels-B6gRk4bf.js +0 -789
- package/dist/panels-B6gRk4bf.js.map +0 -1
- package/dist/plugins-gbI7SBA6.js.map +0 -1
- package/dist/prompts-Pqc3JdW_.d.ts.map +0 -1
- package/dist/run-summary-BU_NgwUT.js +0 -269
- package/dist/run-summary-BU_NgwUT.js.map +0 -1
- package/dist/sandbox-BH0WQkXn.d.ts.map +0 -1
- package/dist/session/sqlite.js.map +0 -1
- package/dist/session-BqRP1hS7.js.map +0 -1
- package/dist/shell-path-h5czzllR.js +0 -1549
- package/dist/shell-path-h5czzllR.js.map +0 -1
- package/dist/skills-BC3dodWp.js.map +0 -1
- package/dist/src-CkNQKFKr.js.map +0 -1
- package/dist/todo-edits-UOwgxGKu.js +0 -528
- package/dist/todo-edits-UOwgxGKu.js.map +0 -1
- package/dist/tool-formatters-DoRLnfuD.d.ts +0 -759
- package/dist/tool-formatters-DoRLnfuD.d.ts.map +0 -1
- package/dist/transcript-anchors-BeudMbXx.d.ts +0 -1762
- package/dist/transcript-anchors-BeudMbXx.d.ts.map +0 -1
- package/dist/turn-operations-C8bMBDl_.d.ts +0 -1258
- package/dist/turn-operations-C8bMBDl_.d.ts.map +0 -1
- package/dist/turn-operations-DrbJlpq9.js.map +0 -1
- package/dist/types-CWgENVLY.js.map +0 -1
- package/dist/xai-CazjStOK.js.map +0 -1
|
@@ -0,0 +1,4941 @@
|
|
|
1
|
+
import { Mn as createExtensionBus, Nn as serializedPayloadBytes, Tt as createExtensionHostRuntimeBridge, at as ExtensionInstallError, bt as filterRegistryPlugins, dt as removeInstalledExtension, ft as updateInstalledExtension, mt as discoverAndBuildExtensionRegistry, pt as createExtensionHud, st as installExtensionFromSpec } from "./agent-DSLNmBp0.js";
|
|
2
|
+
import { d as abortableInstallIo, o as installSkillFromSpec } from "./skills-DKqZJm4X.js";
|
|
3
|
+
import { l as normalizeArtifactInput } from "./artifacts-CV2hUKnL.js";
|
|
4
|
+
import { t as projectUserPaths } from "./project-user-paths-BSgtrRxl.js";
|
|
5
|
+
import { i as writeJsonFileAtomic, n as writeFileAtomic, t as OWNER_ONLY } from "./atomic-write-Ijgf2Mhc.js";
|
|
6
|
+
import { c as setExtensionSessionState, o as loadSession, r as deleteExtensionSessionState } from "./session-xh9JXu6i.js";
|
|
7
|
+
import { a as loginMcpServer, c as resolveInlineSuggestion, s as createPathAutocompleteProvider } from "./src-BRMYRJib.js";
|
|
8
|
+
import { a as canSignInRemotely } from "./xai-CvJTeLrI.js";
|
|
9
|
+
import { a as errorResponse, r as createJsonRpcConnection } from "./json-rpc--HXQti1E.js";
|
|
10
|
+
import { M as selectActiveTodos, N as setTodosForRun, c as sessionModelFallbacks, i as describeForkFailure } from "./session-factory-BUGY5P40.js";
|
|
11
|
+
import { $t as MachineErrorCode, X as enabledMcpsWith, Yt as remoteHostsGate, Z as enabledMcpsWithout, in as toMachineName } from "./tool-formatters-IQFDrdrs.js";
|
|
12
|
+
import { c as isMainSession } from "./orchestration-BCGmB8Ps.js";
|
|
13
|
+
import { a as KNOWN_TOKEN, c as looksLikeSecret, o as isSecretName, s as isSecretPathSegment, u as DEFAULT_SETTINGS } from "./url-D8HCl65b.js";
|
|
14
|
+
import { $ as getSessionMeta, B as parseMcpsFile, Ot as readProjects, P as userSystemMdPath, Q as getSessionHeader, R as discoverProjectMcps, X as deriveSessionTitle, Z as eventsFromTurns, _ as enforceSettingsInvariants, dt as sumRunCosts, et as headerFromData, jt as writeProjects, kt as safelistProjectKey, lt as sessionEnvironmentSummary, mt as toolResultText, nt as listSessionMeta, ot as replayTurns, p as SETTINGS_TOGGLES, s as isWindowOnlySetting, tt as lastContextSizeFromTurns } from "./registry-D6ShBWJU.js";
|
|
15
|
+
import { Jt as createChatHost, Qt as createFileMcpCredentialStore, Tr as createMemoryApprovalsStore, X as createExtensionMcpHost, Xn as insideDeclaredRoots, Yn as machineDiscoveryCwd, _n as pinnedSkillsValue, _t as removeMcpServer, bn as toTodosSnapshot, fr as scopeCwd, gn as PINNED_SKILLS_META_KEY, gt as mcpsWritePath, hn as normalizeLaunchOverrides, j as runOAuthLogin, mr as discoverProjectPlugins, nn as applyEnvironmentSettingsChange, ot as resolveConfig, pn as isInside, pr as buildPluginContributions, vn as readPinnedSkills, vt as upsertMcpServer, zn as discoverProjectSkills } from "./chat-JPJJ8uu5.js";
|
|
16
|
+
import { c as searchSessionHistoryIndexAsync, n as buildSessionSearchIndexAsync, u as searchSessionIndexAsync } from "./history-search-CcYzF8-0.js";
|
|
17
|
+
import { c as removeProviderCredential, f as setProviderCredential, g as BUILTIN_PROVIDERS, l as removeStoredCredential, n as credentialsPath, s as readProviderCredentials, t as applyApiKeyEnv } from "./credentials-JttEuRON.js";
|
|
18
|
+
import { r as detectAuth } from "./cli-shared-C-YCaglL.js";
|
|
19
|
+
import { c as settingField, h as METHOD_NOT_FOUND, n as SYNC_CATEGORIES, o as SYSTEM_MD_FIELD } from "./types-CH5XpIHw.js";
|
|
20
|
+
import { C as IPC_BUILD_ID, E as ensureDaemonRunning, O as probeSocket, S as compareVersions, a as createSignalListeners, c as IpcErrorCode, f as REDACTED, i as IpcVersionMismatchError, l as LOAD_EVENTS_MAX_PAGE_TURNS, n as hashValue, o as openIpcClient, p as SESSION_TURNS_MAX_PAGE, r as isRecord, t as isSyncableSetting, x as ZIDANE_VERSION } from "./settings-keys-Ckf29NeM.js";
|
|
21
|
+
import { Buffer } from "node:buffer";
|
|
22
|
+
import { accessSync, constants, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
23
|
+
import { basename, delimiter, dirname, isAbsolute, join, posix, relative, resolve, sep } from "node:path";
|
|
24
|
+
import { homedir, hostname } from "node:os";
|
|
25
|
+
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
26
|
+
import { connect } from "node:net";
|
|
27
|
+
import process$1 from "node:process";
|
|
28
|
+
//#region src/ipc/transports/paced-writer.ts
|
|
29
|
+
/** Largest slice handed to the socket at once. */
|
|
30
|
+
const SLICE_BYTES = 65536;
|
|
31
|
+
function createPacedWriter(stream) {
|
|
32
|
+
const queue = [];
|
|
33
|
+
let queuedBytes = 0;
|
|
34
|
+
let waiting = false;
|
|
35
|
+
const pump = () => {
|
|
36
|
+
waiting = false;
|
|
37
|
+
while (queue.length > 0) {
|
|
38
|
+
if (stream.destroyed || stream.writableEnded) {
|
|
39
|
+
queue.length = 0;
|
|
40
|
+
queuedBytes = 0;
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
const head = queue[0];
|
|
44
|
+
const slice = head.length > SLICE_BYTES ? head.subarray(0, SLICE_BYTES) : head;
|
|
45
|
+
if (slice === head) queue.shift();
|
|
46
|
+
else queue[0] = head.subarray(SLICE_BYTES);
|
|
47
|
+
queuedBytes -= slice.length;
|
|
48
|
+
if (!stream.write(slice)) {
|
|
49
|
+
waiting = true;
|
|
50
|
+
stream.once("drain", pump);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
return {
|
|
56
|
+
write(text) {
|
|
57
|
+
const bytes = Buffer.from(text);
|
|
58
|
+
queue.push(bytes);
|
|
59
|
+
queuedBytes += bytes.length;
|
|
60
|
+
if (!waiting) pump();
|
|
61
|
+
},
|
|
62
|
+
buffered: () => queuedBytes + stream.writableLength
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
//#endregion
|
|
66
|
+
//#region src/ipc/client.ts
|
|
67
|
+
async function connectIpcClient(options) {
|
|
68
|
+
const socket = await new Promise((resolvePromise, reject) => {
|
|
69
|
+
const s = connect(options.socketPath);
|
|
70
|
+
s.once("connect", () => {
|
|
71
|
+
s.off("error", reject);
|
|
72
|
+
resolvePromise(s);
|
|
73
|
+
});
|
|
74
|
+
s.once("error", reject);
|
|
75
|
+
});
|
|
76
|
+
const signals = createSignalListeners();
|
|
77
|
+
let closed = false;
|
|
78
|
+
const writer = createPacedWriter(socket);
|
|
79
|
+
const connection = createJsonRpcConnection({
|
|
80
|
+
input: socket,
|
|
81
|
+
output: socket,
|
|
82
|
+
send: (json) => writer.write(`${json}\n`),
|
|
83
|
+
onRequest: (message) => errorResponse(message.id, METHOD_NOT_FOUND, "Clients do not accept requests."),
|
|
84
|
+
onNotification: signals.onNotification
|
|
85
|
+
});
|
|
86
|
+
connection.start();
|
|
87
|
+
const close = () => {
|
|
88
|
+
if (closed) return;
|
|
89
|
+
closed = true;
|
|
90
|
+
connection.close();
|
|
91
|
+
socket.destroy();
|
|
92
|
+
};
|
|
93
|
+
socket.on("close", () => {
|
|
94
|
+
if (closed) return;
|
|
95
|
+
closed = true;
|
|
96
|
+
connection.close();
|
|
97
|
+
options.onClose?.();
|
|
98
|
+
});
|
|
99
|
+
socket.on("error", () => {});
|
|
100
|
+
const passive = options.drainOnMismatch === false;
|
|
101
|
+
return openIpcClient({
|
|
102
|
+
rpc: connection,
|
|
103
|
+
close,
|
|
104
|
+
abort: () => socket.destroy(),
|
|
105
|
+
where: options.socketPath
|
|
106
|
+
}, signals.listeners, {
|
|
107
|
+
...options.appVersion ? { appVersion: options.appVersion } : {},
|
|
108
|
+
drain: passive ? "stale-only" : "always",
|
|
109
|
+
buildFence: !passive
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* One-call convenience for windows: make sure a daemon is up (spawning if
|
|
114
|
+
* needed), connect, and — when the running daemon speaks an older protocol
|
|
115
|
+
* — drain it and spawn a fresh one before retrying once.
|
|
116
|
+
*/
|
|
117
|
+
async function connectOrSpawnDaemon(options = {}) {
|
|
118
|
+
const socketPath = await ensureDaemonRunning(options);
|
|
119
|
+
try {
|
|
120
|
+
return await connectIpcClient({
|
|
121
|
+
...options,
|
|
122
|
+
socketPath
|
|
123
|
+
});
|
|
124
|
+
} catch (err) {
|
|
125
|
+
if (!(err instanceof IpcVersionMismatchError)) throw err;
|
|
126
|
+
const drainDeadline = Date.now() + 2e3;
|
|
127
|
+
while (await probeSocket(socketPath)) {
|
|
128
|
+
if (Date.now() > drainDeadline) break;
|
|
129
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
130
|
+
}
|
|
131
|
+
const retryPath = await ensureDaemonRunning(options);
|
|
132
|
+
return connectIpcClient({
|
|
133
|
+
...options,
|
|
134
|
+
socketPath: retryPath
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
//#endregion
|
|
139
|
+
//#region src/ipc/daemon/environment.ts
|
|
140
|
+
/**
|
|
141
|
+
* File-backed pending-approvals persistence (`pending-approvals.json` next
|
|
142
|
+
* to `state.json`). Small, rewritten atomically enough for its purpose —
|
|
143
|
+
* the queue holds at most a handful of rows.
|
|
144
|
+
*/
|
|
145
|
+
function createFileApprovalsStore(path) {
|
|
146
|
+
function read() {
|
|
147
|
+
if (!existsSync(path)) return [];
|
|
148
|
+
try {
|
|
149
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
150
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
151
|
+
} catch {
|
|
152
|
+
return [];
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
function write(rows) {
|
|
156
|
+
if (rows.length === 0) {
|
|
157
|
+
try {
|
|
158
|
+
unlinkSync(path);
|
|
159
|
+
} catch {}
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
writeJsonFileAtomic(path, rows, OWNER_ONLY);
|
|
163
|
+
}
|
|
164
|
+
return {
|
|
165
|
+
insert(request) {
|
|
166
|
+
const rows = read().filter((r) => r.id !== request.id);
|
|
167
|
+
rows.push(request);
|
|
168
|
+
write(rows);
|
|
169
|
+
},
|
|
170
|
+
delete(id) {
|
|
171
|
+
write(read().filter((r) => r.id !== id));
|
|
172
|
+
},
|
|
173
|
+
clear() {
|
|
174
|
+
write([]);
|
|
175
|
+
},
|
|
176
|
+
list() {
|
|
177
|
+
return read();
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
/** The daemon's environment: its own config, per-cwd extension discovery, multi-process. */
|
|
182
|
+
function createDaemonEnvironment(options) {
|
|
183
|
+
return createHostEnvironment(options);
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Disk-backed host environment: the daemon's, or (with `shared`) a host
|
|
187
|
+
* sharing the window's process and setup.
|
|
188
|
+
*/
|
|
189
|
+
async function createHostEnvironment(options) {
|
|
190
|
+
const { shared } = options;
|
|
191
|
+
const config = shared?.config ?? await resolveOwnConfig(options.cwd);
|
|
192
|
+
const store = config.store;
|
|
193
|
+
if (!shared && config.paths.projectDir) process.stderr.write(`[zidane-daemon] warning: project-DB mode is on, so this daemon uses ${config.paths.db}. Windows opened in other projects will share this project's sessions. Run a per-project daemon (ZIDANE_DAEMON_SOCKET) to isolate them.\n`);
|
|
194
|
+
const readSettings = () => ({
|
|
195
|
+
...DEFAULT_SETTINGS,
|
|
196
|
+
...config.initialSettings,
|
|
197
|
+
...config.stateStore.load().settings ?? {}
|
|
198
|
+
});
|
|
199
|
+
/**
|
|
200
|
+
* Fields the invariant forced (only ever `false`), so the stored file records
|
|
201
|
+
* them too — otherwise a later read of `state.settings` would resurrect the
|
|
202
|
+
* value the invariant just rejected.
|
|
203
|
+
*/
|
|
204
|
+
const pickInvariantFields = (merged, patch) => {
|
|
205
|
+
const out = {};
|
|
206
|
+
for (const row of SETTINGS_TOGGLES) if (row.requires && merged[row.key] === false && patch[row.key] !== false) out[row.key] = false;
|
|
207
|
+
return out;
|
|
208
|
+
};
|
|
209
|
+
const writeSettings = (patch) => {
|
|
210
|
+
const state = config.stateStore.load();
|
|
211
|
+
const merged = enforceSettingsInvariants({
|
|
212
|
+
...DEFAULT_SETTINGS,
|
|
213
|
+
...config.initialSettings,
|
|
214
|
+
...state.settings ?? {},
|
|
215
|
+
...patch
|
|
216
|
+
});
|
|
217
|
+
const next = {
|
|
218
|
+
...state.settings ?? {},
|
|
219
|
+
...patch,
|
|
220
|
+
...pickInvariantFields(merged, patch)
|
|
221
|
+
};
|
|
222
|
+
config.stateStore.save({
|
|
223
|
+
...state,
|
|
224
|
+
settings: next
|
|
225
|
+
});
|
|
226
|
+
return merged;
|
|
227
|
+
};
|
|
228
|
+
const userDataDir = () => config.paths.userDir;
|
|
229
|
+
const applyAuthEnv = () => {
|
|
230
|
+
const dir = userDataDir();
|
|
231
|
+
process.env.ZIDANE_CREDENTIALS_PATH = credentialsPath(dir);
|
|
232
|
+
applyApiKeyEnv(dir, shared ? config.providers : BUILTIN_PROVIDERS);
|
|
233
|
+
};
|
|
234
|
+
const base = {
|
|
235
|
+
multiProcess: shared ? shared.multiProcess === true : true,
|
|
236
|
+
broadcast: options.broadcast,
|
|
237
|
+
store,
|
|
238
|
+
readSettings,
|
|
239
|
+
writeSettings,
|
|
240
|
+
userDataDir,
|
|
241
|
+
applyAuthEnv,
|
|
242
|
+
mcpCredentialStore: () => createFileMcpCredentialStore(userDataDir()),
|
|
243
|
+
approvalsPersistence: shared ? createMemoryApprovalsStore() : createFileApprovalsStore(join(dirname(config.paths.state), "pending-approvals.json")),
|
|
244
|
+
...options.debugStream !== void 0 ? { debugStream: options.debugStream } : {},
|
|
245
|
+
config
|
|
246
|
+
};
|
|
247
|
+
if (shared) {
|
|
248
|
+
const { resolve, invalidate, forget } = shared.runtimeConfig;
|
|
249
|
+
return {
|
|
250
|
+
...base,
|
|
251
|
+
cacheDir: () => config.paths.cacheDir,
|
|
252
|
+
runtimeConfig: resolve,
|
|
253
|
+
...shared.hostRuntime ? { hostRuntime: shared.hostRuntime } : {},
|
|
254
|
+
...shared.extensionBus ? { extensionBus: shared.extensionBus } : {},
|
|
255
|
+
...shared.instrumentAgent ? { instrumentAgent: shared.instrumentAgent } : {},
|
|
256
|
+
...shared.terminals !== void 0 ? { terminals: shared.terminals } : {},
|
|
257
|
+
invalidateRuntime: () => invalidate?.(),
|
|
258
|
+
forgetRuntime: () => forget?.() ?? (() => void 0)
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
const hud = createExtensionHud();
|
|
262
|
+
hud.subscribe(() => {
|
|
263
|
+
const state = hud.getState();
|
|
264
|
+
options.broadcast({
|
|
265
|
+
kind: "hud:state",
|
|
266
|
+
statuses: Object.fromEntries(Object.entries(state.statuses).map(([name, s]) => [name, {
|
|
267
|
+
text: s.text,
|
|
268
|
+
loader: s.loader
|
|
269
|
+
}])),
|
|
270
|
+
banners: Object.fromEntries(Object.entries(state.banners).map(([name, lines]) => [name, [...lines]]))
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
hud.onNotify((n) => {
|
|
274
|
+
options.broadcast({
|
|
275
|
+
kind: "hud:notify",
|
|
276
|
+
extension: n.extension,
|
|
277
|
+
text: n.text,
|
|
278
|
+
tone: n.tone
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
const extensionBus = createExtensionBus();
|
|
282
|
+
extensionBus.onEmit((envelope) => {
|
|
283
|
+
options.broadcast({
|
|
284
|
+
kind: "extension:signal",
|
|
285
|
+
...envelope
|
|
286
|
+
});
|
|
287
|
+
});
|
|
288
|
+
const RUNTIME_CACHE_MAX = 16;
|
|
289
|
+
const runtimeCache = /* @__PURE__ */ new Map();
|
|
290
|
+
const hostRuntime = createExtensionHostRuntimeBridge();
|
|
291
|
+
const mcpHost = createExtensionMcpHost({
|
|
292
|
+
prefix: config.prefix,
|
|
293
|
+
readSettings,
|
|
294
|
+
credentialStore: () => createFileMcpCredentialStore(userDataDir())
|
|
295
|
+
});
|
|
296
|
+
/**
|
|
297
|
+
* Best-effort release of an evicted entry's extension registry
|
|
298
|
+
* (teardown hooks, HUD writers — `registry.dispose()` is idempotent).
|
|
299
|
+
* Fire-and-forget: eviction must never block or throw on a broken
|
|
300
|
+
* extension teardown, and a rejected resolve has nothing to release.
|
|
301
|
+
*/
|
|
302
|
+
const disposeRuntime = (promise) => {
|
|
303
|
+
promise.then((config) => config.extensions.dispose()).catch(() => void 0);
|
|
304
|
+
};
|
|
305
|
+
const runtimeConfig = (cwd) => {
|
|
306
|
+
const settings = readSettings();
|
|
307
|
+
const key = [
|
|
308
|
+
cwd,
|
|
309
|
+
JSON.stringify(settings.enabledExtensions ?? null),
|
|
310
|
+
String(settings.loadProjectExtensions === true)
|
|
311
|
+
].join("\0");
|
|
312
|
+
const existing = runtimeCache.get(key);
|
|
313
|
+
if (existing) {
|
|
314
|
+
runtimeCache.delete(key);
|
|
315
|
+
runtimeCache.set(key, existing);
|
|
316
|
+
return existing;
|
|
317
|
+
}
|
|
318
|
+
const promise = (async () => {
|
|
319
|
+
const extensionRegistry = await discoverAndBuildExtensionRegistry({
|
|
320
|
+
cwd,
|
|
321
|
+
hud,
|
|
322
|
+
bus: extensionBus,
|
|
323
|
+
hostRuntime,
|
|
324
|
+
mcp: mcpHost,
|
|
325
|
+
prefix: config.prefix,
|
|
326
|
+
...settings.enabledExtensions !== void 0 ? { enabledAllowlist: [...settings.enabledExtensions] } : {},
|
|
327
|
+
trustProjectExtensions: settings.loadProjectExtensions === true,
|
|
328
|
+
readSettings
|
|
329
|
+
});
|
|
330
|
+
return resolveConfig({
|
|
331
|
+
cwd,
|
|
332
|
+
extensionRegistry,
|
|
333
|
+
store: () => store
|
|
334
|
+
});
|
|
335
|
+
})();
|
|
336
|
+
runtimeCache.set(key, promise);
|
|
337
|
+
while (runtimeCache.size > RUNTIME_CACHE_MAX) {
|
|
338
|
+
const oldest = runtimeCache.keys().next().value;
|
|
339
|
+
if (oldest === void 0) break;
|
|
340
|
+
const evicted = runtimeCache.get(oldest);
|
|
341
|
+
runtimeCache.delete(oldest);
|
|
342
|
+
if (evicted) disposeRuntime(evicted);
|
|
343
|
+
}
|
|
344
|
+
promise.catch(() => {
|
|
345
|
+
if (runtimeCache.get(key) === promise) runtimeCache.delete(key);
|
|
346
|
+
});
|
|
347
|
+
return promise;
|
|
348
|
+
};
|
|
349
|
+
return {
|
|
350
|
+
...base,
|
|
351
|
+
runtimeConfig,
|
|
352
|
+
hostRuntime,
|
|
353
|
+
invalidateRuntime: () => {
|
|
354
|
+
for (const entry of runtimeCache.values()) disposeRuntime(entry);
|
|
355
|
+
runtimeCache.clear();
|
|
356
|
+
},
|
|
357
|
+
forgetRuntime: () => {
|
|
358
|
+
const dropped = [...runtimeCache.values()];
|
|
359
|
+
runtimeCache.clear();
|
|
360
|
+
return () => {
|
|
361
|
+
for (const entry of dropped) disposeRuntime(entry);
|
|
362
|
+
};
|
|
363
|
+
},
|
|
364
|
+
extensionBus
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
/** The daemon's own boot config: paths + store + state, from `cwd`. */
|
|
368
|
+
async function resolveOwnConfig(cwd) {
|
|
369
|
+
const { createTuiStore } = await import("./session/sqlite.js");
|
|
370
|
+
return resolveConfig({
|
|
371
|
+
...cwd !== void 0 ? { cwd } : {},
|
|
372
|
+
store: (resolved) => createTuiStore(resolved.db)
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
//#endregion
|
|
376
|
+
//#region src/ipc/daemon/handlers/params.ts
|
|
377
|
+
/**
|
|
378
|
+
* Shape checks for request params: they arrive as untrusted JSON (the
|
|
379
|
+
* `IpcMethods` types are not a runtime guarantee). The dispatcher runs
|
|
380
|
+
* {@link checkParams} on every request and notification before its handler;
|
|
381
|
+
* handlers use the field checks for what they require. A failed check is an
|
|
382
|
+
* {@link InvalidParamsError}, answered as JSON-RPC "invalid params".
|
|
383
|
+
*/
|
|
384
|
+
var InvalidParamsError = class extends Error {
|
|
385
|
+
constructor(message) {
|
|
386
|
+
super(message);
|
|
387
|
+
this.name = "InvalidParamsError";
|
|
388
|
+
}
|
|
389
|
+
};
|
|
390
|
+
/** Longest session id accepted (minted ids are under 64 characters). */
|
|
391
|
+
const MAX_SESSION_ID_LENGTH = 1024;
|
|
392
|
+
/**
|
|
393
|
+
* A session id: a non-empty string that is one plain path segment. Ids name
|
|
394
|
+
* directories on disk (`<data dir>/sessions/<id>/`, removed recursively on
|
|
395
|
+
* delete), so `..`, separators and NUL are refused.
|
|
396
|
+
*/
|
|
397
|
+
function checkSessionId(value, name = "sessionId") {
|
|
398
|
+
if (typeof value !== "string" || value.length === 0 || value.length > MAX_SESSION_ID_LENGTH) throw new InvalidParamsError(`${name} must be a non-empty string of at most ${MAX_SESSION_ID_LENGTH} characters.`);
|
|
399
|
+
if (value === "." || value === ".." || /[/\\\0]/.test(value)) throw new InvalidParamsError(`${name} must not be a path (no "/", "\\", "." or "..").`);
|
|
400
|
+
return value;
|
|
401
|
+
}
|
|
402
|
+
/** The required `sessionId` of `params`. */
|
|
403
|
+
function sessionIdOf(params) {
|
|
404
|
+
return checkSessionId(params?.sessionId);
|
|
405
|
+
}
|
|
406
|
+
/** An optional non-negative integer; `undefined` when absent. */
|
|
407
|
+
function nonNegativeInt(value, name) {
|
|
408
|
+
if (value === void 0) return void 0;
|
|
409
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) throw new InvalidParamsError(`${name} must be a non-negative integer.`);
|
|
410
|
+
return value;
|
|
411
|
+
}
|
|
412
|
+
/** An optional string; `undefined` when absent. */
|
|
413
|
+
function optionalString(value, name) {
|
|
414
|
+
if (value !== void 0 && typeof value !== "string") throw new InvalidParamsError(`${name} must be a string.`);
|
|
415
|
+
return value;
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* What every method's params must be: absent, `null`, or an object (never an
|
|
419
|
+
* array or a scalar), whose `sessionId` / `sessionIds`, when present, name
|
|
420
|
+
* sessions ({@link checkSessionId}).
|
|
421
|
+
*/
|
|
422
|
+
function checkParams(params) {
|
|
423
|
+
if (params === void 0 || params === null) return;
|
|
424
|
+
if (typeof params !== "object" || Array.isArray(params)) throw new InvalidParamsError("params must be an object.");
|
|
425
|
+
const { sessionId, sessionIds } = params;
|
|
426
|
+
if (sessionId !== void 0) checkSessionId(sessionId);
|
|
427
|
+
if (sessionIds !== void 0) {
|
|
428
|
+
if (!Array.isArray(sessionIds)) throw new InvalidParamsError("sessionIds must be an array.");
|
|
429
|
+
for (const id of sessionIds) checkSessionId(id, "sessionIds[]");
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
//#endregion
|
|
433
|
+
//#region src/ipc/daemon/handlers/machine-links.ts
|
|
434
|
+
const GATE_POLL_MS = 2e3;
|
|
435
|
+
const EMPTY = {
|
|
436
|
+
shares: [],
|
|
437
|
+
uses: []
|
|
438
|
+
};
|
|
439
|
+
/** A server's: no links, nothing loaded or resumed. */
|
|
440
|
+
function serverLinkHandlers() {
|
|
441
|
+
const refuse = () => {
|
|
442
|
+
throw new Error("A zidane-server has no machine links: share a computer with it from that computer (zidane machine share, or its Hosts page).");
|
|
443
|
+
};
|
|
444
|
+
return {
|
|
445
|
+
"machines.share.start": refuse,
|
|
446
|
+
"machines.share.stop": refuse,
|
|
447
|
+
"machines.useServer": refuse,
|
|
448
|
+
"machines.links.status": refuse
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
function createMachineLinkHandlers(options) {
|
|
452
|
+
if (options.server) return serverLinkHandlers();
|
|
453
|
+
const { host, env, broadcast } = options;
|
|
454
|
+
let links;
|
|
455
|
+
let gateTimer;
|
|
456
|
+
const gateOn = () => remoteHostsGate.isEnabled(env.readSettings());
|
|
457
|
+
const stop = () => {
|
|
458
|
+
const current = links;
|
|
459
|
+
links = void 0;
|
|
460
|
+
clearInterval(gateTimer);
|
|
461
|
+
gateTimer = void 0;
|
|
462
|
+
current?.then((l) => l.close()).catch(() => void 0);
|
|
463
|
+
broadcast({
|
|
464
|
+
kind: "machine:links",
|
|
465
|
+
links: EMPTY
|
|
466
|
+
});
|
|
467
|
+
};
|
|
468
|
+
const load = () => {
|
|
469
|
+
links ??= import("./links-j8rED1sR.js").then(({ startMachineLinks }) => startMachineLinks({
|
|
470
|
+
dataDir: env.userDataDir(),
|
|
471
|
+
machines: host.machines,
|
|
472
|
+
hostId: options.hostId,
|
|
473
|
+
appVersion: options.appVersion,
|
|
474
|
+
settings: () => env.readSettings(),
|
|
475
|
+
onChange: (status) => broadcast({
|
|
476
|
+
kind: "machine:links",
|
|
477
|
+
links: status
|
|
478
|
+
}),
|
|
479
|
+
...options.pacing
|
|
480
|
+
}));
|
|
481
|
+
gateTimer ??= setInterval(() => {
|
|
482
|
+
if (!gateOn()) stop();
|
|
483
|
+
}, options.pacing?.pollMs ?? GATE_POLL_MS);
|
|
484
|
+
gateTimer.unref?.();
|
|
485
|
+
return links;
|
|
486
|
+
};
|
|
487
|
+
const required = () => {
|
|
488
|
+
if (!gateOn()) throw new Error(`Remote hosts are off${remoteHostsGate.isKilled() ? ` (${remoteHostsGate.killSwitchEnv}=1)` : ""}: machines are part of them.`);
|
|
489
|
+
return load();
|
|
490
|
+
};
|
|
491
|
+
const str = (value, name) => {
|
|
492
|
+
if (typeof value !== "string" || value === "") throw new InvalidParamsError(`"${name}" must be a non-empty string.`);
|
|
493
|
+
return value;
|
|
494
|
+
};
|
|
495
|
+
if (gateOn() && existsSync(join(env.userDataDir(), "machine-links.json"))) load().catch(() => void 0);
|
|
496
|
+
host.machines.onClose(() => {
|
|
497
|
+
const current = links;
|
|
498
|
+
links = void 0;
|
|
499
|
+
clearInterval(gateTimer);
|
|
500
|
+
current?.then((l) => l.close()).catch(() => void 0);
|
|
501
|
+
});
|
|
502
|
+
return {
|
|
503
|
+
"machines.share.start": async (p) => {
|
|
504
|
+
if (!Array.isArray(p.roots)) throw new InvalidParamsError("\"roots\" must be a list of folders.");
|
|
505
|
+
if (host.workspace.confined) for (const root of p.roots) host.workspace.resolveFolder(String(root));
|
|
506
|
+
return (await required()).startShare({
|
|
507
|
+
hostId: str(p.hostId, "hostId"),
|
|
508
|
+
name: str(p.name, "name"),
|
|
509
|
+
roots: p.roots,
|
|
510
|
+
...typeof p.token === "string" ? { token: p.token } : {}
|
|
511
|
+
});
|
|
512
|
+
},
|
|
513
|
+
"machines.share.stop": async (p) => (await required()).stopShare(str(p.hostId, "hostId")),
|
|
514
|
+
"machines.useServer": async (p) => {
|
|
515
|
+
if (typeof p.enabled !== "boolean") throw new InvalidParamsError("\"enabled\" must be a boolean.");
|
|
516
|
+
return (await required()).setUse(str(p.hostId, "hostId"), p.enabled);
|
|
517
|
+
},
|
|
518
|
+
"machines.links.status": async () => gateOn() ? (await load()).status() : EMPTY
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
//#endregion
|
|
522
|
+
//#region src/ipc/daemon/handlers/machines.ts
|
|
523
|
+
/** Folders listed per call at most. */
|
|
524
|
+
const MAX_DIRS = 2e3;
|
|
525
|
+
function createMachineHandlers({ host, broadcast }) {
|
|
526
|
+
return {
|
|
527
|
+
"machines.list": () => host.machines.list(),
|
|
528
|
+
"machines.forget": (p) => {
|
|
529
|
+
if (typeof p.machineId !== "string") throw new InvalidParamsError("machines.forget: \"machineId\" must be a string.");
|
|
530
|
+
const machine = host.machines.get(p.machineId);
|
|
531
|
+
const forgotten = host.machines.forget(p.machineId);
|
|
532
|
+
if (forgotten && machine) broadcast({
|
|
533
|
+
kind: "machine:status",
|
|
534
|
+
machineId: machine.id,
|
|
535
|
+
name: machine.name,
|
|
536
|
+
status: "offline"
|
|
537
|
+
});
|
|
538
|
+
return { forgotten };
|
|
539
|
+
},
|
|
540
|
+
"machines.listDirs": async (p) => {
|
|
541
|
+
if (typeof p.machineId !== "string") throw new InvalidParamsError("machines.listDirs: \"machineId\" must be a string.");
|
|
542
|
+
const machine = host.machines.get(p.machineId);
|
|
543
|
+
const link = host.machines.link(p.machineId);
|
|
544
|
+
if (!machine || !link) throw new Error(`Unknown machine "${p.machineId}".`);
|
|
545
|
+
const roots = machine.roots.map((root) => root.path);
|
|
546
|
+
const asked = p.path === void 0 || p.path === "" ? roots[0] : p.path;
|
|
547
|
+
if (typeof asked !== "string" || !insideDeclaredRoots(machine.roots, asked)) throw new Error(`${String(asked)} is outside ${machine.name}'s roots.`);
|
|
548
|
+
const path = posix.normalize(asked);
|
|
549
|
+
const root = roots.find((r) => insideDeclaredRoots([{ path: r }], path));
|
|
550
|
+
const { entries } = await link.call("fs.list", {
|
|
551
|
+
handle: {
|
|
552
|
+
id: "host-list-dirs",
|
|
553
|
+
cwd: root
|
|
554
|
+
},
|
|
555
|
+
path
|
|
556
|
+
}, { wait: false });
|
|
557
|
+
return {
|
|
558
|
+
path,
|
|
559
|
+
dirs: entries.map((entry) => typeof entry === "string" ? entry : entry.path).filter((name) => name.endsWith("/")).map((name) => posix.join(path, name.slice(0, -1))).sort().slice(0, MAX_DIRS),
|
|
560
|
+
roots
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
//#endregion
|
|
566
|
+
//#region src/ipc/daemon/handlers/transcript-page.ts
|
|
567
|
+
/**
|
|
568
|
+
* A run longer than this many pages is split (by `limit`, so the client's
|
|
569
|
+
* halving retry on a reply too large still converges).
|
|
570
|
+
*/
|
|
571
|
+
const WHOLE_RUN_PAGES = 5;
|
|
572
|
+
function isPagedLoad(p) {
|
|
573
|
+
return p?.before !== void 0 || p?.limit !== void 0;
|
|
574
|
+
}
|
|
575
|
+
/** Per turn: does a run start there (a parent-run user turn carrying no tool result)? */
|
|
576
|
+
function exchangeBoundaries(turns, runs) {
|
|
577
|
+
const childRuns = new Set(runs.filter((run) => (run.depth ?? 0) > 0 || run.parentRunId !== void 0).map((run) => run.id));
|
|
578
|
+
return turns.map((turn) => turn.role === "user" && !(turn.runId !== void 0 && childRuns.has(turn.runId)) && !turn.content.some((block) => block.type === "tool_result"));
|
|
579
|
+
}
|
|
580
|
+
/** Per turn: can a page start there without leaving a tool call's result behind (not a tool-result turn)? */
|
|
581
|
+
function turnCutPoints(turns) {
|
|
582
|
+
return turns.map((turn) => !(turn.role === "user" && turn.content.some((block) => block.type === "tool_result")));
|
|
583
|
+
}
|
|
584
|
+
/**
|
|
585
|
+
* `[from, to)` of the page, and the cursor of the one before it, over the
|
|
586
|
+
* turns' {@link exchangeBoundaries} (run starts) and {@link turnCutPoints}
|
|
587
|
+
* (where a run too long to send whole may be split; run starts when absent).
|
|
588
|
+
*/
|
|
589
|
+
function transcriptPage(boundaries, paging, cuts = boundaries) {
|
|
590
|
+
const before = nonNegativeInt(paging.before, "before");
|
|
591
|
+
const limit = Math.min(Math.max(nonNegativeInt(paging.limit, "limit") ?? 200, 1), LOAD_EVENTS_MAX_PAGE_TURNS);
|
|
592
|
+
const to = Math.min(before ?? boundaries.length, boundaries.length);
|
|
593
|
+
if (to === 0) return {
|
|
594
|
+
from: 0,
|
|
595
|
+
to: 0
|
|
596
|
+
};
|
|
597
|
+
/** Where the run holding turn `end - 1` starts (0 when no run start precedes it). */
|
|
598
|
+
const runStart = (end) => {
|
|
599
|
+
let i = end - 1;
|
|
600
|
+
while (i > 0 && !boundaries[i]) i--;
|
|
601
|
+
return i;
|
|
602
|
+
};
|
|
603
|
+
let from = runStart(to);
|
|
604
|
+
const ceiling = limit * 5;
|
|
605
|
+
if (to - from > ceiling) {
|
|
606
|
+
from = to - ceiling;
|
|
607
|
+
while (from < to - 1 && !cuts[from]) from++;
|
|
608
|
+
} else while (from > 0) {
|
|
609
|
+
const previous = runStart(from);
|
|
610
|
+
if (to - previous > limit) break;
|
|
611
|
+
from = previous;
|
|
612
|
+
}
|
|
613
|
+
return {
|
|
614
|
+
from,
|
|
615
|
+
to,
|
|
616
|
+
...from > 0 ? { cursor: from } : {}
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
//#endregion
|
|
620
|
+
//#region src/ipc/daemon/handlers/artifacts.ts
|
|
621
|
+
/** Wire-supplied limit → sane positive integer capped at 1000, else `undefined` (no limit). */
|
|
622
|
+
function boundedLimit(raw) {
|
|
623
|
+
return Number.isInteger(raw) && raw > 0 ? Math.min(raw, 1e3) : void 0;
|
|
624
|
+
}
|
|
625
|
+
function createArtifactHandlers({ store, broadcast }) {
|
|
626
|
+
return {
|
|
627
|
+
"sessions.artifacts": async (p) => {
|
|
628
|
+
if (!store.listArtifacts) return [];
|
|
629
|
+
return store.listArtifacts({
|
|
630
|
+
sessionId: p.sessionId,
|
|
631
|
+
...typeof p.kind === "string" ? { kind: p.kind } : {},
|
|
632
|
+
...boundedLimit(p.limit) !== void 0 ? { limit: boundedLimit(p.limit) } : {}
|
|
633
|
+
});
|
|
634
|
+
},
|
|
635
|
+
"artifacts.list": async (p) => {
|
|
636
|
+
if (!store.listArtifacts) return [];
|
|
637
|
+
return store.listArtifacts({
|
|
638
|
+
...p.projectRoot === null || typeof p.projectRoot === "string" ? { projectRoot: p.projectRoot } : {},
|
|
639
|
+
...typeof p.kind === "string" ? { kind: p.kind } : {},
|
|
640
|
+
...typeof p.uri === "string" ? { uri: p.uri } : {},
|
|
641
|
+
...typeof p.since === "number" && Number.isFinite(p.since) ? { since: p.since } : {},
|
|
642
|
+
...boundedLimit(p.limit) !== void 0 ? { limit: boundedLimit(p.limit) } : {}
|
|
643
|
+
});
|
|
644
|
+
},
|
|
645
|
+
"artifacts.delete": async (p) => {
|
|
646
|
+
if (!store.deleteArtifact) return false;
|
|
647
|
+
await store.deleteArtifact(p.sessionId, p.artifactId);
|
|
648
|
+
broadcast({
|
|
649
|
+
kind: "artifact:changed",
|
|
650
|
+
sessionId: p.sessionId,
|
|
651
|
+
op: "delete",
|
|
652
|
+
artifactId: p.artifactId
|
|
653
|
+
});
|
|
654
|
+
return true;
|
|
655
|
+
},
|
|
656
|
+
"artifacts.upsert": async (p) => {
|
|
657
|
+
if (!store.upsertArtifact) return null;
|
|
658
|
+
const normalized = normalizeArtifactInput(p.artifact);
|
|
659
|
+
if (!normalized || typeof p.artifact?.sessionId !== "string" || typeof p.artifact?.id !== "string") return null;
|
|
660
|
+
const now = Date.now();
|
|
661
|
+
const stored = await store.upsertArtifact({
|
|
662
|
+
...normalized,
|
|
663
|
+
id: p.artifact.id,
|
|
664
|
+
sessionId: p.artifact.sessionId,
|
|
665
|
+
...typeof p.artifact.runId === "string" ? { runId: p.artifact.runId } : {},
|
|
666
|
+
...typeof p.artifact.turnId === "string" ? { turnId: p.artifact.turnId } : {},
|
|
667
|
+
createdAt: typeof p.artifact.createdAt === "number" ? p.artifact.createdAt : now,
|
|
668
|
+
updatedAt: now
|
|
669
|
+
});
|
|
670
|
+
if (stored) broadcast({
|
|
671
|
+
kind: "artifact:changed",
|
|
672
|
+
sessionId: stored.sessionId,
|
|
673
|
+
op: "upsert",
|
|
674
|
+
artifactId: stored.id,
|
|
675
|
+
artifact: stored
|
|
676
|
+
});
|
|
677
|
+
return stored;
|
|
678
|
+
}
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
//#endregion
|
|
682
|
+
//#region src/ipc/daemon/handlers/auth.ts
|
|
683
|
+
/** Longest API key accepted (keys are well under 1 KiB; this bounds a mistaken paste). */
|
|
684
|
+
const MAX_KEY_LENGTH = 8192;
|
|
685
|
+
/** The host's providers and their credentials: lookups, statuses, and applying a change. */
|
|
686
|
+
function createProviderAuth({ host, env }) {
|
|
687
|
+
const registry = () => env.config.providers;
|
|
688
|
+
const dataDir = () => env.userDataDir();
|
|
689
|
+
const descriptorOf = (provider) => {
|
|
690
|
+
const descriptor = typeof provider === "string" ? registry()[provider] : void 0;
|
|
691
|
+
if (!descriptor) throw new Error(`Unknown provider "${String(provider)}".`);
|
|
692
|
+
return descriptor;
|
|
693
|
+
};
|
|
694
|
+
const statusOf = (key) => {
|
|
695
|
+
const auth = detectAuth(dataDir(), { [key]: registry()[key] })[0];
|
|
696
|
+
return {
|
|
697
|
+
key: auth.key,
|
|
698
|
+
label: auth.label,
|
|
699
|
+
configured: auth.available,
|
|
700
|
+
oauth: registry()[key]?.oauthProvider !== void 0,
|
|
701
|
+
...auth.envKey ? { envKey: auth.envKey } : {},
|
|
702
|
+
methods: auth.methods.map((m) => ({
|
|
703
|
+
source: m.source,
|
|
704
|
+
active: m.active === true,
|
|
705
|
+
...m.key !== void 0 ? { key: m.key } : {},
|
|
706
|
+
...m.label !== void 0 ? { label: m.label } : {},
|
|
707
|
+
detail: m.detail
|
|
708
|
+
}))
|
|
709
|
+
};
|
|
710
|
+
};
|
|
711
|
+
/** The stored API key values of a provider (to clear them from the environment once removed). */
|
|
712
|
+
const storedKeys = (descriptor) => readProviderCredentials(dataDir(), descriptor).flatMap((s) => s.credential.kind === "apikey" && s.credential.value ? [s.credential.value] : []);
|
|
713
|
+
/** Make the change take effect: env re-applied, a removed key out of it, agents rebuilt. */
|
|
714
|
+
const applied = (descriptor, removedKeys = []) => {
|
|
715
|
+
if (env.applyAuthEnv) env.applyAuthEnv();
|
|
716
|
+
else applyApiKeyEnv(dataDir(), registry(), { log: () => void 0 });
|
|
717
|
+
const envKey = descriptor.envKey;
|
|
718
|
+
if (envKey && removedKeys.includes(process.env[envKey] ?? "") && !storedKeys(descriptor).includes(process.env[envKey])) delete process.env[envKey];
|
|
719
|
+
host.rebuildAllAgents().catch(() => void 0);
|
|
720
|
+
};
|
|
721
|
+
return {
|
|
722
|
+
registry,
|
|
723
|
+
dataDir,
|
|
724
|
+
descriptorOf,
|
|
725
|
+
statusOf,
|
|
726
|
+
storedKeys,
|
|
727
|
+
applied
|
|
728
|
+
};
|
|
729
|
+
}
|
|
730
|
+
function createAuthHandlers({ auth }) {
|
|
731
|
+
const { registry, dataDir, descriptorOf, statusOf, storedKeys, applied } = auth;
|
|
732
|
+
return {
|
|
733
|
+
"auth.status": () => ({ providers: Object.keys(registry()).map(statusOf) }),
|
|
734
|
+
"auth.setApiKey": (p) => {
|
|
735
|
+
const descriptor = descriptorOf(p.provider);
|
|
736
|
+
const key = typeof p.key === "string" ? p.key.trim() : "";
|
|
737
|
+
if (!key) throw new Error("The API key is empty.");
|
|
738
|
+
if (key.length > MAX_KEY_LENGTH) throw new Error(`The API key is longer than ${MAX_KEY_LENGTH} characters.`);
|
|
739
|
+
setProviderCredential(dataDir(), descriptor, {
|
|
740
|
+
kind: "apikey",
|
|
741
|
+
value: key
|
|
742
|
+
});
|
|
743
|
+
applied(descriptor);
|
|
744
|
+
return statusOf(descriptor.key);
|
|
745
|
+
},
|
|
746
|
+
"auth.remove": (p) => {
|
|
747
|
+
const descriptor = descriptorOf(p.provider);
|
|
748
|
+
const before = storedKeys(descriptor);
|
|
749
|
+
let removed;
|
|
750
|
+
if (typeof p.account === "string") removed = removeStoredCredential(dataDir(), descriptor, p.account);
|
|
751
|
+
else {
|
|
752
|
+
removed = readProviderCredentials(dataDir(), descriptor).length > 0;
|
|
753
|
+
removeProviderCredential(dataDir(), descriptor);
|
|
754
|
+
}
|
|
755
|
+
if (removed) applied(descriptor, before.filter((k) => !storedKeys(descriptor).includes(k)));
|
|
756
|
+
return {
|
|
757
|
+
removed,
|
|
758
|
+
provider: statusOf(descriptor.key)
|
|
759
|
+
};
|
|
760
|
+
}
|
|
761
|
+
};
|
|
762
|
+
}
|
|
763
|
+
//#endregion
|
|
764
|
+
//#region src/ipc/daemon/handlers/browser.ts
|
|
765
|
+
function createBrowserHandlers({ host }) {
|
|
766
|
+
return {
|
|
767
|
+
methods: {
|
|
768
|
+
"browser.registerHost": (p, peer) => {
|
|
769
|
+
(peer.browserHosts ??= /* @__PURE__ */ new Set()).add(p.hostId);
|
|
770
|
+
host.browser.registerHost(p.hostId);
|
|
771
|
+
return { ok: true };
|
|
772
|
+
},
|
|
773
|
+
"browser.unregisterHost": (p, peer) => {
|
|
774
|
+
peer.browserHosts?.delete(p.hostId);
|
|
775
|
+
host.browser.unregisterHost(p.hostId);
|
|
776
|
+
return { ok: true };
|
|
777
|
+
},
|
|
778
|
+
"browser.respond": (p, peer) => host.browser.respond(p.requestId, p.response, peer.browserHosts ?? /* @__PURE__ */ new Set())
|
|
779
|
+
},
|
|
780
|
+
releasePeer(peer) {
|
|
781
|
+
for (const hostId of peer.browserHosts ?? []) host.browser.unregisterHost(hostId);
|
|
782
|
+
}
|
|
783
|
+
};
|
|
784
|
+
}
|
|
785
|
+
//#endregion
|
|
786
|
+
//#region src/ipc/daemon/handlers/catalog-install.ts
|
|
787
|
+
const DEFAULT_TIMEOUT_MS = 3e5;
|
|
788
|
+
const MAX_SPEC_LENGTH = 500;
|
|
789
|
+
const MAX_MCP_CONFIG_BYTES = 65536;
|
|
790
|
+
const NAME_RE$1 = /^[\w@.-]{1,100}$/;
|
|
791
|
+
const MCP_TRANSPORTS = /* @__PURE__ */ new Set([
|
|
792
|
+
"stdio",
|
|
793
|
+
"sse",
|
|
794
|
+
"streamable-http"
|
|
795
|
+
]);
|
|
796
|
+
const OFF_MESSAGE = "Installs from clients are off on this server (allowRemoteInstalls is off).";
|
|
797
|
+
/** A spec a server may fetch: GitHub or npm. Anything path-like is refused before the parsers see it. */
|
|
798
|
+
function checkSpec(spec) {
|
|
799
|
+
if (typeof spec !== "string" || spec.trim() === "") throw new InvalidParamsError("spec must be a non-empty string.");
|
|
800
|
+
const value = spec.trim();
|
|
801
|
+
if (value.length > MAX_SPEC_LENGTH) throw new InvalidParamsError(`spec is longer than ${MAX_SPEC_LENGTH} characters.`);
|
|
802
|
+
const segments = value.split(/[/:#@]/);
|
|
803
|
+
if (/^(?:[./~\\]|file:)/i.test(value) || value.includes("\\") || segments.includes(".") || segments.includes("..")) throw new InvalidParamsError(`"${value}" is a path: a server installs from GitHub (owner/repo, a GitHub URL) or npm only.`);
|
|
804
|
+
return value;
|
|
805
|
+
}
|
|
806
|
+
function checkName(name, what) {
|
|
807
|
+
if (typeof name !== "string" || !NAME_RE$1.test(name) || name === "." || name === "..") throw new InvalidParamsError(`${what} must be a plain name (letters, digits, ., _, -, @).`);
|
|
808
|
+
return name;
|
|
809
|
+
}
|
|
810
|
+
function checkKind(kind) {
|
|
811
|
+
if (kind !== "skill" && kind !== "extension") throw new InvalidParamsError("kind must be \"skill\" or \"extension\".");
|
|
812
|
+
return kind;
|
|
813
|
+
}
|
|
814
|
+
function isStringRecord(value) {
|
|
815
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) && Object.values(value).every((v) => typeof v === "string");
|
|
816
|
+
}
|
|
817
|
+
function checkMcpConfig(config) {
|
|
818
|
+
if (typeof config !== "object" || config === null || Array.isArray(config)) throw new InvalidParamsError("config must be an MCP server object.");
|
|
819
|
+
if (JSON.stringify(config).length > MAX_MCP_CONFIG_BYTES) throw new InvalidParamsError(`config is larger than ${MAX_MCP_CONFIG_BYTES} bytes.`);
|
|
820
|
+
const c = config;
|
|
821
|
+
checkName(c.name, "name");
|
|
822
|
+
if (c.transport !== void 0 && !MCP_TRANSPORTS.has(c.transport)) throw new InvalidParamsError("transport must be stdio, sse or streamable-http.");
|
|
823
|
+
const hasCommand = typeof c.command === "string" && c.command.trim() !== "";
|
|
824
|
+
let hasUrl = false;
|
|
825
|
+
if (typeof c.url === "string") {
|
|
826
|
+
try {
|
|
827
|
+
hasUrl = ["http:", "https:"].includes(new URL(c.url).protocol);
|
|
828
|
+
} catch {}
|
|
829
|
+
if (!hasUrl) throw new InvalidParamsError("url must be an http(s) URL.");
|
|
830
|
+
}
|
|
831
|
+
if (!hasCommand && !hasUrl) throw new InvalidParamsError("An MCP server needs a command (stdio) or a url.");
|
|
832
|
+
if (c.args !== void 0 && (!Array.isArray(c.args) || !c.args.every((a) => typeof a === "string"))) throw new InvalidParamsError("args must be strings.");
|
|
833
|
+
for (const key of ["env", "headers"]) if (c[key] !== void 0 && !isStringRecord(c[key])) throw new InvalidParamsError(`${key} must map names to strings.`);
|
|
834
|
+
return c;
|
|
835
|
+
}
|
|
836
|
+
/** How long a stopped install may take to roll back before the refusal is answered anyway. */
|
|
837
|
+
const ROLLBACK_GRACE_MS = 1e4;
|
|
838
|
+
/**
|
|
839
|
+
* `run` bounded by `ms`: past it, its signal fires (children killed, no next
|
|
840
|
+
* step: `abortableInstallIo`) and the refusal waits for the installer's own
|
|
841
|
+
* rollback (staging dir, transactional target), up to {@link ROLLBACK_GRACE_MS}.
|
|
842
|
+
*/
|
|
843
|
+
async function bounded(run, ms, what) {
|
|
844
|
+
const controller = new AbortController();
|
|
845
|
+
const work = run(controller.signal);
|
|
846
|
+
let timer;
|
|
847
|
+
try {
|
|
848
|
+
return await Promise.race([work, new Promise((_, reject) => {
|
|
849
|
+
timer = setTimeout(() => {
|
|
850
|
+
controller.abort();
|
|
851
|
+
reject(/* @__PURE__ */ new Error(`${what} took longer than ${Math.round(ms / 1e3)}s and was stopped.`));
|
|
852
|
+
}, ms);
|
|
853
|
+
})]);
|
|
854
|
+
} catch (err) {
|
|
855
|
+
if (controller.signal.aborted) {
|
|
856
|
+
let grace;
|
|
857
|
+
await Promise.race([work.catch(() => void 0), new Promise((resolve) => {
|
|
858
|
+
grace = setTimeout(resolve, ROLLBACK_GRACE_MS);
|
|
859
|
+
})]);
|
|
860
|
+
clearTimeout(grace);
|
|
861
|
+
}
|
|
862
|
+
throw err;
|
|
863
|
+
} finally {
|
|
864
|
+
clearTimeout(timer);
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
function createCatalogInstallHandlers({ host, env, broadcast, setSettings, options }) {
|
|
868
|
+
const timeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
869
|
+
const io = options?.io;
|
|
870
|
+
const prefix = () => env.config.prefix;
|
|
871
|
+
/** The host user's roots for `subPath` (`~/.agents/<sub>`, `~/.<prefix>/<sub>`), read at call time. */
|
|
872
|
+
const userRoots = (subPath) => projectUserPaths({
|
|
873
|
+
subPath,
|
|
874
|
+
cwd: homedir(),
|
|
875
|
+
home: homedir(),
|
|
876
|
+
prefix: prefix()
|
|
877
|
+
}).filter((p) => p.source === "user").map((p) => p.path);
|
|
878
|
+
/** Gate, run, audit: every attempt leaves one line, refusals and failures included. */
|
|
879
|
+
async function guarded(peer, event, fields, run) {
|
|
880
|
+
if (!options) throw new Error("This host takes no installs from clients (a zidane-server does, with allowRemoteInstalls on).");
|
|
881
|
+
if (!options.allowed()) {
|
|
882
|
+
options.audit?.(peer, {
|
|
883
|
+
event,
|
|
884
|
+
...fields,
|
|
885
|
+
ok: false,
|
|
886
|
+
refused: "allowRemoteInstalls is off"
|
|
887
|
+
});
|
|
888
|
+
throw new Error(OFF_MESSAGE);
|
|
889
|
+
}
|
|
890
|
+
try {
|
|
891
|
+
const result = await run();
|
|
892
|
+
options.audit?.(peer, {
|
|
893
|
+
event,
|
|
894
|
+
...fields,
|
|
895
|
+
ok: true
|
|
896
|
+
});
|
|
897
|
+
return result;
|
|
898
|
+
} catch (err) {
|
|
899
|
+
options.audit?.(peer, {
|
|
900
|
+
event,
|
|
901
|
+
...fields,
|
|
902
|
+
ok: false,
|
|
903
|
+
error: err instanceof Error ? err.message : String(err)
|
|
904
|
+
});
|
|
905
|
+
throw err;
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
const changed = (catalog) => broadcast({
|
|
909
|
+
kind: "catalog:changed",
|
|
910
|
+
catalog
|
|
911
|
+
});
|
|
912
|
+
/** Skills and MCP servers are read when an agent is built. */
|
|
913
|
+
const rebuild = () => {
|
|
914
|
+
host.rebuildAllAgents({ rediscover: true }).catch(() => void 0);
|
|
915
|
+
};
|
|
916
|
+
/** Extensions live in the runtime registry: forget it, rebuild, dispose the old one once idle (as `runtime.reload`). */
|
|
917
|
+
const reloadExtensions = () => {
|
|
918
|
+
const disposeOld = env.forgetRuntime();
|
|
919
|
+
host.rebuildAllAgents({ rediscover: true }).then(disposeOld, disposeOld);
|
|
920
|
+
};
|
|
921
|
+
/** `name` dropped from (or added to) an explicit allowlist; an absent list means "all", left alone. */
|
|
922
|
+
const allowlist = (key, name, present) => {
|
|
923
|
+
const list = env.readSettings()[key];
|
|
924
|
+
if (list === void 0 || list.includes(name) === present) return false;
|
|
925
|
+
setSettings({ [key]: present ? [...list, name] : list.filter((n) => n !== name) });
|
|
926
|
+
return true;
|
|
927
|
+
};
|
|
928
|
+
/** The user-scope install directory named `name` under `subPath`, if any. */
|
|
929
|
+
const userDir = (subPath, name, marker) => userRoots(subPath).map((root) => join(root, name)).find((dir) => existsSync(marker ? join(dir, marker) : dir));
|
|
930
|
+
return {
|
|
931
|
+
"catalog.install": (p, peer) => guarded(peer, "catalog.install", {
|
|
932
|
+
kind: p.kind,
|
|
933
|
+
spec: typeof p.spec === "string" ? p.spec.slice(0, MAX_SPEC_LENGTH) : void 0
|
|
934
|
+
}, async () => {
|
|
935
|
+
const kind = checkKind(p.kind);
|
|
936
|
+
const spec = checkSpec(p.spec);
|
|
937
|
+
if (p.scope !== void 0 && p.scope !== "user") throw new InvalidParamsError("A server installs in its user scope only (scope: \"user\").");
|
|
938
|
+
const progress = [];
|
|
939
|
+
const onProgress = (line) => {
|
|
940
|
+
if (progress.length < 200) progress.push(line);
|
|
941
|
+
};
|
|
942
|
+
const home = homedir();
|
|
943
|
+
if (kind === "skill") {
|
|
944
|
+
const result = await bounded((signal) => installSkillFromSpec({
|
|
945
|
+
source: spec,
|
|
946
|
+
scope: "user",
|
|
947
|
+
cwd: home,
|
|
948
|
+
home,
|
|
949
|
+
prefix: prefix(),
|
|
950
|
+
onProgress,
|
|
951
|
+
io: abortableInstallIo(io, signal),
|
|
952
|
+
...Array.isArray(p.skills) ? { skills: p.skills.filter((s) => typeof s === "string") } : {},
|
|
953
|
+
...p.all === true ? { all: true } : {},
|
|
954
|
+
...p.overwrite === true ? { overwrite: true } : {}
|
|
955
|
+
}), timeoutMs, `Installing ${spec}`);
|
|
956
|
+
rebuild();
|
|
957
|
+
changed("skills");
|
|
958
|
+
const skipped = result.skipped?.length ? `. Skipped existing: ${result.skipped.join(", ")}` : "";
|
|
959
|
+
return {
|
|
960
|
+
installed: [...result.installed],
|
|
961
|
+
message: `installed ${result.installed.join(", ")} (${result.provenance ?? result.engine})${skipped}`,
|
|
962
|
+
progress
|
|
963
|
+
};
|
|
964
|
+
}
|
|
965
|
+
let result;
|
|
966
|
+
try {
|
|
967
|
+
result = await bounded((signal) => installExtensionFromSpec({
|
|
968
|
+
source: spec,
|
|
969
|
+
scope: "user",
|
|
970
|
+
cwd: home,
|
|
971
|
+
home,
|
|
972
|
+
prefix: prefix(),
|
|
973
|
+
onProgress,
|
|
974
|
+
io: abortableInstallIo(io, signal),
|
|
975
|
+
...typeof p.extension === "string" ? { extension: p.extension } : {},
|
|
976
|
+
...p.overwrite === true ? { overwrite: true } : {}
|
|
977
|
+
}), timeoutMs, `Installing ${spec}`);
|
|
978
|
+
} catch (err) {
|
|
979
|
+
if (err instanceof ExtensionInstallError && err.code === "exists") throw new Error("already installed on the server: update or remove it first.");
|
|
980
|
+
throw err;
|
|
981
|
+
}
|
|
982
|
+
if (!allowlist("enabledExtensions", result.name, true)) reloadExtensions();
|
|
983
|
+
changed("extensions");
|
|
984
|
+
const warn = result.warnings.length > 0 ? `. ${result.warnings.join(". ")}` : "";
|
|
985
|
+
return {
|
|
986
|
+
installed: [result.name],
|
|
987
|
+
message: `installed "${result.name}" (${result.provenance})${warn}`,
|
|
988
|
+
progress
|
|
989
|
+
};
|
|
990
|
+
}),
|
|
991
|
+
"catalog.remove": (p, peer) => guarded(peer, "catalog.remove", {
|
|
992
|
+
kind: p.kind,
|
|
993
|
+
name: p.name
|
|
994
|
+
}, async () => {
|
|
995
|
+
const kind = checkKind(p.kind);
|
|
996
|
+
const name = checkName(p.name, "name");
|
|
997
|
+
if (kind === "skill") {
|
|
998
|
+
const dir = userDir("skills", name, "SKILL.md");
|
|
999
|
+
if (!dir) throw new Error(`No user-scope skill "${name}" on this server (project skills are files in their folder).`);
|
|
1000
|
+
rmSync(dir, {
|
|
1001
|
+
recursive: true,
|
|
1002
|
+
force: true
|
|
1003
|
+
});
|
|
1004
|
+
allowlist("enabledSkills", name, false);
|
|
1005
|
+
rebuild();
|
|
1006
|
+
changed("skills");
|
|
1007
|
+
return {
|
|
1008
|
+
removed: name,
|
|
1009
|
+
message: `deleted "${name}"`
|
|
1010
|
+
};
|
|
1011
|
+
}
|
|
1012
|
+
const dir = userDir("extensions", name);
|
|
1013
|
+
if (!dir) throw new Error(`No user-scope extension "${name}" on this server.`);
|
|
1014
|
+
removeInstalledExtension({
|
|
1015
|
+
dir,
|
|
1016
|
+
cwd: homedir(),
|
|
1017
|
+
home: homedir(),
|
|
1018
|
+
prefix: prefix()
|
|
1019
|
+
});
|
|
1020
|
+
if (!allowlist("enabledExtensions", name, false)) reloadExtensions();
|
|
1021
|
+
changed("extensions");
|
|
1022
|
+
return {
|
|
1023
|
+
removed: name,
|
|
1024
|
+
message: `removed "${name}"`
|
|
1025
|
+
};
|
|
1026
|
+
}),
|
|
1027
|
+
"catalog.update": (p, peer) => guarded(peer, "catalog.update", {
|
|
1028
|
+
kind: p.kind,
|
|
1029
|
+
name: p.name
|
|
1030
|
+
}, async () => {
|
|
1031
|
+
const kind = checkKind(p.kind);
|
|
1032
|
+
const name = checkName(p.name, "name");
|
|
1033
|
+
if (kind === "skill") throw new Error("Skills are not updated in place: install again with overwrite.");
|
|
1034
|
+
const dir = userDir("extensions", name);
|
|
1035
|
+
if (!dir) throw new Error(`No user-scope extension "${name}" on this server.`);
|
|
1036
|
+
const result = await bounded((signal) => updateInstalledExtension({
|
|
1037
|
+
dir,
|
|
1038
|
+
cwd: homedir(),
|
|
1039
|
+
home: homedir(),
|
|
1040
|
+
prefix: prefix(),
|
|
1041
|
+
io: abortableInstallIo(io, signal)
|
|
1042
|
+
}), timeoutMs, `Updating ${name}`);
|
|
1043
|
+
if (result.upToDate) return {
|
|
1044
|
+
updated: result.name,
|
|
1045
|
+
upToDate: true,
|
|
1046
|
+
message: `"${result.name}" is already up to date${result.commit ? ` (${result.commit.slice(0, 7)})` : ""}`
|
|
1047
|
+
};
|
|
1048
|
+
reloadExtensions();
|
|
1049
|
+
changed("extensions");
|
|
1050
|
+
return {
|
|
1051
|
+
updated: result.name,
|
|
1052
|
+
upToDate: false,
|
|
1053
|
+
message: `updated "${result.name}" → ${result.provenance}`
|
|
1054
|
+
};
|
|
1055
|
+
}),
|
|
1056
|
+
"mcp.upsert": (p, peer) => guarded(peer, "mcp.upsert", { name: p.config?.name }, () => {
|
|
1057
|
+
const config = checkMcpConfig(p.config);
|
|
1058
|
+
const result = upsertMcpServer(mcpsWritePath("user", {
|
|
1059
|
+
home: homedir(),
|
|
1060
|
+
cwd: homedir(),
|
|
1061
|
+
prefix: prefix()
|
|
1062
|
+
}), config);
|
|
1063
|
+
const enabled = result.action === "added" ? enabledMcpsWith(env.readSettings().enabledMcps, config.name) : void 0;
|
|
1064
|
+
if (enabled) setSettings({ enabledMcps: enabled });
|
|
1065
|
+
rebuild();
|
|
1066
|
+
changed("mcps");
|
|
1067
|
+
return {
|
|
1068
|
+
name: config.name,
|
|
1069
|
+
created: result.action === "added"
|
|
1070
|
+
};
|
|
1071
|
+
}),
|
|
1072
|
+
"mcp.remove": (p, peer) => guarded(peer, "mcp.remove", { name: p.name }, () => {
|
|
1073
|
+
const name = checkName(p.name, "name");
|
|
1074
|
+
const path = mcpsWritePath("user", {
|
|
1075
|
+
home: homedir(),
|
|
1076
|
+
cwd: homedir(),
|
|
1077
|
+
prefix: prefix()
|
|
1078
|
+
});
|
|
1079
|
+
try {
|
|
1080
|
+
removeMcpServer(path, name);
|
|
1081
|
+
} catch (err) {
|
|
1082
|
+
if (!existsSync(path) || /no entry named/.test(err instanceof Error ? err.message : "")) return { removed: false };
|
|
1083
|
+
throw err;
|
|
1084
|
+
}
|
|
1085
|
+
const enabled = enabledMcpsWithout(env.readSettings().enabledMcps, name);
|
|
1086
|
+
if (enabled) setSettings({ enabledMcps: enabled });
|
|
1087
|
+
rebuild();
|
|
1088
|
+
changed("mcps");
|
|
1089
|
+
return { removed: true };
|
|
1090
|
+
})
|
|
1091
|
+
};
|
|
1092
|
+
}
|
|
1093
|
+
/** For `ipc.hello`: installs are offered when the server allows them. */
|
|
1094
|
+
function remoteInstallsAllowed(options) {
|
|
1095
|
+
return options?.allowed() === true;
|
|
1096
|
+
}
|
|
1097
|
+
//#endregion
|
|
1098
|
+
//#region src/ipc/daemon/redact.ts
|
|
1099
|
+
/**
|
|
1100
|
+
* Secret redaction for what a host shows about its MCP servers
|
|
1101
|
+
* (`catalog.mcps`): command-line arguments and URLs, which config files
|
|
1102
|
+
* routinely fill with tokens. Over-redacting an ordinary argument is a
|
|
1103
|
+
* cosmetic loss; under-redacting leaks a credential to every client, so the
|
|
1104
|
+
* rules lean wide, but stay off file paths, package names and plain words.
|
|
1105
|
+
*
|
|
1106
|
+
* - URLs (`scheme://…`, as an argument or inside one): userinfo, query values,
|
|
1107
|
+
* fragment values, and secret-looking path segments.
|
|
1108
|
+
* - Values of secret-looking flags (`--token x`, `--api-key=x`), of header
|
|
1109
|
+
* flags (`-H "Name: value"`, `--header=Name: value`), of secret-looking
|
|
1110
|
+
* headers anywhere (`Authorization: Bearer …`), and of secret-looking
|
|
1111
|
+
* `KEY=VALUE` pairs (`-e GITHUB_TOKEN=…`, `env API_KEY=…`, `--pass x`).
|
|
1112
|
+
* - JSON arguments (`--config '{"apiKey":"…"}'`): the value of every
|
|
1113
|
+
* secret-named key at any depth; the other strings as values.
|
|
1114
|
+
* - Connection strings and form bodies (`Server=x;Password=…`,
|
|
1115
|
+
* `a=1&token=…`): each secret-named pair's value.
|
|
1116
|
+
* - Known token shapes anywhere in an argument (`sk-…`, `ghp_…`, `xoxb-…`,
|
|
1117
|
+
* `AKIA…`, `glpat-…`, `zds_…`), and whole high-entropy arguments.
|
|
1118
|
+
*
|
|
1119
|
+
* What counts as secret-looking is shared (`src/chat/secret-shapes`): the
|
|
1120
|
+
* remote host URL check refuses the same shapes in a URL's path.
|
|
1121
|
+
*/
|
|
1122
|
+
const HEADER_FLAGS = /* @__PURE__ */ new Set([
|
|
1123
|
+
"-H",
|
|
1124
|
+
"--header",
|
|
1125
|
+
"--headers"
|
|
1126
|
+
]);
|
|
1127
|
+
const URL_SHAPED = /^[a-z][a-z0-9+.-]*:\/\//i;
|
|
1128
|
+
const HEADER = /^([\w-]+):(\s*)\S.*$/s;
|
|
1129
|
+
const KEY_VALUE = /^([A-Z_][\w.-]*)=(.*)$/is;
|
|
1130
|
+
/** `k=v` pairs joined by `;` or `&` (a connection string, a form body): `Server=x;Password=y`, `a=1&token=x`. */
|
|
1131
|
+
const DELIMITED_PAIRS = /^[a-z_][\w .-]*=[^;&]*(?:[;&][a-z_][\w .-]*=[^;&]*)+[;&]?$/i;
|
|
1132
|
+
/** A JSON object or array argument (`--config '{"apiKey":"…"}'`). */
|
|
1133
|
+
const JSON_SHAPED = /^\s*[{[][\s\S]*[}\]]\s*$/;
|
|
1134
|
+
const scrubKnownTokens = (value) => value.replace(KNOWN_TOKEN, REDACTED);
|
|
1135
|
+
function redactPairs(text) {
|
|
1136
|
+
return text.split("&").map((pair) => {
|
|
1137
|
+
const eq = pair.indexOf("=");
|
|
1138
|
+
return eq < 0 ? pair : `${pair.slice(0, eq)}=${REDACTED}`;
|
|
1139
|
+
}).join("&");
|
|
1140
|
+
}
|
|
1141
|
+
/**
|
|
1142
|
+
* `https://user:pw@host/p/<key>/sse?k=v#t=x` → `https://<redacted>@host/p/<redacted>/sse?k=<redacted>#t=<redacted>`;
|
|
1143
|
+
* an unparsable URL is redacted whole.
|
|
1144
|
+
*/
|
|
1145
|
+
function redactUrl(raw) {
|
|
1146
|
+
let url;
|
|
1147
|
+
try {
|
|
1148
|
+
url = new URL(raw);
|
|
1149
|
+
} catch {
|
|
1150
|
+
return REDACTED;
|
|
1151
|
+
}
|
|
1152
|
+
const credentials = url.username !== "" || url.password !== "" ? `${REDACTED}@` : "";
|
|
1153
|
+
const path = url.pathname.split("/").map((segment) => isSecretPathSegment(segment) ? REDACTED : segment).join("/");
|
|
1154
|
+
const keys = [...url.searchParams.keys()];
|
|
1155
|
+
const query = keys.length > 0 ? `?${keys.map((key) => `${encodeURIComponent(key)}=${REDACTED}`).join("&")}` : "";
|
|
1156
|
+
const fragment = url.hash.slice(1);
|
|
1157
|
+
const hash = fragment === "" ? "" : fragment.includes("=") ? `#${redactPairs(fragment)}` : looksLikeSecret(fragment) ? `#${REDACTED}` : `#${fragment}`;
|
|
1158
|
+
const authority = url.host !== "" || raw.includes("//") ? `//${credentials}${url.host}` : "";
|
|
1159
|
+
return scrubKnownTokens(`${url.protocol}${authority}${path}${query}${hash}`);
|
|
1160
|
+
}
|
|
1161
|
+
function redactHeader(value) {
|
|
1162
|
+
const match = HEADER.exec(value);
|
|
1163
|
+
return match ? `${match[1]}:${match[2]}${REDACTED}` : REDACTED;
|
|
1164
|
+
}
|
|
1165
|
+
/** One value standing on its own (a positional argument, or what follows `--flag=`). */
|
|
1166
|
+
function redactValue(value) {
|
|
1167
|
+
if (URL_SHAPED.test(value)) return redactUrl(value);
|
|
1168
|
+
const header = HEADER.exec(value);
|
|
1169
|
+
if (header && isSecretName(header[1])) return redactHeader(value);
|
|
1170
|
+
const json = JSON_SHAPED.test(value) ? redactJson(value) : null;
|
|
1171
|
+
if (json !== null) return json;
|
|
1172
|
+
if (DELIMITED_PAIRS.test(value)) return redactDelimitedPairs(value);
|
|
1173
|
+
const pair = KEY_VALUE.exec(value);
|
|
1174
|
+
if (pair) return `${pair[1]}=${isSecretName(pair[1]) ? REDACTED : redactValue(pair[2])}`;
|
|
1175
|
+
if (looksLikeSecret(value)) return REDACTED;
|
|
1176
|
+
return scrubKnownTokens(value);
|
|
1177
|
+
}
|
|
1178
|
+
/** `Server=x;Password=y` → `Server=x;Password=<redacted>`: each secret-named pair's value, the others' as values. */
|
|
1179
|
+
function redactDelimitedPairs(value) {
|
|
1180
|
+
return value.split(/([;&])/).map((part) => {
|
|
1181
|
+
const eq = part.indexOf("=");
|
|
1182
|
+
if (eq < 0) return part;
|
|
1183
|
+
const name = part.slice(0, eq);
|
|
1184
|
+
return `${name}=${isSecretName(name.trim()) ? REDACTED : redactValue(part.slice(eq + 1))}`;
|
|
1185
|
+
}).join("");
|
|
1186
|
+
}
|
|
1187
|
+
/** A JSON argument with every secret-named key's value redacted (any depth), the other strings as values; `null` when not JSON. */
|
|
1188
|
+
function redactJson(value) {
|
|
1189
|
+
let parsed;
|
|
1190
|
+
try {
|
|
1191
|
+
parsed = JSON.parse(value);
|
|
1192
|
+
} catch {
|
|
1193
|
+
return null;
|
|
1194
|
+
}
|
|
1195
|
+
if (parsed === null || typeof parsed !== "object") return null;
|
|
1196
|
+
const walk = (node) => {
|
|
1197
|
+
if (typeof node === "string") return redactValue(node);
|
|
1198
|
+
if (Array.isArray(node)) return node.map(walk);
|
|
1199
|
+
if (node !== null && typeof node === "object") return Object.fromEntries(Object.entries(node).map(([key, child]) => [key, isSecretName(key) ? REDACTED : walk(child)]));
|
|
1200
|
+
return node;
|
|
1201
|
+
};
|
|
1202
|
+
return JSON.stringify(walk(parsed));
|
|
1203
|
+
}
|
|
1204
|
+
/** An MCP server's arguments with every secret-looking value replaced by {@link REDACTED}. */
|
|
1205
|
+
function redactArgs(args) {
|
|
1206
|
+
const out = [];
|
|
1207
|
+
for (let i = 0; i < args.length; i++) {
|
|
1208
|
+
const arg = args[i];
|
|
1209
|
+
if (!arg.startsWith("-")) {
|
|
1210
|
+
out.push(redactValue(arg));
|
|
1211
|
+
continue;
|
|
1212
|
+
}
|
|
1213
|
+
const eq = arg.indexOf("=");
|
|
1214
|
+
const flag = eq > 0 ? arg.slice(0, eq) : arg;
|
|
1215
|
+
if (eq > 0) {
|
|
1216
|
+
const value = arg.slice(eq + 1);
|
|
1217
|
+
out.push(`${flag}=${HEADER_FLAGS.has(flag) ? redactHeader(value) : isSecretName(flag) ? REDACTED : redactValue(value)}`);
|
|
1218
|
+
continue;
|
|
1219
|
+
}
|
|
1220
|
+
out.push(scrubKnownTokens(arg));
|
|
1221
|
+
const next = args[i + 1];
|
|
1222
|
+
if (next === void 0 || next.startsWith("-")) continue;
|
|
1223
|
+
if (HEADER_FLAGS.has(flag)) {
|
|
1224
|
+
out.push(redactHeader(next));
|
|
1225
|
+
i++;
|
|
1226
|
+
} else if (isSecretName(flag)) {
|
|
1227
|
+
out.push(REDACTED);
|
|
1228
|
+
i++;
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
return out;
|
|
1232
|
+
}
|
|
1233
|
+
//#endregion
|
|
1234
|
+
//#region src/ipc/daemon/catalog-entries.ts
|
|
1235
|
+
const allows = (list, name) => list === void 0 || list.includes(name);
|
|
1236
|
+
/**
|
|
1237
|
+
* Disk skills (gated by `enabledSkills`), then plugin and extension skills
|
|
1238
|
+
* (gated by their plugin / extension, so always on here), later ones winning
|
|
1239
|
+
* a name like the agent's own merge (`mergeSkillCatalog`).
|
|
1240
|
+
*/
|
|
1241
|
+
function skillEntries(input) {
|
|
1242
|
+
const byName = /* @__PURE__ */ new Map();
|
|
1243
|
+
const add = (skill, origin) => {
|
|
1244
|
+
const contributedBy = origin === "plugin" ? skill.metadata?.["zidane.plugin"] : origin === "extension" ? skill.metadata?.["zidane.extension"] : void 0;
|
|
1245
|
+
byName.set(skill.name, {
|
|
1246
|
+
name: skill.name,
|
|
1247
|
+
description: skill.description,
|
|
1248
|
+
...skill.source ? { source: skill.source } : {},
|
|
1249
|
+
...skill.location ? { location: skill.location } : {},
|
|
1250
|
+
origin,
|
|
1251
|
+
...contributedBy ? { contributedBy } : {},
|
|
1252
|
+
enabled: origin === "disk" ? allows(input.settings.enabledSkills, skill.name) : true,
|
|
1253
|
+
toggleable: origin === "disk"
|
|
1254
|
+
});
|
|
1255
|
+
};
|
|
1256
|
+
for (const skill of input.disk) add(skill, "disk");
|
|
1257
|
+
for (const skill of input.plugin) add(skill, "plugin");
|
|
1258
|
+
for (const skill of input.extension) add(skill, "extension");
|
|
1259
|
+
return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
1260
|
+
}
|
|
1261
|
+
function redactValues(record) {
|
|
1262
|
+
if (!record) return void 0;
|
|
1263
|
+
return Object.fromEntries(Object.keys(record).map((key) => [key, REDACTED]));
|
|
1264
|
+
}
|
|
1265
|
+
function mcpEntries(servers, errors, settings) {
|
|
1266
|
+
return {
|
|
1267
|
+
servers: servers.map(({ config, source, path }) => {
|
|
1268
|
+
const env = redactValues(config.env);
|
|
1269
|
+
const headers = redactValues(config.headers);
|
|
1270
|
+
return {
|
|
1271
|
+
name: config.name,
|
|
1272
|
+
transport: config.transport,
|
|
1273
|
+
source,
|
|
1274
|
+
path,
|
|
1275
|
+
enabled: allows(settings.enabledMcps, config.name),
|
|
1276
|
+
toggleable: true,
|
|
1277
|
+
...config.command !== void 0 ? { command: config.command } : {},
|
|
1278
|
+
...config.args ? { args: redactArgs(config.args) } : {},
|
|
1279
|
+
...config.url !== void 0 ? { url: redactUrl(config.url) } : {},
|
|
1280
|
+
...config.cwd !== void 0 ? { cwd: config.cwd } : {},
|
|
1281
|
+
...config.scope !== void 0 ? { scope: config.scope } : {},
|
|
1282
|
+
...config.auth !== void 0 ? { auth: config.auth } : {},
|
|
1283
|
+
...env ? { env } : {},
|
|
1284
|
+
...headers ? { headers } : {}
|
|
1285
|
+
};
|
|
1286
|
+
}),
|
|
1287
|
+
errors: errors.map((e) => ({
|
|
1288
|
+
path: e.path,
|
|
1289
|
+
source: e.source,
|
|
1290
|
+
message: e.message,
|
|
1291
|
+
...e.informational ? { informational: true } : {}
|
|
1292
|
+
}))
|
|
1293
|
+
};
|
|
1294
|
+
}
|
|
1295
|
+
function capabilities(contributions) {
|
|
1296
|
+
const labels = [];
|
|
1297
|
+
const add = (label, present) => {
|
|
1298
|
+
if (present) labels.push(label);
|
|
1299
|
+
};
|
|
1300
|
+
add("agents", Object.keys(contributions.agents).length > 0);
|
|
1301
|
+
add("providers", Object.keys(contributions.providers).length > 0);
|
|
1302
|
+
add("tools", Object.keys(contributions.tools).length > 0);
|
|
1303
|
+
add("hooks", Object.values(contributions.hooks).some((handlers) => handlers !== void 0 && handlers.length > 0));
|
|
1304
|
+
add("mcps", contributions.mcpServers.length > 0);
|
|
1305
|
+
add("skills", contributions.skills.length > 0);
|
|
1306
|
+
add("commands", contributions.commands.length > 0);
|
|
1307
|
+
add("completions", contributions.completionProviders.length > 0);
|
|
1308
|
+
add("themes", Object.keys(contributions.themes).length > 0);
|
|
1309
|
+
add("system", contributions.systemFragments.length > 0);
|
|
1310
|
+
return labels;
|
|
1311
|
+
}
|
|
1312
|
+
/** Loaded, then inactive (disabled or broken), first one of a name wins. */
|
|
1313
|
+
function extensionEntries(loaded, inactive, settings) {
|
|
1314
|
+
const rows = /* @__PURE__ */ new Map();
|
|
1315
|
+
for (const ext of [...loaded, ...inactive]) {
|
|
1316
|
+
if (rows.has(ext.name)) continue;
|
|
1317
|
+
rows.set(ext.name, {
|
|
1318
|
+
name: ext.name,
|
|
1319
|
+
source: ext.source,
|
|
1320
|
+
enabled: !ext.error && allows(settings.enabledExtensions, ext.name),
|
|
1321
|
+
toggleable: !ext.error,
|
|
1322
|
+
...ext.definition?.version ? { version: ext.definition.version } : {},
|
|
1323
|
+
...ext.definition?.description ? { description: ext.definition.description } : {},
|
|
1324
|
+
...ext.dir ? { dir: ext.dir } : {},
|
|
1325
|
+
...ext.error ? { error: ext.error } : {},
|
|
1326
|
+
...ext.contributions ? { capabilities: capabilities(ext.contributions) } : {}
|
|
1327
|
+
});
|
|
1328
|
+
}
|
|
1329
|
+
return [...rows.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
1330
|
+
}
|
|
1331
|
+
/** Disk plugins, then the ones extensions loaded (a name on disk wins, as the agent's gate decides). */
|
|
1332
|
+
function pluginEntries(disk, fromExtensions, settings) {
|
|
1333
|
+
const sources = [...disk.map((p) => ({
|
|
1334
|
+
plugin: p.plugin,
|
|
1335
|
+
name: p.name,
|
|
1336
|
+
origin: {
|
|
1337
|
+
kind: "disk",
|
|
1338
|
+
scope: p.source,
|
|
1339
|
+
dir: p.path
|
|
1340
|
+
}
|
|
1341
|
+
})), ...fromExtensions.map((r) => ({
|
|
1342
|
+
plugin: r.plugin,
|
|
1343
|
+
name: r.name,
|
|
1344
|
+
origin: {
|
|
1345
|
+
kind: "extension",
|
|
1346
|
+
extensionName: r.extensionName,
|
|
1347
|
+
dir: r.dir
|
|
1348
|
+
}
|
|
1349
|
+
}))];
|
|
1350
|
+
const rows = /* @__PURE__ */ new Map();
|
|
1351
|
+
for (const { plugin, name, origin } of sources) {
|
|
1352
|
+
if (rows.has(name)) continue;
|
|
1353
|
+
const error = plugin.ok ? void 0 : plugin.diagnostics.find((d) => d.severity === "error")?.message;
|
|
1354
|
+
rows.set(name, {
|
|
1355
|
+
name,
|
|
1356
|
+
origin,
|
|
1357
|
+
ok: plugin.ok,
|
|
1358
|
+
enabled: plugin.ok && allows(settings.enabledPlugins, name),
|
|
1359
|
+
toggleable: plugin.ok,
|
|
1360
|
+
...plugin.manifest?.version !== void 0 ? { version: plugin.manifest.version } : {},
|
|
1361
|
+
...plugin.manifest?.description !== void 0 ? { description: plugin.manifest.description } : {},
|
|
1362
|
+
skills: plugin.skills.map((s) => s.name),
|
|
1363
|
+
mcpServers: plugin.mcpServers.map((s) => s.name),
|
|
1364
|
+
...error ? { error } : {}
|
|
1365
|
+
});
|
|
1366
|
+
}
|
|
1367
|
+
return [...rows.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
1368
|
+
}
|
|
1369
|
+
//#endregion
|
|
1370
|
+
//#region src/ipc/daemon/handlers/catalogs.ts
|
|
1371
|
+
const kindOf = (kind) => typeof kind === "string" ? kind : void 0;
|
|
1372
|
+
/**
|
|
1373
|
+
* The folder a catalog scans: `cwd` (inside the roots on a server), else the
|
|
1374
|
+
* host user's home. A machine's folder (`docs/MACHINES.md` § Folders) is
|
|
1375
|
+
* never read here: a folder of this host's stands in for the user scope.
|
|
1376
|
+
*/
|
|
1377
|
+
function catalogFolder({ env, host }, cwd, kind) {
|
|
1378
|
+
if (cwd !== void 0 && typeof cwd !== "string") throw new Error("cwd must be a string.");
|
|
1379
|
+
if (!cwd) return scopeCwd("");
|
|
1380
|
+
const folder = host.resolveFolder(cwd, kindOf(kind));
|
|
1381
|
+
return folder.machine ? machineDiscoveryCwd(env.userDataDir()) : scopeCwd(folder.cwd);
|
|
1382
|
+
}
|
|
1383
|
+
function createCatalogHandlers({ env, host }) {
|
|
1384
|
+
const folderOf = (cwd, kind) => catalogFolder({
|
|
1385
|
+
env,
|
|
1386
|
+
host
|
|
1387
|
+
}, cwd, kind);
|
|
1388
|
+
/** User scope here, project scope read on the machine for a machine's folder. */
|
|
1389
|
+
const catalogOf = async (cwd, kind, prefix) => {
|
|
1390
|
+
if (typeof cwd !== "string" || !cwd || !host.resolveFolder(cwd, kindOf(kind)).machine) return { dir: folderOf(cwd, kind) };
|
|
1391
|
+
const { discoveryCwd, project } = await host.folderCatalog(cwd, kindOf(kind), prefix);
|
|
1392
|
+
return {
|
|
1393
|
+
dir: discoveryCwd,
|
|
1394
|
+
...project ? { project } : {}
|
|
1395
|
+
};
|
|
1396
|
+
};
|
|
1397
|
+
const scanFlags = () => {
|
|
1398
|
+
const settings = env.readSettings();
|
|
1399
|
+
return {
|
|
1400
|
+
settings,
|
|
1401
|
+
foreignAgents: settings.importForeignConfigs === true,
|
|
1402
|
+
ruler: settings.importRulerConfigs !== false
|
|
1403
|
+
};
|
|
1404
|
+
};
|
|
1405
|
+
const plugins = async (dir, config) => {
|
|
1406
|
+
const { plugins: found } = await discoverProjectPlugins({
|
|
1407
|
+
cwd: dir,
|
|
1408
|
+
prefix: config.prefix,
|
|
1409
|
+
...config.pluginScanPaths.length > 0 ? { extraRoots: config.pluginScanPaths } : {}
|
|
1410
|
+
});
|
|
1411
|
+
return found;
|
|
1412
|
+
};
|
|
1413
|
+
return {
|
|
1414
|
+
"catalog.skills": async (p) => {
|
|
1415
|
+
const { dir, project } = await catalogOf(p?.cwd, p?.kind, env.config.prefix);
|
|
1416
|
+
const config = await env.runtimeConfig(dir);
|
|
1417
|
+
const { settings, foreignAgents, ruler } = scanFlags();
|
|
1418
|
+
const [found, discovered] = await Promise.all([discoverProjectSkills({
|
|
1419
|
+
cwd: dir,
|
|
1420
|
+
prefix: config.prefix,
|
|
1421
|
+
foreignAgents,
|
|
1422
|
+
ruler
|
|
1423
|
+
}), plugins(dir, config)]);
|
|
1424
|
+
const machineNames = new Set((project?.skills ?? []).map((skill) => skill.name));
|
|
1425
|
+
const disk = [...project?.skills ?? [], ...found.filter((skill) => !machineNames.has(skill.name))];
|
|
1426
|
+
const pluginSkills = buildPluginContributions({
|
|
1427
|
+
discovered,
|
|
1428
|
+
...settings.enabledPlugins !== void 0 ? { enabled: settings.enabledPlugins } : {}
|
|
1429
|
+
}).skills;
|
|
1430
|
+
return { skills: skillEntries({
|
|
1431
|
+
disk,
|
|
1432
|
+
plugin: pluginSkills,
|
|
1433
|
+
extension: filterRegistryPlugins(config.extensions, settings.enabledPlugins, new Set(discovered.map((d) => d.name))).skills,
|
|
1434
|
+
settings
|
|
1435
|
+
}) };
|
|
1436
|
+
},
|
|
1437
|
+
"catalog.mcps": async (p) => {
|
|
1438
|
+
const { dir, project } = await catalogOf(p?.cwd, p?.kind, env.config.prefix);
|
|
1439
|
+
const config = await env.runtimeConfig(dir);
|
|
1440
|
+
const { settings, foreignAgents, ruler } = scanFlags();
|
|
1441
|
+
const { servers: found, errors } = discoverProjectMcps({
|
|
1442
|
+
cwd: dir,
|
|
1443
|
+
prefix: config.prefix,
|
|
1444
|
+
foreignAgents,
|
|
1445
|
+
ruler
|
|
1446
|
+
});
|
|
1447
|
+
const machineNames = new Set((project?.mcps ?? []).map((server) => server.config.name));
|
|
1448
|
+
return mcpEntries([...project?.mcps ?? [], ...found.filter((server) => !machineNames.has(server.config.name))], errors, settings);
|
|
1449
|
+
},
|
|
1450
|
+
"catalog.extensions": async (p) => {
|
|
1451
|
+
const config = await env.runtimeConfig(folderOf(p?.cwd, p?.kind));
|
|
1452
|
+
return { extensions: extensionEntries(config.extensions.loaded, config.extensions.inactive, env.readSettings()) };
|
|
1453
|
+
},
|
|
1454
|
+
"catalog.plugins": async (p) => {
|
|
1455
|
+
const dir = folderOf(p?.cwd, p?.kind);
|
|
1456
|
+
const config = await env.runtimeConfig(dir);
|
|
1457
|
+
return { plugins: pluginEntries(await plugins(dir, config), config.extensions.plugins, env.readSettings()) };
|
|
1458
|
+
},
|
|
1459
|
+
"models.list": async (p) => {
|
|
1460
|
+
const config = p?.cwd ? await env.runtimeConfig(folderOf(p.cwd, p.kind)) : env.config;
|
|
1461
|
+
return { providers: detectAuth(env.userDataDir(), config.providers).filter((auth) => auth.available).map((auth) => ({
|
|
1462
|
+
key: auth.key,
|
|
1463
|
+
label: config.providers[auth.key]?.label ?? auth.label,
|
|
1464
|
+
models: JSON.parse(JSON.stringify(config.modelsFor(auth.key)))
|
|
1465
|
+
})) };
|
|
1466
|
+
}
|
|
1467
|
+
};
|
|
1468
|
+
}
|
|
1469
|
+
//#endregion
|
|
1470
|
+
//#region src/ipc/daemon/handlers/chat.ts
|
|
1471
|
+
function createChatHandlers({ host, clearDraft }) {
|
|
1472
|
+
return {
|
|
1473
|
+
"chat.send": async (p, peer) => {
|
|
1474
|
+
const { wake: _stripped, keepDraft, ...params } = p;
|
|
1475
|
+
const result = await host.sendPrompt(params);
|
|
1476
|
+
if (!keepDraft) clearDraft(p.sessionId, peer);
|
|
1477
|
+
return result;
|
|
1478
|
+
},
|
|
1479
|
+
"chat.interrupt": (p) => host.interrupt(p.sessionId),
|
|
1480
|
+
"chat.steer": (p) => host.steer(p.sessionId, p.message),
|
|
1481
|
+
"chat.followUp": (p) => host.followUp(p.sessionId, p.message),
|
|
1482
|
+
"chat.pushQueued": async (p, peer) => {
|
|
1483
|
+
const result = await host.pushQueued(p.sessionId, p.message, p.activatedSkills);
|
|
1484
|
+
if (!p.keepDraft) clearDraft(p.sessionId, peer);
|
|
1485
|
+
return result;
|
|
1486
|
+
},
|
|
1487
|
+
"chat.isRunning": (p) => host.isSessionRunning(p.sessionId),
|
|
1488
|
+
"chat.rebuildAgent": (p) => host.disposeAgentForRebuild(p.sessionId),
|
|
1489
|
+
"chat.listWork": (p) => host.listRunningWork(p.sessionId),
|
|
1490
|
+
"chat.cancelWork": (p) => host.cancelRunningWork(p.sessionId, p.id, p.reason),
|
|
1491
|
+
"commands.list": (p) => host.listCommands(p.cwd, typeof p.kind === "string" ? p.kind : void 0),
|
|
1492
|
+
"commands.completeArgs": (p) => host.completeCommandArgs(p.cwd, p.name, p.argsPrefix, typeof p.kind === "string" ? p.kind : void 0)
|
|
1493
|
+
};
|
|
1494
|
+
}
|
|
1495
|
+
//#endregion
|
|
1496
|
+
//#region src/ipc/daemon/handlers/context.ts
|
|
1497
|
+
function createContextHandlers({ host }) {
|
|
1498
|
+
return {
|
|
1499
|
+
"sessions.systemMd": (p) => host.sessionSystemMd(p.sessionId),
|
|
1500
|
+
"sessions.contextSegments": (p) => host.getContextSegments(p.sessionId),
|
|
1501
|
+
"sessions.createSystemOverride": (p) => host.createSystemOverride(p.sessionId),
|
|
1502
|
+
"sessions.contextBreakdown": (p) => host.getContextBreakdown(p.sessionId)
|
|
1503
|
+
};
|
|
1504
|
+
}
|
|
1505
|
+
//#endregion
|
|
1506
|
+
//#region src/ipc/daemon/handlers/daemon.ts
|
|
1507
|
+
function createDaemonHandlers({ host, appVersion, startedAt, lifecycle, identity }) {
|
|
1508
|
+
return {
|
|
1509
|
+
"daemon.info": () => ({
|
|
1510
|
+
protocolVersion: 13,
|
|
1511
|
+
appVersion,
|
|
1512
|
+
pid: process.pid,
|
|
1513
|
+
startedAt,
|
|
1514
|
+
...identity
|
|
1515
|
+
}),
|
|
1516
|
+
"daemon.shutdown": async (p) => {
|
|
1517
|
+
if (p?.whenIdle === true && host.hasActiveWork()) {
|
|
1518
|
+
lifecycle.retireWhenIdle();
|
|
1519
|
+
return { deferred: true };
|
|
1520
|
+
}
|
|
1521
|
+
setImmediate(lifecycle.shutdown);
|
|
1522
|
+
}
|
|
1523
|
+
};
|
|
1524
|
+
}
|
|
1525
|
+
//#endregion
|
|
1526
|
+
//#region src/ipc/daemon/handlers/drafts.ts
|
|
1527
|
+
/**
|
|
1528
|
+
* Bound a peer-supplied attachment list: shape-guarded entries only,
|
|
1529
|
+
* 24 MiB of base64 per list. Shared by `queue.add` and
|
|
1530
|
+
* `drafts.setAttachments`.
|
|
1531
|
+
*/
|
|
1532
|
+
function boundAttachments(list) {
|
|
1533
|
+
let budget = 25165824;
|
|
1534
|
+
const out = [];
|
|
1535
|
+
for (const entry of list) {
|
|
1536
|
+
const a = entry;
|
|
1537
|
+
if (typeof a?.mediaType !== "string" || typeof a?.data !== "string") continue;
|
|
1538
|
+
budget -= a.data.length;
|
|
1539
|
+
if (budget < 0) break;
|
|
1540
|
+
out.push({
|
|
1541
|
+
mediaType: a.mediaType,
|
|
1542
|
+
data: a.data,
|
|
1543
|
+
...typeof a.name === "string" ? { name: a.name.slice(0, 256) } : {}
|
|
1544
|
+
});
|
|
1545
|
+
}
|
|
1546
|
+
return out;
|
|
1547
|
+
}
|
|
1548
|
+
function createDraftHandlers({ broadcastExcept }) {
|
|
1549
|
+
/**
|
|
1550
|
+
* Shared per-session prompt drafts. Ephemeral by design — in-memory
|
|
1551
|
+
* only, cleared when any window submits, gone on daemon restart.
|
|
1552
|
+
*/
|
|
1553
|
+
const drafts = /* @__PURE__ */ new Map();
|
|
1554
|
+
/** Total base64 bytes currently pinned by ALL draft attachments. */
|
|
1555
|
+
function draftAttachmentBytes() {
|
|
1556
|
+
let total = 0;
|
|
1557
|
+
for (const entry of drafts.values()) for (const attachment of entry.attachments ?? []) total += attachment.data.length;
|
|
1558
|
+
return total;
|
|
1559
|
+
}
|
|
1560
|
+
function clearDraft(sessionId, origin) {
|
|
1561
|
+
const entry = drafts.get(sessionId);
|
|
1562
|
+
if (!entry) return;
|
|
1563
|
+
drafts.delete(sessionId);
|
|
1564
|
+
if (entry.text) broadcastExcept(origin, {
|
|
1565
|
+
kind: "draft:changed",
|
|
1566
|
+
sessionId,
|
|
1567
|
+
text: ""
|
|
1568
|
+
});
|
|
1569
|
+
if (entry.attachments?.length) broadcastExcept(origin, {
|
|
1570
|
+
kind: "draft:attachments",
|
|
1571
|
+
sessionId,
|
|
1572
|
+
attachments: []
|
|
1573
|
+
});
|
|
1574
|
+
}
|
|
1575
|
+
/** Drop empty draft entries so the map only holds live composing state. */
|
|
1576
|
+
function compactDraft(sessionId, entry) {
|
|
1577
|
+
if (!entry.text && !entry.shell && !entry.attachments?.length && !entry.search?.active) drafts.delete(sessionId);
|
|
1578
|
+
else drafts.set(sessionId, entry);
|
|
1579
|
+
}
|
|
1580
|
+
return {
|
|
1581
|
+
methods: {
|
|
1582
|
+
"drafts.get": (p) => {
|
|
1583
|
+
const entry = drafts.get(p.sessionId);
|
|
1584
|
+
return {
|
|
1585
|
+
text: entry?.text ?? "",
|
|
1586
|
+
...entry?.shell ? { shell: true } : {},
|
|
1587
|
+
...entry?.attachments?.length ? { attachments: entry.attachments } : {},
|
|
1588
|
+
...entry?.search?.active ? { search: entry.search } : {}
|
|
1589
|
+
};
|
|
1590
|
+
},
|
|
1591
|
+
"drafts.set": (p, peer) => {
|
|
1592
|
+
const text = typeof p.text === "string" ? p.text.slice(0, 65536) : "";
|
|
1593
|
+
const truncated = typeof p.text === "string" && p.text.length > text.length;
|
|
1594
|
+
if (!drafts.has(p.sessionId) && drafts.size >= 256) return;
|
|
1595
|
+
const entry = drafts.get(p.sessionId) ?? {};
|
|
1596
|
+
entry.text = text.length > 0 ? text : void 0;
|
|
1597
|
+
entry.shell = p.shell === true ? true : void 0;
|
|
1598
|
+
compactDraft(p.sessionId, entry);
|
|
1599
|
+
broadcastExcept(truncated ? null : peer, {
|
|
1600
|
+
kind: "draft:changed",
|
|
1601
|
+
sessionId: p.sessionId,
|
|
1602
|
+
text,
|
|
1603
|
+
...p.shell === true ? { shell: true } : {}
|
|
1604
|
+
});
|
|
1605
|
+
},
|
|
1606
|
+
"search.set": (p, peer) => {
|
|
1607
|
+
const active = p.active === true;
|
|
1608
|
+
const query = typeof p.query === "string" ? p.query.slice(0, 4096) : "";
|
|
1609
|
+
if (!drafts.has(p.sessionId) && drafts.size >= 256) return;
|
|
1610
|
+
const entry = drafts.get(p.sessionId) ?? {};
|
|
1611
|
+
entry.search = active || query.length > 0 ? {
|
|
1612
|
+
active,
|
|
1613
|
+
query
|
|
1614
|
+
} : void 0;
|
|
1615
|
+
compactDraft(p.sessionId, entry);
|
|
1616
|
+
broadcastExcept(peer, {
|
|
1617
|
+
kind: "search:changed",
|
|
1618
|
+
sessionId: p.sessionId,
|
|
1619
|
+
active,
|
|
1620
|
+
query
|
|
1621
|
+
});
|
|
1622
|
+
},
|
|
1623
|
+
"drafts.setAttachments": (p, peer) => {
|
|
1624
|
+
const bounded = Array.isArray(p.attachments) ? boundAttachments(p.attachments) : [];
|
|
1625
|
+
if (!drafts.has(p.sessionId) && drafts.size >= 256) return;
|
|
1626
|
+
const currentBytes = (drafts.get(p.sessionId)?.attachments ?? []).reduce((sum, a) => sum + a.data.length, 0);
|
|
1627
|
+
const incomingBytes = bounded.reduce((sum, a) => sum + a.data.length, 0);
|
|
1628
|
+
if (draftAttachmentBytes() - currentBytes + incomingBytes > 100663296) return;
|
|
1629
|
+
const entry = drafts.get(p.sessionId) ?? {};
|
|
1630
|
+
entry.attachments = bounded.length > 0 ? bounded : void 0;
|
|
1631
|
+
compactDraft(p.sessionId, entry);
|
|
1632
|
+
broadcastExcept(peer, {
|
|
1633
|
+
kind: "draft:attachments",
|
|
1634
|
+
sessionId: p.sessionId,
|
|
1635
|
+
attachments: bounded
|
|
1636
|
+
});
|
|
1637
|
+
}
|
|
1638
|
+
},
|
|
1639
|
+
clearDraft,
|
|
1640
|
+
forget: (sessionId) => {
|
|
1641
|
+
drafts.delete(sessionId);
|
|
1642
|
+
}
|
|
1643
|
+
};
|
|
1644
|
+
}
|
|
1645
|
+
//#endregion
|
|
1646
|
+
//#region src/ipc/daemon/handlers/sessions.ts
|
|
1647
|
+
async function loadOrThrow(store, sessionId) {
|
|
1648
|
+
const data = await store.load(sessionId);
|
|
1649
|
+
if (!data) throw new Error(`Session "${sessionId}" not found.`);
|
|
1650
|
+
return data;
|
|
1651
|
+
}
|
|
1652
|
+
function createSessionHandlers({ host, store, broadcast, forgetSession, hostId, workspace }) {
|
|
1653
|
+
const stamp = (row) => hostId ? {
|
|
1654
|
+
...row,
|
|
1655
|
+
hostId
|
|
1656
|
+
} : row;
|
|
1657
|
+
/**
|
|
1658
|
+
* Apply a metadata-only patch. Prefers the host's live-session path (safe
|
|
1659
|
+
* mid-run — routes through the in-memory `Session` whose turns are the
|
|
1660
|
+
* source of truth); only falls back to a store-level full-document
|
|
1661
|
+
* `save()` when the session is idle, where no run is appending turns so
|
|
1662
|
+
* `load → mutate → save` can't drop concurrently-persisted turns.
|
|
1663
|
+
*
|
|
1664
|
+
* `bumpUpdatedAt: false` skips the recency bump so a rename doesn't yank
|
|
1665
|
+
* the row to the top of recency-sorted lists (mirrors the GUI).
|
|
1666
|
+
*/
|
|
1667
|
+
async function patchSessionMeta(sessionId, patch, opts = {}) {
|
|
1668
|
+
await host.patchSessionMeta(sessionId, patch, opts);
|
|
1669
|
+
broadcast({
|
|
1670
|
+
kind: "sessions:changed",
|
|
1671
|
+
op: "update",
|
|
1672
|
+
sessionId
|
|
1673
|
+
});
|
|
1674
|
+
}
|
|
1675
|
+
async function patchSession(sessionId, mutate) {
|
|
1676
|
+
const data = await loadOrThrow(store, sessionId);
|
|
1677
|
+
mutate(data);
|
|
1678
|
+
data.updatedAt = Date.now();
|
|
1679
|
+
await store.save(data);
|
|
1680
|
+
broadcast({
|
|
1681
|
+
kind: "sessions:changed",
|
|
1682
|
+
op: "update",
|
|
1683
|
+
sessionId
|
|
1684
|
+
});
|
|
1685
|
+
}
|
|
1686
|
+
async function createSessionRow(params) {
|
|
1687
|
+
const metadata = creationMetadata(params);
|
|
1688
|
+
const onMachine = params.environment?.kind ? host.environments.get(params.environment.kind)?.checkFolder !== void 0 : false;
|
|
1689
|
+
const cwd = workspace?.describe().confined && !onMachine ? workspace.sessionFolder(params.cwd ?? "") : params.cwd;
|
|
1690
|
+
const id = await host.createSession({
|
|
1691
|
+
cwd,
|
|
1692
|
+
...metadata ? { metadata } : {},
|
|
1693
|
+
...params.title ? { title: params.title } : {},
|
|
1694
|
+
...params.agentId ? { agentId: params.agentId } : {},
|
|
1695
|
+
...params.providerKey && params.modelId ? {
|
|
1696
|
+
providerKey: params.providerKey,
|
|
1697
|
+
modelId: params.modelId
|
|
1698
|
+
} : {},
|
|
1699
|
+
...params.fallbacks ? { fallbacks: params.fallbacks } : {},
|
|
1700
|
+
...params.extensionEdit ? { extensionEdit: true } : {},
|
|
1701
|
+
...params.skillEdit?.name ? { skillEdit: { name: params.skillEdit.name } } : {},
|
|
1702
|
+
...params.environment?.kind ? { environment: {
|
|
1703
|
+
kind: params.environment.kind,
|
|
1704
|
+
...params.environment.branch ? { branch: params.environment.branch } : {},
|
|
1705
|
+
...params.environment.base ? { base: params.environment.base } : {}
|
|
1706
|
+
} } : {}
|
|
1707
|
+
});
|
|
1708
|
+
const data = await loadOrThrow(store, id);
|
|
1709
|
+
broadcast({
|
|
1710
|
+
kind: "sessions:changed",
|
|
1711
|
+
op: "create",
|
|
1712
|
+
sessionId: id
|
|
1713
|
+
});
|
|
1714
|
+
return stamp(toRow(host, data));
|
|
1715
|
+
}
|
|
1716
|
+
return {
|
|
1717
|
+
"sessions.list": async (p) => (await listSessionRows(host, store, p)).map(stamp),
|
|
1718
|
+
"sessions.create": (p) => createSessionRow(p),
|
|
1719
|
+
"sessions.delete": async (p) => {
|
|
1720
|
+
forgetSession(p.sessionId);
|
|
1721
|
+
const { environment } = await host.deleteSession(p.sessionId);
|
|
1722
|
+
broadcast({
|
|
1723
|
+
kind: "sessions:changed",
|
|
1724
|
+
op: "delete",
|
|
1725
|
+
sessionId: p.sessionId
|
|
1726
|
+
});
|
|
1727
|
+
return environment.removed && !environment.reason ? {} : { environment };
|
|
1728
|
+
},
|
|
1729
|
+
"sessions.setTitle": (p) => patchSessionMeta(p.sessionId, { title: p.title }, { bumpUpdatedAt: false }),
|
|
1730
|
+
"sessions.setArchived": (p) => patchSessionMeta(p.sessionId, { archived: p.archived }),
|
|
1731
|
+
"sessions.setAgentId": async (p) => {
|
|
1732
|
+
if (await host.setLiveSessionAgentId(p.sessionId, p.agentId)) broadcast({
|
|
1733
|
+
kind: "sessions:changed",
|
|
1734
|
+
op: "update",
|
|
1735
|
+
sessionId: p.sessionId
|
|
1736
|
+
});
|
|
1737
|
+
else await patchSession(p.sessionId, (d) => {
|
|
1738
|
+
d.agentId = p.agentId;
|
|
1739
|
+
});
|
|
1740
|
+
host.disposeAgentForRebuild(p.sessionId);
|
|
1741
|
+
},
|
|
1742
|
+
"sessions.setEffort": (p) => patchSessionMeta(p.sessionId, { effort: p.effort }),
|
|
1743
|
+
"sessions.setModel": async (p) => {
|
|
1744
|
+
await host.setSessionModel(p.sessionId, {
|
|
1745
|
+
providerKey: p.providerKey,
|
|
1746
|
+
modelId: p.modelId,
|
|
1747
|
+
...p.fallbacks ? { fallbacks: p.fallbacks } : {},
|
|
1748
|
+
...p.effort ? { effort: p.effort } : {},
|
|
1749
|
+
...p.modelOptions ? { modelOptions: p.modelOptions } : {}
|
|
1750
|
+
});
|
|
1751
|
+
broadcast({
|
|
1752
|
+
kind: "sessions:changed",
|
|
1753
|
+
op: "update",
|
|
1754
|
+
sessionId: p.sessionId
|
|
1755
|
+
});
|
|
1756
|
+
},
|
|
1757
|
+
"sessions.setModelOptions": (p) => patchSessionMeta(p.sessionId, { modelOptions: p.options }),
|
|
1758
|
+
"sessions.setFolder": (p) => {
|
|
1759
|
+
const cwd = typeof p.cwd === "string" ? p.cwd : "";
|
|
1760
|
+
return host.setSessionFolder(p.sessionId, cwd);
|
|
1761
|
+
}
|
|
1762
|
+
};
|
|
1763
|
+
}
|
|
1764
|
+
/** The creation-time metadata a client may stamp: the pick's effort and options, pinned skills. */
|
|
1765
|
+
function creationMetadata(params) {
|
|
1766
|
+
const metadata = {};
|
|
1767
|
+
if (typeof params.effort === "string") metadata.effort = params.effort;
|
|
1768
|
+
if (params.modelOptions && typeof params.modelOptions === "object") {
|
|
1769
|
+
const options = Object.fromEntries(Object.entries(params.modelOptions).filter(([, on]) => typeof on === "boolean"));
|
|
1770
|
+
if (Object.keys(options).length > 0) metadata.modelOptions = options;
|
|
1771
|
+
}
|
|
1772
|
+
if (Array.isArray(params.pinnedSkills)) {
|
|
1773
|
+
const pins = readPinnedSkills(params.pinnedSkills.slice(0, 64));
|
|
1774
|
+
if (pins.size > 0) metadata[PINNED_SKILLS_META_KEY] = pinnedSkillsValue(pins);
|
|
1775
|
+
}
|
|
1776
|
+
return Object.keys(metadata).length > 0 ? metadata : void 0;
|
|
1777
|
+
}
|
|
1778
|
+
async function listSessionRows(host, store, p) {
|
|
1779
|
+
const filter = p?.projectRoot !== void 0 ? { projectRoot: p.projectRoot } : void 0;
|
|
1780
|
+
const applyArchive = (rows) => {
|
|
1781
|
+
const filtered = rows.filter((row) => {
|
|
1782
|
+
if (p?.archive === "active") return !row.archived;
|
|
1783
|
+
if (p?.archive === "archived") return row.archived;
|
|
1784
|
+
return true;
|
|
1785
|
+
});
|
|
1786
|
+
filtered.sort((a, b) => b.updatedAt - a.updatedAt);
|
|
1787
|
+
return filtered;
|
|
1788
|
+
};
|
|
1789
|
+
if (store.listMeta) return applyArchive((await store.listMeta(filter)).map((meta) => {
|
|
1790
|
+
const md = meta.metadata;
|
|
1791
|
+
const title = deriveSessionTitle(meta.firstUserTurn ? [meta.firstUserTurn] : [], md);
|
|
1792
|
+
return {
|
|
1793
|
+
id: meta.id,
|
|
1794
|
+
title,
|
|
1795
|
+
projectRoot: meta.projectRoot ?? null,
|
|
1796
|
+
cwd: typeof md.cwd === "string" ? md.cwd : meta.projectRoot ?? "",
|
|
1797
|
+
...meta.agentId ? { agentId: meta.agentId } : {},
|
|
1798
|
+
status: host.isSessionRunning(meta.id) ? "running" : meta.status === "running" && Date.now() - meta.updatedAt >= 3e5 ? "idle" : meta.status ?? "idle",
|
|
1799
|
+
createdAt: meta.createdAt ?? 0,
|
|
1800
|
+
updatedAt: meta.updatedAt,
|
|
1801
|
+
archived: md.archived === true,
|
|
1802
|
+
...(() => {
|
|
1803
|
+
const environment = sessionEnvironmentSummary(md);
|
|
1804
|
+
return environment ? { environment } : {};
|
|
1805
|
+
})(),
|
|
1806
|
+
...typeof md.providerKey === "string" ? { providerKey: md.providerKey } : {},
|
|
1807
|
+
...typeof md.modelId === "string" ? { modelId: md.modelId } : {},
|
|
1808
|
+
...typeof md.effort === "string" ? { effort: md.effort } : {},
|
|
1809
|
+
...md.modelOptions && typeof md.modelOptions === "object" ? { modelOptions: md.modelOptions } : {},
|
|
1810
|
+
...(() => {
|
|
1811
|
+
const fallbacks = sessionModelFallbacks(md);
|
|
1812
|
+
return fallbacks ? { modelFallbacks: fallbacks } : {};
|
|
1813
|
+
})()
|
|
1814
|
+
};
|
|
1815
|
+
}));
|
|
1816
|
+
const ids = await store.list(filter);
|
|
1817
|
+
const rows = [];
|
|
1818
|
+
for (const id of ids) {
|
|
1819
|
+
const data = await store.load(id);
|
|
1820
|
+
if (!data) continue;
|
|
1821
|
+
rows.push(toRow(host, data));
|
|
1822
|
+
}
|
|
1823
|
+
return applyArchive(rows);
|
|
1824
|
+
}
|
|
1825
|
+
function toRow(host, data) {
|
|
1826
|
+
const meta = data.metadata ?? {};
|
|
1827
|
+
const title = deriveSessionTitle(data.turns, meta);
|
|
1828
|
+
return {
|
|
1829
|
+
id: data.id,
|
|
1830
|
+
title,
|
|
1831
|
+
...isMainSession(meta) ? { isMain: true } : {},
|
|
1832
|
+
projectRoot: data.projectRoot ?? null,
|
|
1833
|
+
cwd: typeof meta.cwd === "string" ? meta.cwd : data.projectRoot ?? "",
|
|
1834
|
+
...typeof data.agentId === "string" ? { agentId: data.agentId } : {},
|
|
1835
|
+
status: host.isSessionRunning(data.id) ? "running" : data.status === "running" && Date.now() - data.updatedAt >= 3e5 ? "idle" : data.status,
|
|
1836
|
+
createdAt: data.createdAt,
|
|
1837
|
+
updatedAt: data.updatedAt,
|
|
1838
|
+
archived: meta.archived === true,
|
|
1839
|
+
...(() => {
|
|
1840
|
+
const environment = sessionEnvironmentSummary(meta);
|
|
1841
|
+
return environment ? { environment } : {};
|
|
1842
|
+
})(),
|
|
1843
|
+
...typeof meta.providerKey === "string" ? { providerKey: meta.providerKey } : {},
|
|
1844
|
+
...typeof meta.modelId === "string" ? { modelId: meta.modelId } : {},
|
|
1845
|
+
...typeof meta.effort === "string" ? { effort: meta.effort } : {},
|
|
1846
|
+
...meta.modelOptions && typeof meta.modelOptions === "object" ? { modelOptions: meta.modelOptions } : {},
|
|
1847
|
+
...(() => {
|
|
1848
|
+
const fallbacks = sessionModelFallbacks(meta);
|
|
1849
|
+
return fallbacks ? { modelFallbacks: fallbacks } : {};
|
|
1850
|
+
})()
|
|
1851
|
+
};
|
|
1852
|
+
}
|
|
1853
|
+
//#endregion
|
|
1854
|
+
//#region src/ipc/daemon/handlers/environments.ts
|
|
1855
|
+
function createEnvironmentHandlers({ host, env, setSettings }) {
|
|
1856
|
+
return {
|
|
1857
|
+
"sessions.retryEnvironment": async (p) => {
|
|
1858
|
+
await loadOrThrow(env.store, p.sessionId);
|
|
1859
|
+
await host.retryEnvironment(p.sessionId, { background: true });
|
|
1860
|
+
},
|
|
1861
|
+
"sessions.environmentActions": (p) => host.listEnvironmentActions(p.sessionId),
|
|
1862
|
+
"sessions.runEnvironmentAction": (p) => host.runEnvironmentAction(p.sessionId, p.actionId),
|
|
1863
|
+
"environments.list": (p) => host.listEnvironments(p.cwd ?? "", typeof p.kind === "string" ? p.kind : void 0),
|
|
1864
|
+
"environments.verify": (p) => host.verifyEnvironment(p.kind),
|
|
1865
|
+
"environments.update": async (p, peer) => {
|
|
1866
|
+
const write = applyEnvironmentSettingsChange(host.environments, env.readSettings(), p);
|
|
1867
|
+
for (const secret of write.secrets) host.environmentSecrets.set(p.kind, secret.key, secret.value);
|
|
1868
|
+
await setSettings({ patch: write.settingsPatch }, peer);
|
|
1869
|
+
}
|
|
1870
|
+
};
|
|
1871
|
+
}
|
|
1872
|
+
//#endregion
|
|
1873
|
+
//#region src/ipc/daemon/handlers/extensions.ts
|
|
1874
|
+
function createExtensionHandlers({ env, broadcastExcept }) {
|
|
1875
|
+
return { "extension.emit": async (p, peer) => {
|
|
1876
|
+
const extensionId = typeof p.extensionId === "string" ? p.extensionId : "";
|
|
1877
|
+
if (extensionId.length === 0 || extensionId.length > 256) return;
|
|
1878
|
+
if (typeof p.seq !== "number" || !Number.isFinite(p.seq)) return;
|
|
1879
|
+
const sessionId = typeof p.sessionId === "string" ? p.sessionId : void 0;
|
|
1880
|
+
const bytes = serializedPayloadBytes(p.payload);
|
|
1881
|
+
if (bytes === null || bytes > 65536) return;
|
|
1882
|
+
let registry = env.config.extensions;
|
|
1883
|
+
if (sessionId !== void 0) try {
|
|
1884
|
+
const data = await loadOrThrow(env.store, sessionId);
|
|
1885
|
+
const meta = data.metadata ?? {};
|
|
1886
|
+
const cwd = typeof meta.cwd === "string" ? meta.cwd : data.projectRoot ?? "";
|
|
1887
|
+
registry = (await env.runtimeConfig(cwd)).extensions;
|
|
1888
|
+
} catch {
|
|
1889
|
+
return;
|
|
1890
|
+
}
|
|
1891
|
+
if (!registry.loaded.some((ext) => !ext.error && ext.name === extensionId && ext.definition?.permissions?.includes("ipc") === true)) return;
|
|
1892
|
+
const nonce = typeof p.nonce === "string" ? p.nonce : void 0;
|
|
1893
|
+
const envelope = {
|
|
1894
|
+
extensionId,
|
|
1895
|
+
...sessionId !== void 0 ? { sessionId } : {},
|
|
1896
|
+
payload: p.payload,
|
|
1897
|
+
seq: p.seq,
|
|
1898
|
+
...nonce !== void 0 ? { nonce } : {}
|
|
1899
|
+
};
|
|
1900
|
+
env.extensionBus?.deliver(envelope);
|
|
1901
|
+
broadcastExcept(peer, {
|
|
1902
|
+
kind: "extension:signal",
|
|
1903
|
+
...envelope
|
|
1904
|
+
});
|
|
1905
|
+
} };
|
|
1906
|
+
}
|
|
1907
|
+
//#endregion
|
|
1908
|
+
//#region src/ipc/daemon/handlers/fs.ts
|
|
1909
|
+
function createFsHandlers({ workspace }) {
|
|
1910
|
+
return {
|
|
1911
|
+
"fs.roots": () => workspace.describe(),
|
|
1912
|
+
"fs.listDirs": (p) => workspace.list(optionalString(p?.path, "path"))
|
|
1913
|
+
};
|
|
1914
|
+
}
|
|
1915
|
+
//#endregion
|
|
1916
|
+
//#region src/ipc/daemon/handlers/history.ts
|
|
1917
|
+
const SEARCH_INDEX_CACHE_MAX = 64;
|
|
1918
|
+
function createHistoryHandlers({ host, store }) {
|
|
1919
|
+
/** Latest `history.searchAll` generation per peer — see the handler. */
|
|
1920
|
+
const searchGenerations = /* @__PURE__ */ new WeakMap();
|
|
1921
|
+
const searchIndexCache = /* @__PURE__ */ new Map();
|
|
1922
|
+
async function sessionSearchIndex(sessionId) {
|
|
1923
|
+
const liveTurns = host.getLiveTurns(sessionId);
|
|
1924
|
+
let turns;
|
|
1925
|
+
let key;
|
|
1926
|
+
if (liveTurns) {
|
|
1927
|
+
turns = [...liveTurns];
|
|
1928
|
+
key = `live:${turns.length}:${turns.at(-1)?.id ?? ""}`;
|
|
1929
|
+
} else {
|
|
1930
|
+
const data = await store.load(sessionId);
|
|
1931
|
+
if (!data) return null;
|
|
1932
|
+
turns = data.turns;
|
|
1933
|
+
key = `store:${data.updatedAt}:${data.turns.length}`;
|
|
1934
|
+
}
|
|
1935
|
+
const cached = searchIndexCache.get(sessionId);
|
|
1936
|
+
if (cached?.key === key) {
|
|
1937
|
+
searchIndexCache.delete(sessionId);
|
|
1938
|
+
searchIndexCache.set(sessionId, cached);
|
|
1939
|
+
return cached.index;
|
|
1940
|
+
}
|
|
1941
|
+
const index = await buildSessionSearchIndexAsync(turns);
|
|
1942
|
+
searchIndexCache.set(sessionId, {
|
|
1943
|
+
key,
|
|
1944
|
+
index
|
|
1945
|
+
});
|
|
1946
|
+
while (searchIndexCache.size > SEARCH_INDEX_CACHE_MAX) {
|
|
1947
|
+
const oldest = searchIndexCache.keys().next().value;
|
|
1948
|
+
if (oldest === void 0) break;
|
|
1949
|
+
searchIndexCache.delete(oldest);
|
|
1950
|
+
}
|
|
1951
|
+
return index;
|
|
1952
|
+
}
|
|
1953
|
+
return {
|
|
1954
|
+
"history.searchSession": async (p) => {
|
|
1955
|
+
const index = await sessionSearchIndex(p.sessionId);
|
|
1956
|
+
if (!index) return [];
|
|
1957
|
+
const limit = Number.isInteger(p.limit) && p.limit > 0 ? Math.min(p.limit, index.turns.length) : index.turns.length;
|
|
1958
|
+
const matches = await searchSessionIndexAsync(index, p.query, { limit });
|
|
1959
|
+
const order = new Map(index.turns.map((entry, i) => [entry.turn.id, i]));
|
|
1960
|
+
matches.sort((a, b) => (order.get(a.turn.id) ?? 0) - (order.get(b.turn.id) ?? 0));
|
|
1961
|
+
return matches;
|
|
1962
|
+
},
|
|
1963
|
+
"history.searchAll": async (p, peer) => {
|
|
1964
|
+
const generation = (searchGenerations.get(peer) ?? 0) + 1;
|
|
1965
|
+
searchGenerations.set(peer, generation);
|
|
1966
|
+
const results = {};
|
|
1967
|
+
const ids = Array.isArray(p.sessionIds) ? p.sessionIds.slice(0, 500) : [];
|
|
1968
|
+
for (const sessionId of ids) {
|
|
1969
|
+
if (searchGenerations.get(peer) !== generation) break;
|
|
1970
|
+
try {
|
|
1971
|
+
const index = await sessionSearchIndex(sessionId);
|
|
1972
|
+
if (!index) continue;
|
|
1973
|
+
const match = await searchSessionHistoryIndexAsync(index, p.query, p.excerptLimit ?? 3);
|
|
1974
|
+
if (match) results[sessionId] = match;
|
|
1975
|
+
} catch {}
|
|
1976
|
+
}
|
|
1977
|
+
return results;
|
|
1978
|
+
}
|
|
1979
|
+
};
|
|
1980
|
+
}
|
|
1981
|
+
//#endregion
|
|
1982
|
+
//#region src/ipc/daemon/handlers/hitl.ts
|
|
1983
|
+
function createHitlHandlers({ host }) {
|
|
1984
|
+
return {
|
|
1985
|
+
"interactions.list": (p) => host.interactions.list(p?.sessionId),
|
|
1986
|
+
"interactions.respond": (p) => host.interactions.respond(p.requestId, p.response),
|
|
1987
|
+
"interactions.cancel": (p) => host.interactions.cancel(p.requestId, p.reason),
|
|
1988
|
+
"interactions.rehydrate": (p) => host.interactions.rehydrate(p.sessionId),
|
|
1989
|
+
"approvals.resolve": (p) => host.approvals.resolve(p.requestId, p.decision),
|
|
1990
|
+
"approvals.denyAll": (p) => host.approvals.denyAll(p?.sessionId),
|
|
1991
|
+
"approvals.listPending": () => host.approvals.listPending()
|
|
1992
|
+
};
|
|
1993
|
+
}
|
|
1994
|
+
//#endregion
|
|
1995
|
+
//#region src/ipc/daemon/handlers/launch-overrides.ts
|
|
1996
|
+
/**
|
|
1997
|
+
* A confined host (a server) drops a `cwd` outside its workspace roots
|
|
1998
|
+
* itself (`ChatHost.setLaunchOverrides`): a window's own folder is a path on
|
|
1999
|
+
* ITS machine, which the session must not move to.
|
|
2000
|
+
*/
|
|
2001
|
+
function createLaunchOverrideHandlers({ host }) {
|
|
2002
|
+
/**
|
|
2003
|
+
* Each client's launch overrides per session, oldest setter first. The
|
|
2004
|
+
* session runs the most recent connected setter's; a client clearing, or
|
|
2005
|
+
* disconnecting, drops only its own, and the previous setter's apply again.
|
|
2006
|
+
*/
|
|
2007
|
+
const launchOverridesByPeer = /* @__PURE__ */ new Map();
|
|
2008
|
+
/** Run the latest connected setter's overrides (see `launchOverridesByPeer`). */
|
|
2009
|
+
const applyLaunchOverrides = (sessionId) => {
|
|
2010
|
+
const setters = launchOverridesByPeer.get(sessionId);
|
|
2011
|
+
const latest = setters ? [...setters.values()].at(-1) : void 0;
|
|
2012
|
+
if (!latest) launchOverridesByPeer.delete(sessionId);
|
|
2013
|
+
host.setLaunchOverrides(sessionId, latest ?? null);
|
|
2014
|
+
};
|
|
2015
|
+
return {
|
|
2016
|
+
methods: { "sessions.setLaunchOverrides": (p, peer) => {
|
|
2017
|
+
const { sessionId, ...rest } = p;
|
|
2018
|
+
const next = normalizeLaunchOverrides(rest);
|
|
2019
|
+
const setters = launchOverridesByPeer.get(sessionId) ?? /* @__PURE__ */ new Map();
|
|
2020
|
+
setters.delete(peer);
|
|
2021
|
+
if (next) setters.set(peer, next);
|
|
2022
|
+
launchOverridesByPeer.set(sessionId, setters);
|
|
2023
|
+
applyLaunchOverrides(sessionId);
|
|
2024
|
+
} },
|
|
2025
|
+
releasePeer(peer) {
|
|
2026
|
+
for (const [sessionId, setters] of launchOverridesByPeer) if (setters.delete(peer)) applyLaunchOverrides(sessionId);
|
|
2027
|
+
},
|
|
2028
|
+
forget(sessionId) {
|
|
2029
|
+
launchOverridesByPeer.delete(sessionId);
|
|
2030
|
+
}
|
|
2031
|
+
};
|
|
2032
|
+
}
|
|
2033
|
+
//#endregion
|
|
2034
|
+
//#region src/ipc/daemon/handlers/machine-serve.ts
|
|
2035
|
+
/**
|
|
2036
|
+
* Executors kept between their connections (tasks, terminals), keyed by the
|
|
2037
|
+
* token that attached them and the host id it named: a host id is the
|
|
2038
|
+
* client's word, so another token naming it gets an executor of its own,
|
|
2039
|
+
* never the first one's tasks, terminals or exits. Each token holds at most
|
|
2040
|
+
* {@link MAX_PER_PRINCIPAL} and evicts only its own idle ones.
|
|
2041
|
+
*/
|
|
2042
|
+
const MAX_PER_PRINCIPAL = 4;
|
|
2043
|
+
const MAX_ATTACHED_HOSTS = 32;
|
|
2044
|
+
var MachineServeError = class extends Error {
|
|
2045
|
+
code = MachineErrorCode.MethodNotAllowed;
|
|
2046
|
+
};
|
|
2047
|
+
/** A capacity limit: the client retries later (`../../machine/reverse.ts`). */
|
|
2048
|
+
var MachineServeBusyError = class extends MachineServeError {
|
|
2049
|
+
code = MachineErrorCode.Busy;
|
|
2050
|
+
};
|
|
2051
|
+
/** Who attached: the token's id (`''` on a host without auth, a loopback test server). */
|
|
2052
|
+
const principalOf = (peer) => peer.principal ?? "";
|
|
2053
|
+
function createMachineServeHandlers(input) {
|
|
2054
|
+
const { host } = input;
|
|
2055
|
+
const allowed = () => input.options?.allowed() === true;
|
|
2056
|
+
const served = /* @__PURE__ */ new Map();
|
|
2057
|
+
const byPeer = /* @__PURE__ */ new Map();
|
|
2058
|
+
const roots = () => {
|
|
2059
|
+
const { roots: list, confined } = host.workspace;
|
|
2060
|
+
if (!confined || list.length === 0) throw new MachineServeError("This host has no workspace roots to lend.");
|
|
2061
|
+
return [...list];
|
|
2062
|
+
};
|
|
2063
|
+
/** Make room for one more of `principal`'s: its own idle executor goes, never another token's. */
|
|
2064
|
+
const makeRoom = (principal) => {
|
|
2065
|
+
const own = [...served].filter(([, entry]) => entry.principal === principal);
|
|
2066
|
+
if (own.length < MAX_PER_PRINCIPAL && served.size < MAX_ATTACHED_HOSTS) return;
|
|
2067
|
+
const idle = own.find(([, entry]) => entry.peer === null);
|
|
2068
|
+
if (!idle) throw new MachineServeBusyError(own.length >= MAX_PER_PRINCIPAL ? `This token already has ${MAX_PER_PRINCIPAL} hosts attached here.` : `This server already lends itself to ${MAX_ATTACHED_HOSTS} hosts.`);
|
|
2069
|
+
served.delete(idle[0]);
|
|
2070
|
+
idle[1].executor.close();
|
|
2071
|
+
};
|
|
2072
|
+
async function executorFor(hostId, peer) {
|
|
2073
|
+
const principal = principalOf(peer);
|
|
2074
|
+
const key = `${principal}:${hostId}`;
|
|
2075
|
+
const existing = served.get(key);
|
|
2076
|
+
if (existing) {
|
|
2077
|
+
const previous = existing.peer;
|
|
2078
|
+
if (previous && previous !== peer) byPeer.delete(previous);
|
|
2079
|
+
existing.peer = peer;
|
|
2080
|
+
return existing.executor;
|
|
2081
|
+
}
|
|
2082
|
+
makeRoom(principal);
|
|
2083
|
+
const { createMachineExecutor } = await import("./executor-Cx5-8yqi.js");
|
|
2084
|
+
const entry = {
|
|
2085
|
+
peer,
|
|
2086
|
+
principal,
|
|
2087
|
+
executor: void 0
|
|
2088
|
+
};
|
|
2089
|
+
entry.executor = createMachineExecutor({
|
|
2090
|
+
roots: roots().map((path) => ({ path })),
|
|
2091
|
+
dataDir: join(input.dataDir(), "machine-self"),
|
|
2092
|
+
sandbox: input.settings(),
|
|
2093
|
+
credentialsDataDir: input.dataDir(),
|
|
2094
|
+
notify: (method, params) => {
|
|
2095
|
+
const target = entry.peer;
|
|
2096
|
+
if (!target || !input.isConnected(target)) return false;
|
|
2097
|
+
target.send({
|
|
2098
|
+
kind: "machine:notification",
|
|
2099
|
+
method,
|
|
2100
|
+
params
|
|
2101
|
+
});
|
|
2102
|
+
return true;
|
|
2103
|
+
}
|
|
2104
|
+
});
|
|
2105
|
+
served.set(key, entry);
|
|
2106
|
+
return entry.executor;
|
|
2107
|
+
}
|
|
2108
|
+
const attachedOf = (peer) => {
|
|
2109
|
+
const key = byPeer.get(peer);
|
|
2110
|
+
const entry = key ? served.get(key) : void 0;
|
|
2111
|
+
if (!entry || entry.peer !== peer) throw new MachineServeError("Send machine.attach first on this connection.");
|
|
2112
|
+
return entry;
|
|
2113
|
+
};
|
|
2114
|
+
return {
|
|
2115
|
+
allowed,
|
|
2116
|
+
methods: {
|
|
2117
|
+
"machine.attach": async (p, peer) => {
|
|
2118
|
+
if (!allowed()) throw new MachineServeError("This server does not lend itself as a machine (allowAsMachine is off).");
|
|
2119
|
+
if (typeof p.hostId !== "string" || !/^[\w-]{1,64}$/.test(p.hostId)) throw new MachineServeError("machine.attach: \"hostId\" must be 1 to 64 letters, digits, \"-\" or \"_\".");
|
|
2120
|
+
const executor = await executorFor(p.hostId, peer);
|
|
2121
|
+
byPeer.set(peer, `${principalOf(peer)}:${p.hostId}`);
|
|
2122
|
+
executor.linkUp();
|
|
2123
|
+
const { machineIdentity } = await import("./identity-CwfO8K-t.js").then((n) => n.t);
|
|
2124
|
+
return {
|
|
2125
|
+
machineId: machineIdentity(join(input.dataDir(), "machine-self")),
|
|
2126
|
+
name: toMachineName(input.hostName),
|
|
2127
|
+
roots: roots().map((path) => ({ path })),
|
|
2128
|
+
capabilities: {
|
|
2129
|
+
shell: true,
|
|
2130
|
+
terminals: typeof globalThis.Bun !== "undefined",
|
|
2131
|
+
background: true,
|
|
2132
|
+
worktrees: true,
|
|
2133
|
+
stream: true,
|
|
2134
|
+
sandboxed: executor.sandbox.on
|
|
2135
|
+
},
|
|
2136
|
+
version: ZIDANE_VERSION,
|
|
2137
|
+
protocolVersion: 1
|
|
2138
|
+
};
|
|
2139
|
+
},
|
|
2140
|
+
"machine.call": async (p, peer) => {
|
|
2141
|
+
if (!allowed()) throw new MachineServeError("This server does not lend itself as a machine (allowAsMachine is off).");
|
|
2142
|
+
if (typeof p.method !== "string") throw new MachineServeError("machine.call: \"method\" must be a string.");
|
|
2143
|
+
return await attachedOf(peer).executor.handle(p.method, p.params ?? {});
|
|
2144
|
+
},
|
|
2145
|
+
"machine.detach": (_p, peer) => {
|
|
2146
|
+
const key = byPeer.get(peer);
|
|
2147
|
+
if (!key) return { detached: false };
|
|
2148
|
+
byPeer.delete(peer);
|
|
2149
|
+
const entry = served.get(key);
|
|
2150
|
+
if (entry?.peer === peer) {
|
|
2151
|
+
entry.peer = null;
|
|
2152
|
+
entry.executor.linkLost();
|
|
2153
|
+
}
|
|
2154
|
+
return { detached: true };
|
|
2155
|
+
}
|
|
2156
|
+
},
|
|
2157
|
+
releasePeer(peer) {
|
|
2158
|
+
const key = byPeer.get(peer);
|
|
2159
|
+
if (!key) return;
|
|
2160
|
+
byPeer.delete(peer);
|
|
2161
|
+
const entry = served.get(key);
|
|
2162
|
+
if (entry?.peer === peer) {
|
|
2163
|
+
entry.peer = null;
|
|
2164
|
+
entry.executor.linkLost();
|
|
2165
|
+
}
|
|
2166
|
+
}
|
|
2167
|
+
};
|
|
2168
|
+
}
|
|
2169
|
+
//#endregion
|
|
2170
|
+
//#region src/ipc/daemon/handlers/oauth-flows.ts
|
|
2171
|
+
/** How long a flow waits for its code. */
|
|
2172
|
+
const OAUTH_FLOW_TTL_MS = 6e5;
|
|
2173
|
+
/** Open flows per connection. */
|
|
2174
|
+
const MAX_FLOWS_PER_PEER = 4;
|
|
2175
|
+
/** How long a login may take to produce its URL. */
|
|
2176
|
+
const BEGIN_TIMEOUT_MS = 3e4;
|
|
2177
|
+
function stepChannel(signal) {
|
|
2178
|
+
let current;
|
|
2179
|
+
const waiters = [];
|
|
2180
|
+
return {
|
|
2181
|
+
ask: (step) => new Promise((resolve, reject) => {
|
|
2182
|
+
if (signal.aborted) return reject(/* @__PURE__ */ new Error("The sign-in was cancelled."));
|
|
2183
|
+
const full = {
|
|
2184
|
+
...step,
|
|
2185
|
+
id: randomBytes(9).toString("base64url")
|
|
2186
|
+
};
|
|
2187
|
+
current = {
|
|
2188
|
+
step: full,
|
|
2189
|
+
resolve
|
|
2190
|
+
};
|
|
2191
|
+
signal.addEventListener("abort", () => reject(/* @__PURE__ */ new Error("The sign-in was cancelled.")), { once: true });
|
|
2192
|
+
for (const waiter of waiters.splice(0)) waiter(full);
|
|
2193
|
+
}),
|
|
2194
|
+
pending: () => current?.step,
|
|
2195
|
+
next: () => current ? Promise.resolve(current.step) : new Promise((resolve) => waiters.push(resolve)),
|
|
2196
|
+
answer(stepId, value) {
|
|
2197
|
+
const step = current?.step;
|
|
2198
|
+
if (!step || step.id !== stepId) throw new Error("That sign-in is not waiting on this question (answered already, or another one).");
|
|
2199
|
+
if (step.kind === "select" && value !== void 0 && !step.options.some((option) => option.id === value)) throw new InvalidParamsError(`"${value}" is not one of the choices.`);
|
|
2200
|
+
if (step.kind === "prompt" && (value === void 0 || value.trim() === "" && !step.allowEmpty)) throw new InvalidParamsError("That question needs an answer.");
|
|
2201
|
+
const { resolve } = current;
|
|
2202
|
+
current = void 0;
|
|
2203
|
+
resolve(value);
|
|
2204
|
+
}
|
|
2205
|
+
};
|
|
2206
|
+
}
|
|
2207
|
+
function deferred() {
|
|
2208
|
+
let resolve;
|
|
2209
|
+
return {
|
|
2210
|
+
promise: new Promise((r) => {
|
|
2211
|
+
resolve = r;
|
|
2212
|
+
}),
|
|
2213
|
+
resolve
|
|
2214
|
+
};
|
|
2215
|
+
}
|
|
2216
|
+
const LOOPBACK_HOSTS = /* @__PURE__ */ new Set([
|
|
2217
|
+
"127.0.0.1",
|
|
2218
|
+
"localhost",
|
|
2219
|
+
"[::1]",
|
|
2220
|
+
"::1"
|
|
2221
|
+
]);
|
|
2222
|
+
/** `http://<loopback>:<port>/<path>`, nothing else: where an MCP sign-in may send the browser. */
|
|
2223
|
+
function loopbackRedirect(raw) {
|
|
2224
|
+
let url;
|
|
2225
|
+
try {
|
|
2226
|
+
url = typeof raw === "string" ? new URL(raw) : void 0;
|
|
2227
|
+
} catch {}
|
|
2228
|
+
if (!url || url.protocol !== "http:" || !LOOPBACK_HOSTS.has(url.hostname) || !url.port || url.username || url.password || url.search || url.hash) throw new InvalidParamsError("redirectUri must be a loopback URL on the client (http://127.0.0.1:<port>/callback).");
|
|
2229
|
+
return url.toString();
|
|
2230
|
+
}
|
|
2231
|
+
/** `state` in what a user pasted: a callback URL, a `code=…&state=…` query, or Anthropic's `code#state`. */
|
|
2232
|
+
function stateIn(input) {
|
|
2233
|
+
const value = input.trim();
|
|
2234
|
+
try {
|
|
2235
|
+
return new URL(value).searchParams.get("state") ?? void 0;
|
|
2236
|
+
} catch {}
|
|
2237
|
+
if (value.includes("#")) return value.split("#", 2)[1];
|
|
2238
|
+
if (value.includes("state=")) return new URLSearchParams(value).get("state") ?? void 0;
|
|
2239
|
+
}
|
|
2240
|
+
/** The code in a pasted callback URL / query / `code#state`. */
|
|
2241
|
+
function codeIn(input) {
|
|
2242
|
+
const value = input.trim();
|
|
2243
|
+
try {
|
|
2244
|
+
return new URL(value).searchParams.get("code") ?? void 0;
|
|
2245
|
+
} catch {}
|
|
2246
|
+
if (value.includes("#")) return value.split("#", 2)[0];
|
|
2247
|
+
if (value.includes("code=")) return new URLSearchParams(value).get("code") ?? void 0;
|
|
2248
|
+
return value || void 0;
|
|
2249
|
+
}
|
|
2250
|
+
function isLoopbackUrl(raw) {
|
|
2251
|
+
if (!raw) return false;
|
|
2252
|
+
try {
|
|
2253
|
+
const url = new URL(raw);
|
|
2254
|
+
return url.protocol === "http:" && LOOPBACK_HOSTS.has(url.hostname);
|
|
2255
|
+
} catch {
|
|
2256
|
+
return false;
|
|
2257
|
+
}
|
|
2258
|
+
}
|
|
2259
|
+
/** What a provider's manual-input path reads: the pasted text, or the callback URL rebuilt from a code + state. */
|
|
2260
|
+
function manualInput(delivery, redirectUri) {
|
|
2261
|
+
if ("input" in delivery) return delivery.input;
|
|
2262
|
+
const query = new URLSearchParams({
|
|
2263
|
+
code: delivery.code,
|
|
2264
|
+
...delivery.state !== void 0 ? { state: delivery.state } : {}
|
|
2265
|
+
}).toString();
|
|
2266
|
+
return redirectUri ? `${redirectUri}?${query}` : query;
|
|
2267
|
+
}
|
|
2268
|
+
/** First of: what the login produced, its early end, a timeout, the abort. */
|
|
2269
|
+
async function firstOf(started, login, signal) {
|
|
2270
|
+
let timer;
|
|
2271
|
+
try {
|
|
2272
|
+
return await Promise.race([
|
|
2273
|
+
started,
|
|
2274
|
+
login.then(() => {
|
|
2275
|
+
throw new Error("The sign-in finished without a browser step (already signed in?).");
|
|
2276
|
+
}),
|
|
2277
|
+
new Promise((_, reject) => {
|
|
2278
|
+
timer = setTimeout(reject, BEGIN_TIMEOUT_MS, /* @__PURE__ */ new Error(`The sign-in did not produce an authorization URL within ${BEGIN_TIMEOUT_MS / 1e3}s.`));
|
|
2279
|
+
signal.addEventListener("abort", () => reject(/* @__PURE__ */ new Error("The sign-in was cancelled.")), { once: true });
|
|
2280
|
+
})
|
|
2281
|
+
]);
|
|
2282
|
+
} finally {
|
|
2283
|
+
clearTimeout(timer);
|
|
2284
|
+
}
|
|
2285
|
+
}
|
|
2286
|
+
/**
|
|
2287
|
+
* A provider's login, running here for a client: its URL (once known), its
|
|
2288
|
+
* questions, the code the client delivers, its stored result. Refused for a
|
|
2289
|
+
* provider that would listen on this machine (see the module header).
|
|
2290
|
+
*/
|
|
2291
|
+
function startProviderLogin(auth, provider) {
|
|
2292
|
+
const descriptor = auth.descriptorOf(provider);
|
|
2293
|
+
if (!descriptor.oauthProvider) throw new Error(`OAuth not supported for ${descriptor.label} (${descriptor.key}). Use an API key instead.`);
|
|
2294
|
+
if (!canSignInRemotely(descriptor.oauthProvider)) throw new Error(`${descriptor.label}'s sign-in can't run from another computer: it waits for the browser on a port of the server's own machine. Sign in on the server machine or use an API key.`);
|
|
2295
|
+
const abort = new AbortController();
|
|
2296
|
+
const code = deferred();
|
|
2297
|
+
const steps = stepChannel(abort.signal);
|
|
2298
|
+
const url = deferred();
|
|
2299
|
+
const login = runOAuthLogin(descriptor, {
|
|
2300
|
+
onUrl: (u, instructions) => url.resolve({
|
|
2301
|
+
url: u,
|
|
2302
|
+
...instructions ? { instructions } : {}
|
|
2303
|
+
}),
|
|
2304
|
+
onDeviceCode: (info) => url.resolve({
|
|
2305
|
+
url: info.verificationUri,
|
|
2306
|
+
userCode: info.userCode,
|
|
2307
|
+
device: true
|
|
2308
|
+
}),
|
|
2309
|
+
onManualCodeInput: () => code.promise,
|
|
2310
|
+
onPrompt: (prompt) => steps.ask({
|
|
2311
|
+
kind: "prompt",
|
|
2312
|
+
message: prompt.message,
|
|
2313
|
+
...prompt.placeholder ? { placeholder: prompt.placeholder } : {},
|
|
2314
|
+
...prompt.allowEmpty ? { allowEmpty: true } : {}
|
|
2315
|
+
}).then((value) => value ?? ""),
|
|
2316
|
+
onSelect: (prompt) => steps.ask({
|
|
2317
|
+
kind: "select",
|
|
2318
|
+
message: prompt.message,
|
|
2319
|
+
options: prompt.options.map((option) => ({
|
|
2320
|
+
id: option.id,
|
|
2321
|
+
label: option.label
|
|
2322
|
+
}))
|
|
2323
|
+
}),
|
|
2324
|
+
signal: abort.signal,
|
|
2325
|
+
callbackServer: false,
|
|
2326
|
+
openBrowser: false
|
|
2327
|
+
});
|
|
2328
|
+
login.catch(() => void 0);
|
|
2329
|
+
let redirectUri;
|
|
2330
|
+
const started = url.promise.then((first) => {
|
|
2331
|
+
if (!first.device) {
|
|
2332
|
+
const redirect = new URL(first.url).searchParams.get("redirect_uri");
|
|
2333
|
+
if (isLoopbackUrl(redirect)) redirectUri = redirect;
|
|
2334
|
+
}
|
|
2335
|
+
return {
|
|
2336
|
+
url: first.url,
|
|
2337
|
+
mode: first.device ? "device" : redirectUri ? "loopback" : "browser",
|
|
2338
|
+
...redirectUri ? { redirectUri } : {},
|
|
2339
|
+
...first.userCode ? { userCode: first.userCode } : {},
|
|
2340
|
+
...first.instructions ? { instructions: first.instructions } : {}
|
|
2341
|
+
};
|
|
2342
|
+
});
|
|
2343
|
+
started.catch(() => void 0);
|
|
2344
|
+
return {
|
|
2345
|
+
login,
|
|
2346
|
+
abort,
|
|
2347
|
+
result: login.then((credentials) => {
|
|
2348
|
+
setProviderCredential(auth.dataDir(), descriptor, {
|
|
2349
|
+
kind: "oauth",
|
|
2350
|
+
...credentials
|
|
2351
|
+
});
|
|
2352
|
+
auth.applied(descriptor);
|
|
2353
|
+
return { provider: auth.statusOf(descriptor.key) };
|
|
2354
|
+
}),
|
|
2355
|
+
steps,
|
|
2356
|
+
started,
|
|
2357
|
+
deliver: (delivery) => code.resolve(manualInput(delivery, redirectUri))
|
|
2358
|
+
};
|
|
2359
|
+
}
|
|
2360
|
+
/** The flow `flowId` names, if it exists and belongs to this connection. */
|
|
2361
|
+
function flowOf(flows, flowId, peer) {
|
|
2362
|
+
const flow = typeof flowId === "string" ? flows.get(flowId) : void 0;
|
|
2363
|
+
if (!flow) throw new Error("No sign-in with that id: it finished, expired or was cancelled. Start it again.");
|
|
2364
|
+
if (flow.peer !== peer) throw new Error("That sign-in belongs to another connection.");
|
|
2365
|
+
return flow;
|
|
2366
|
+
}
|
|
2367
|
+
/** Hand the flow its code (from the client's loopback) or pasted input, once, after checking its `state`. */
|
|
2368
|
+
function deliverCode(flow, p, end) {
|
|
2369
|
+
const code = typeof p.code === "string" && p.code !== "" ? p.code : void 0;
|
|
2370
|
+
const input = typeof p.input === "string" && p.input.trim() !== "" ? p.input : void 0;
|
|
2371
|
+
if (code === void 0 && input === void 0) return;
|
|
2372
|
+
if (flow.delivered) throw new Error("That sign-in already received its code.");
|
|
2373
|
+
const state = code !== void 0 ? typeof p.state === "string" ? p.state : void 0 : stateIn(input);
|
|
2374
|
+
if (flow.expectedState !== void 0 && (state !== void 0 || code !== void 0) && state !== flow.expectedState) {
|
|
2375
|
+
end(flow, "OAuth state mismatch.");
|
|
2376
|
+
throw new Error("OAuth state mismatch: the sign-in was refused. Start it again.");
|
|
2377
|
+
}
|
|
2378
|
+
flow.delivered = true;
|
|
2379
|
+
flow.deliver(code !== void 0 ? {
|
|
2380
|
+
code,
|
|
2381
|
+
...state !== void 0 ? { state } : {}
|
|
2382
|
+
} : { input });
|
|
2383
|
+
}
|
|
2384
|
+
function createOAuthFlowHandlers({ host, env, auth, isConnected, ttlMs = OAUTH_FLOW_TTL_MS }) {
|
|
2385
|
+
const flows = /* @__PURE__ */ new Map();
|
|
2386
|
+
const end = (flow, reason) => {
|
|
2387
|
+
clearTimeout(flow.timer);
|
|
2388
|
+
flows.delete(flow.id);
|
|
2389
|
+
if (!flow.abort.signal.aborted) flow.abort.abort(new Error(reason));
|
|
2390
|
+
};
|
|
2391
|
+
/** Register a flow whose login is already running; it ends at expiry, on cancel, or once settled and collected. */
|
|
2392
|
+
function register(peer, parts) {
|
|
2393
|
+
const id = randomBytes(24).toString("base64url");
|
|
2394
|
+
const flow = {
|
|
2395
|
+
id,
|
|
2396
|
+
peer,
|
|
2397
|
+
expiresAt: Date.now() + ttlMs,
|
|
2398
|
+
delivered: false,
|
|
2399
|
+
...parts,
|
|
2400
|
+
timer: void 0
|
|
2401
|
+
};
|
|
2402
|
+
flow.timer = setTimeout(end, ttlMs, flow, "The sign-in expired.");
|
|
2403
|
+
flow.timer.unref?.();
|
|
2404
|
+
flow.result.catch(() => void 0);
|
|
2405
|
+
flows.set(id, flow);
|
|
2406
|
+
return flow;
|
|
2407
|
+
}
|
|
2408
|
+
const ownFlows = (peer) => [...flows.values()].filter((flow) => flow.peer === peer);
|
|
2409
|
+
/** MCP begins in flight per connection: counted against the cap before their flow registers. */
|
|
2410
|
+
const startingMcp = /* @__PURE__ */ new Map();
|
|
2411
|
+
async function beginProvider(peer, provider) {
|
|
2412
|
+
const { login, ...parts } = startProviderLogin(auth, provider);
|
|
2413
|
+
const flow = register(peer, parts);
|
|
2414
|
+
try {
|
|
2415
|
+
return await stateOf(flow, login);
|
|
2416
|
+
} catch (err) {
|
|
2417
|
+
end(flow, "The sign-in did not start.");
|
|
2418
|
+
throw err;
|
|
2419
|
+
}
|
|
2420
|
+
}
|
|
2421
|
+
/** The flow's URL, or the question it waits on first; its `state` and redirect recorded once the URL is known. */
|
|
2422
|
+
async function stateOf(flow, login) {
|
|
2423
|
+
const steps = flow.steps;
|
|
2424
|
+
const next = await firstOf(Promise.race([flow.started.then((begin) => ({ begin })), steps.next().then((step) => ({ step }))]), login, flow.abort.signal);
|
|
2425
|
+
if ("step" in next) return {
|
|
2426
|
+
flowId: flow.id,
|
|
2427
|
+
url: "",
|
|
2428
|
+
mode: "step",
|
|
2429
|
+
step: next.step,
|
|
2430
|
+
expiresAt: flow.expiresAt
|
|
2431
|
+
};
|
|
2432
|
+
const begin = next.begin;
|
|
2433
|
+
flow.begun = begin;
|
|
2434
|
+
if (begin.mode !== "device") {
|
|
2435
|
+
flow.expectedState = new URL(begin.url).searchParams.get("state") ?? void 0;
|
|
2436
|
+
if (begin.redirectUri) flow.redirectUri = begin.redirectUri;
|
|
2437
|
+
}
|
|
2438
|
+
return {
|
|
2439
|
+
flowId: flow.id,
|
|
2440
|
+
...begin,
|
|
2441
|
+
expiresAt: flow.expiresAt
|
|
2442
|
+
};
|
|
2443
|
+
}
|
|
2444
|
+
async function beginMcp(peer, p) {
|
|
2445
|
+
const redirectUri = loopbackRedirect(p.redirectUri);
|
|
2446
|
+
const dir = catalogFolder({
|
|
2447
|
+
env,
|
|
2448
|
+
host
|
|
2449
|
+
}, p.cwd, p.kind);
|
|
2450
|
+
const config = await env.runtimeConfig(dir);
|
|
2451
|
+
const settings = env.readSettings();
|
|
2452
|
+
const { servers } = discoverProjectMcps({
|
|
2453
|
+
cwd: dir,
|
|
2454
|
+
prefix: config.prefix,
|
|
2455
|
+
foreignAgents: settings.importForeignConfigs === true,
|
|
2456
|
+
ruler: settings.importRulerConfigs !== false
|
|
2457
|
+
});
|
|
2458
|
+
const entry = servers.find((server) => server.config.name === p.mcpServer)?.config;
|
|
2459
|
+
if (!entry) throw new Error(`No MCP server "${p.mcpServer}" in that folder's catalog.`);
|
|
2460
|
+
const abort = new AbortController();
|
|
2461
|
+
const state = randomBytes(24).toString("base64url");
|
|
2462
|
+
const callbackCode = deferred();
|
|
2463
|
+
const started = deferred();
|
|
2464
|
+
const login = loginMcpServer(entry, {
|
|
2465
|
+
store: env.mcpCredentialStore?.() ?? createFileMcpCredentialStore(env.userDataDir()),
|
|
2466
|
+
signal: abort.signal,
|
|
2467
|
+
state,
|
|
2468
|
+
timeoutMs: ttlMs,
|
|
2469
|
+
callback: {
|
|
2470
|
+
redirectUri,
|
|
2471
|
+
promise: callbackCode.promise,
|
|
2472
|
+
close: async () => void 0
|
|
2473
|
+
},
|
|
2474
|
+
onAuthorizationUrl: (url) => started.resolve({ url: url.toString() })
|
|
2475
|
+
});
|
|
2476
|
+
login.catch(() => void 0);
|
|
2477
|
+
const first = await firstOf(started.promise, login, abort.signal).catch((err) => {
|
|
2478
|
+
abort.abort();
|
|
2479
|
+
throw err;
|
|
2480
|
+
});
|
|
2481
|
+
const flow = register(peer, {
|
|
2482
|
+
abort,
|
|
2483
|
+
result: login.then(() => {
|
|
2484
|
+
host.rebuildAllAgents({ rediscover: true }).catch(() => void 0);
|
|
2485
|
+
return { mcpServer: entry.name };
|
|
2486
|
+
}),
|
|
2487
|
+
expectedState: state,
|
|
2488
|
+
redirectUri,
|
|
2489
|
+
deliver: (delivery) => {
|
|
2490
|
+
if ("input" in delivery) {
|
|
2491
|
+
const code = codeIn(delivery.input);
|
|
2492
|
+
const pastedState = stateIn(delivery.input);
|
|
2493
|
+
if (code) callbackCode.resolve({
|
|
2494
|
+
code,
|
|
2495
|
+
...pastedState !== void 0 ? { state: pastedState } : {}
|
|
2496
|
+
});
|
|
2497
|
+
return;
|
|
2498
|
+
}
|
|
2499
|
+
callbackCode.resolve(delivery);
|
|
2500
|
+
}
|
|
2501
|
+
});
|
|
2502
|
+
return {
|
|
2503
|
+
flowId: flow.id,
|
|
2504
|
+
url: first.url,
|
|
2505
|
+
mode: "loopback",
|
|
2506
|
+
redirectUri,
|
|
2507
|
+
expiresAt: flow.expiresAt
|
|
2508
|
+
};
|
|
2509
|
+
}
|
|
2510
|
+
return {
|
|
2511
|
+
methods: {
|
|
2512
|
+
"auth.oauth.begin": async (p, peer) => {
|
|
2513
|
+
const target = [p.provider, p.mcpServer].filter((value) => value !== void 0);
|
|
2514
|
+
if (target.length !== 1 || typeof target[0] !== "string" || target[0] === "") throw new InvalidParamsError("auth.oauth.begin needs exactly one of \"provider\" or \"mcpServer\".");
|
|
2515
|
+
if (ownFlows(peer).length + (startingMcp.get(peer) ?? 0) >= MAX_FLOWS_PER_PEER) throw new Error(`This connection already has ${MAX_FLOWS_PER_PEER} sign-ins open: finish or cancel one first.`);
|
|
2516
|
+
let begun;
|
|
2517
|
+
if (p.provider !== void 0) begun = await beginProvider(peer, p.provider);
|
|
2518
|
+
else {
|
|
2519
|
+
startingMcp.set(peer, (startingMcp.get(peer) ?? 0) + 1);
|
|
2520
|
+
try {
|
|
2521
|
+
begun = await beginMcp(peer, {
|
|
2522
|
+
mcpServer: p.mcpServer,
|
|
2523
|
+
...p.cwd !== void 0 ? { cwd: p.cwd } : {},
|
|
2524
|
+
...p.kind !== void 0 ? { kind: p.kind } : {},
|
|
2525
|
+
...p.redirectUri !== void 0 ? { redirectUri: p.redirectUri } : {}
|
|
2526
|
+
});
|
|
2527
|
+
} finally {
|
|
2528
|
+
const left = (startingMcp.get(peer) ?? 1) - 1;
|
|
2529
|
+
if (left > 0) startingMcp.set(peer, left);
|
|
2530
|
+
else startingMcp.delete(peer);
|
|
2531
|
+
}
|
|
2532
|
+
}
|
|
2533
|
+
if (!isConnected(peer)) {
|
|
2534
|
+
const flow = flows.get(begun.flowId);
|
|
2535
|
+
if (flow) end(flow, "The connection closed.");
|
|
2536
|
+
}
|
|
2537
|
+
return begun;
|
|
2538
|
+
},
|
|
2539
|
+
"auth.oauth.complete": async (p, peer) => {
|
|
2540
|
+
const flow = flowOf(flows, p.flowId, peer);
|
|
2541
|
+
deliverCode(flow, p, end);
|
|
2542
|
+
let asking = false;
|
|
2543
|
+
try {
|
|
2544
|
+
const outcome = await (flow.steps ? Promise.race([flow.result, flow.steps.next().then((step) => ({ step }))]) : flow.result);
|
|
2545
|
+
asking = outcome.step !== void 0;
|
|
2546
|
+
return outcome;
|
|
2547
|
+
} catch (err) {
|
|
2548
|
+
throw new Error(flow.abort.signal.aborted && flow.abort.signal.reason instanceof Error ? flow.abort.signal.reason.message : err instanceof Error ? err.message : String(err));
|
|
2549
|
+
} finally {
|
|
2550
|
+
if (!asking) end(flow, "The sign-in finished.");
|
|
2551
|
+
}
|
|
2552
|
+
},
|
|
2553
|
+
"auth.oauth.respond": async (p, peer) => {
|
|
2554
|
+
const flow = flowOf(flows, p.flowId, peer);
|
|
2555
|
+
if (!flow.steps || typeof p.stepId !== "string") throw new InvalidParamsError("auth.oauth.respond needs the flow's \"stepId\".");
|
|
2556
|
+
if (p.value !== void 0 && typeof p.value !== "string") throw new InvalidParamsError("\"value\" must be a string.");
|
|
2557
|
+
const cancelled = p.value === void 0 && flow.steps.pending()?.kind === "select";
|
|
2558
|
+
flow.steps.answer(p.stepId, p.value);
|
|
2559
|
+
if (cancelled) {
|
|
2560
|
+
end(flow, "The sign-in was cancelled.");
|
|
2561
|
+
throw new Error("The sign-in was cancelled (no choice made).");
|
|
2562
|
+
}
|
|
2563
|
+
return flow.begun ? {
|
|
2564
|
+
flowId: flow.id,
|
|
2565
|
+
...flow.begun,
|
|
2566
|
+
expiresAt: flow.expiresAt
|
|
2567
|
+
} : stateOf(flow, flow.result);
|
|
2568
|
+
},
|
|
2569
|
+
"auth.oauth.cancel": (p, peer) => {
|
|
2570
|
+
const flow = typeof p.flowId === "string" ? flows.get(p.flowId) : void 0;
|
|
2571
|
+
if (!flow || flow.peer !== peer) return { cancelled: false };
|
|
2572
|
+
end(flow, "The sign-in was cancelled.");
|
|
2573
|
+
return { cancelled: true };
|
|
2574
|
+
}
|
|
2575
|
+
},
|
|
2576
|
+
releasePeer(peer) {
|
|
2577
|
+
for (const flow of ownFlows(peer)) end(flow, "The connection closed.");
|
|
2578
|
+
}
|
|
2579
|
+
};
|
|
2580
|
+
}
|
|
2581
|
+
//#endregion
|
|
2582
|
+
//#region src/ipc/daemon/handlers/orchestration.ts
|
|
2583
|
+
function createOrchestrationHandlers({ host }) {
|
|
2584
|
+
return {
|
|
2585
|
+
"orchestration.main": () => host.orchestration.ensureMain(),
|
|
2586
|
+
"orchestration.createWorker": ({ mainId, ...params }) => host.orchestration.createWorker(mainId, params),
|
|
2587
|
+
"orchestration.list": (p) => host.orchestration.list(p.mainId, p.limit),
|
|
2588
|
+
"orchestration.read": async (p) => await host.orchestration.canOrchestrate(p.mainId, p.sessionId) ? host.orchestration.read(p.sessionId) : null,
|
|
2589
|
+
"orchestration.send": (p) => host.orchestration.send(p.mainId, p.sessionId, p.prompt, p.wake),
|
|
2590
|
+
"orchestration.steer": (p) => host.orchestration.steer(p.mainId, p.sessionId, p.message),
|
|
2591
|
+
"orchestration.cancel": (p) => host.orchestration.cancel(p.mainId, p.sessionId),
|
|
2592
|
+
"orchestration.transcript": (p) => host.orchestration.transcript(p.mainId, p.sessionId, p.limit)
|
|
2593
|
+
};
|
|
2594
|
+
}
|
|
2595
|
+
//#endregion
|
|
2596
|
+
//#region src/ipc/daemon/handlers/presence.ts
|
|
2597
|
+
function createPresenceHandlers({ env, peers }) {
|
|
2598
|
+
return {
|
|
2599
|
+
"presence.list": () => peers.computePresence(),
|
|
2600
|
+
"presence.watch": (p, peer) => {
|
|
2601
|
+
peer.watching = p.sessionId;
|
|
2602
|
+
peers.broadcastPresence();
|
|
2603
|
+
env.store.load(p.sessionId).then((data) => {
|
|
2604
|
+
if (!data) return;
|
|
2605
|
+
const meta = data.metadata ?? {};
|
|
2606
|
+
const cwd = typeof meta.cwd === "string" ? meta.cwd : data.projectRoot ?? "";
|
|
2607
|
+
return env.runtimeConfig(cwd);
|
|
2608
|
+
}).catch(() => void 0);
|
|
2609
|
+
}
|
|
2610
|
+
};
|
|
2611
|
+
}
|
|
2612
|
+
//#endregion
|
|
2613
|
+
//#region src/ipc/daemon/handlers/queue.ts
|
|
2614
|
+
function createQueueHandlers({ host, clearDraft }) {
|
|
2615
|
+
return {
|
|
2616
|
+
"queue.list": (p) => host.queueList(p.sessionId),
|
|
2617
|
+
"queue.add": async (p, peer) => {
|
|
2618
|
+
const text = typeof p.text === "string" ? p.text.slice(0, 65536) : "";
|
|
2619
|
+
const activatedSkills = Array.isArray(p.activatedSkills) ? p.activatedSkills.filter((name) => typeof name === "string") : void 0;
|
|
2620
|
+
const attachments = Array.isArray(p.attachments) ? boundAttachments(p.attachments) : void 0;
|
|
2621
|
+
const asCommand = typeof p.asCommand === "string" ? p.asCommand.slice(0, 128) : void 0;
|
|
2622
|
+
const result = await host.queueAdd(p.sessionId, text, activatedSkills, attachments, asCommand);
|
|
2623
|
+
if (p.keepDraft !== true) clearDraft(p.sessionId, peer);
|
|
2624
|
+
return result;
|
|
2625
|
+
},
|
|
2626
|
+
"queue.remove": (p) => host.queueRemove(p.sessionId, p.id),
|
|
2627
|
+
"queue.update": (p) => host.queueUpdate(p.sessionId, p.id, typeof p.text === "string" ? p.text.slice(0, 65536) : "", updatedAttachments(p.attachments)),
|
|
2628
|
+
"queue.attachments": (p) => host.queueAttachments(p.sessionId, p.id),
|
|
2629
|
+
"queue.pushNow": (p) => host.queuePushNow(p.sessionId, p.id)
|
|
2630
|
+
};
|
|
2631
|
+
}
|
|
2632
|
+
/**
|
|
2633
|
+
* The list `queue.update` stores in place of an entry's attachments. An entry
|
|
2634
|
+
* that is malformed or over the budget is refused, not dropped: the list
|
|
2635
|
+
* REPLACES what is stored, so a quietly shorter list would delete the rest.
|
|
2636
|
+
*/
|
|
2637
|
+
function updatedAttachments(list) {
|
|
2638
|
+
if (!Array.isArray(list)) return void 0;
|
|
2639
|
+
const bounded = boundAttachments(list);
|
|
2640
|
+
if (bounded.length !== list.length) throw new InvalidParamsError("attachments: an entry is malformed, or the list is over 24 MiB.");
|
|
2641
|
+
return bounded;
|
|
2642
|
+
}
|
|
2643
|
+
//#endregion
|
|
2644
|
+
//#region src/ipc/daemon/handlers/resume.ts
|
|
2645
|
+
function createResumeHandlers({ log }) {
|
|
2646
|
+
return { "sessions.resume": (p) => {
|
|
2647
|
+
if (p.epoch !== log.epoch) return {
|
|
2648
|
+
ok: false,
|
|
2649
|
+
reason: "epoch"
|
|
2650
|
+
};
|
|
2651
|
+
const signals = typeof p.afterSeq === "number" ? log.replay(p.sessionId, p.afterSeq) : null;
|
|
2652
|
+
return signals ? {
|
|
2653
|
+
ok: true,
|
|
2654
|
+
signals
|
|
2655
|
+
} : {
|
|
2656
|
+
ok: false,
|
|
2657
|
+
reason: "gap"
|
|
2658
|
+
};
|
|
2659
|
+
} };
|
|
2660
|
+
}
|
|
2661
|
+
//#endregion
|
|
2662
|
+
//#region src/ipc/daemon/handlers/session-reads.ts
|
|
2663
|
+
function pageBound(value, name, fallback, max) {
|
|
2664
|
+
return Math.min(nonNegativeInt(value, name) ?? fallback, max);
|
|
2665
|
+
}
|
|
2666
|
+
function createSessionReadHandlers({ host, store, snapshots }) {
|
|
2667
|
+
/** The document as the host holds it: the live copy (ahead of the store mid-run, and holding the todos), else the stored one. */
|
|
2668
|
+
const documentOf = async (sessionId) => {
|
|
2669
|
+
const live = host.getLiveSession(sessionId);
|
|
2670
|
+
if (live) return live.toJSON();
|
|
2671
|
+
const loaded = await loadSession(store, sessionId, { persistHeal: false });
|
|
2672
|
+
return loaded ? loaded.toJSON() : null;
|
|
2673
|
+
};
|
|
2674
|
+
return {
|
|
2675
|
+
"sessions.listMeta": (p) => {
|
|
2676
|
+
const projectRoot = p?.projectRoot;
|
|
2677
|
+
if (projectRoot !== void 0 && projectRoot !== null && typeof projectRoot !== "string") throw new InvalidParamsError("projectRoot must be a string or null.");
|
|
2678
|
+
return listSessionMeta(store, projectRoot !== void 0 ? { projectRoot } : void 0);
|
|
2679
|
+
},
|
|
2680
|
+
"sessions.getMeta": (p) => getSessionMeta(store, sessionIdOf(p)),
|
|
2681
|
+
"sessions.get": async (p) => {
|
|
2682
|
+
const sessionId = sessionIdOf(p);
|
|
2683
|
+
if (p.turns !== false) return documentOf(sessionId);
|
|
2684
|
+
const snapshot = await snapshots.document(store, sessionId, host.getLiveSession(sessionId));
|
|
2685
|
+
return snapshot && {
|
|
2686
|
+
...snapshot.data,
|
|
2687
|
+
turns: [],
|
|
2688
|
+
version: snapshot.version
|
|
2689
|
+
};
|
|
2690
|
+
},
|
|
2691
|
+
"sessions.getTurns": async (p) => {
|
|
2692
|
+
const sessionId = sessionIdOf(p);
|
|
2693
|
+
const snapshot = await snapshots.document(store, sessionId, host.getLiveSession(sessionId));
|
|
2694
|
+
if (!snapshot) return null;
|
|
2695
|
+
const from = pageBound(p.from, "from", 0, Number.MAX_SAFE_INTEGER);
|
|
2696
|
+
const limit = Math.max(1, pageBound(p.limit, "limit", 200, SESSION_TURNS_MAX_PAGE));
|
|
2697
|
+
const { turns } = snapshot.data;
|
|
2698
|
+
return {
|
|
2699
|
+
turns: turns.slice(from, from + limit),
|
|
2700
|
+
total: turns.length,
|
|
2701
|
+
version: snapshot.version
|
|
2702
|
+
};
|
|
2703
|
+
},
|
|
2704
|
+
"sessions.getToolResult": async (p) => {
|
|
2705
|
+
const sessionId = sessionIdOf(p);
|
|
2706
|
+
if (typeof p.callId !== "string" || p.callId.length === 0) throw new InvalidParamsError("callId must be a non-empty string.");
|
|
2707
|
+
const data = await documentOf(sessionId);
|
|
2708
|
+
if (!data) return null;
|
|
2709
|
+
const named = typeof p.turnId === "string" ? data.turns.filter((turn) => turn.id === p.turnId) : [];
|
|
2710
|
+
for (const turn of [...named, ...[...data.turns].reverse()]) for (const block of turn.content) if (block.type === "tool_result" && block.callId === p.callId) return toolResultText(block.output);
|
|
2711
|
+
return null;
|
|
2712
|
+
},
|
|
2713
|
+
"sessions.getHeader": async (p) => {
|
|
2714
|
+
const sessionId = sessionIdOf(p);
|
|
2715
|
+
const live = host.getLiveSession(sessionId);
|
|
2716
|
+
if (p.usage !== true) return live ? headerFromData(live.toJSON()) : getSessionHeader(store, sessionId);
|
|
2717
|
+
const data = live ? live.toJSON() : await store.load(sessionId);
|
|
2718
|
+
if (!data) return null;
|
|
2719
|
+
return {
|
|
2720
|
+
...live ? headerFromData(data) : await getSessionHeader(store, sessionId) ?? headerFromData({
|
|
2721
|
+
...data,
|
|
2722
|
+
id: sessionId
|
|
2723
|
+
}),
|
|
2724
|
+
contextTokens: lastContextSizeFromTurns(data.turns, data.runs),
|
|
2725
|
+
costUsd: sumRunCosts(data.runs)
|
|
2726
|
+
};
|
|
2727
|
+
},
|
|
2728
|
+
"sessions.setExtensionState": async (p) => {
|
|
2729
|
+
const sessionId = sessionIdOf(p);
|
|
2730
|
+
if (typeof p.extension !== "string" || typeof p.key !== "string") throw new InvalidParamsError("extension and key must be strings.");
|
|
2731
|
+
const session = host.getLiveSession(sessionId) ?? await loadSession(store, sessionId, { persistHeal: false });
|
|
2732
|
+
if (!session) throw new Error(`Session "${sessionId}" not found.`);
|
|
2733
|
+
if (p.value === void 0) await deleteExtensionSessionState(session, p.extension, p.key);
|
|
2734
|
+
else await setExtensionSessionState(session, p.extension, p.key, p.value);
|
|
2735
|
+
}
|
|
2736
|
+
};
|
|
2737
|
+
}
|
|
2738
|
+
//#endregion
|
|
2739
|
+
//#region src/ipc/daemon/handlers/settings.ts
|
|
2740
|
+
/**
|
|
2741
|
+
* Settings the host reads when it BUILDS an agent (not per call): changing
|
|
2742
|
+
* one rebuilds the live agents. `enabledPlugins` applies the plugin gate;
|
|
2743
|
+
* a live agent would otherwise keep a disabled plugin's skills and stdio MCP
|
|
2744
|
+
* servers until something unrelated rebuilt it.
|
|
2745
|
+
*/
|
|
2746
|
+
const AGENT_BUILD_KEYS = [
|
|
2747
|
+
"enabledMcps",
|
|
2748
|
+
"disabledMcpTools",
|
|
2749
|
+
"enabledSkills",
|
|
2750
|
+
"enabledPlugins",
|
|
2751
|
+
"importForeignConfigs",
|
|
2752
|
+
"importRulerConfigs",
|
|
2753
|
+
"artifacts"
|
|
2754
|
+
];
|
|
2755
|
+
function createSettingsHandlers({ host, env, broadcast }) {
|
|
2756
|
+
return {
|
|
2757
|
+
"runtime.reload": () => {
|
|
2758
|
+
const disposeOld = env.forgetRuntime();
|
|
2759
|
+
host.rebuildAllAgents().then(disposeOld, disposeOld);
|
|
2760
|
+
},
|
|
2761
|
+
"settings.get": () => env.readSettings(),
|
|
2762
|
+
"settings.set": (p) => {
|
|
2763
|
+
const patch = p?.patch;
|
|
2764
|
+
if (typeof patch !== "object" || patch === null || Array.isArray(patch)) throw new InvalidParamsError("patch must be an object of settings.");
|
|
2765
|
+
const before = env.readSettings();
|
|
2766
|
+
const next = env.writeSettings(p.patch);
|
|
2767
|
+
const changed = (key) => JSON.stringify(before[key]) !== JSON.stringify(next[key]);
|
|
2768
|
+
if (changed("enabledExtensions") || changed("loadProjectExtensions")) {
|
|
2769
|
+
const disposeOld = env.forgetRuntime();
|
|
2770
|
+
host.rebuildAllAgents({ rediscover: true }).then(disposeOld, disposeOld);
|
|
2771
|
+
} else if (AGENT_BUILD_KEYS.some(changed)) host.rebuildAllAgents({ rediscover: true }).catch(() => void 0);
|
|
2772
|
+
if (changed("interactionMode") && next.interactionMode === "skip") host.interactions.resolveAutomatically();
|
|
2773
|
+
host.applyAgentSettings(before, next);
|
|
2774
|
+
broadcast({
|
|
2775
|
+
kind: "settings:changed",
|
|
2776
|
+
settings: next
|
|
2777
|
+
});
|
|
2778
|
+
return next;
|
|
2779
|
+
}
|
|
2780
|
+
};
|
|
2781
|
+
}
|
|
2782
|
+
//#endregion
|
|
2783
|
+
//#region src/ipc/daemon/handlers/suggest.ts
|
|
2784
|
+
function createSuggestHandlers({ env, broadcastExcept }) {
|
|
2785
|
+
/**
|
|
2786
|
+
* Shared inline-autocomplete state — per-session latest-wins
|
|
2787
|
+
* coalescing for `suggest.resolve`. `seq` is the newest sequence seen
|
|
2788
|
+
* (stale requests return without computing); `controller` aborts an
|
|
2789
|
+
* in-flight compute when a newer request supersedes it (the GUI's
|
|
2790
|
+
* controller-map pattern, daemon-side). Entries outlive their compute
|
|
2791
|
+
* (the seq high-water mark), so the map is an LRU of 256 sessions:
|
|
2792
|
+
* refusing new keys at the cap instead would switch suggestions off for
|
|
2793
|
+
* every session past the 256th until the daemon restarts.
|
|
2794
|
+
*/
|
|
2795
|
+
const suggests = /* @__PURE__ */ new Map();
|
|
2796
|
+
return {
|
|
2797
|
+
methods: { "suggest.resolve": async (p, peer) => {
|
|
2798
|
+
const sessionId = String(p.sessionId);
|
|
2799
|
+
const seq = typeof p.seq === "number" ? p.seq : 0;
|
|
2800
|
+
const text = typeof p.text === "string" ? p.text.slice(0, 65536) : "";
|
|
2801
|
+
const caret = typeof p.caret === "number" ? Math.max(0, Math.min(Math.floor(p.caret), text.length)) : text.length;
|
|
2802
|
+
const prev = suggests.get(sessionId);
|
|
2803
|
+
if (prev && seq < prev.seq) return {
|
|
2804
|
+
suggestion: null,
|
|
2805
|
+
seq
|
|
2806
|
+
};
|
|
2807
|
+
prev?.controller?.abort();
|
|
2808
|
+
const controller = new AbortController();
|
|
2809
|
+
const entry = {
|
|
2810
|
+
seq,
|
|
2811
|
+
controller
|
|
2812
|
+
};
|
|
2813
|
+
suggests.delete(sessionId);
|
|
2814
|
+
suggests.set(sessionId, entry);
|
|
2815
|
+
if (suggests.size > 256) {
|
|
2816
|
+
const [oldest, evicted] = suggests.entries().next().value;
|
|
2817
|
+
evicted.controller?.abort();
|
|
2818
|
+
suggests.delete(oldest);
|
|
2819
|
+
}
|
|
2820
|
+
try {
|
|
2821
|
+
const data = await loadOrThrow(env.store, sessionId);
|
|
2822
|
+
const meta = data.metadata ?? {};
|
|
2823
|
+
const cwd = typeof meta.cwd === "string" ? meta.cwd : data.projectRoot ?? "";
|
|
2824
|
+
const config = await env.runtimeConfig(cwd);
|
|
2825
|
+
const providers = [createPathAutocompleteProvider({ cwd: () => cwd }), ...config.extensions.inlineAutocompleteProviders];
|
|
2826
|
+
const result = await resolveInlineSuggestion(providers, {
|
|
2827
|
+
text,
|
|
2828
|
+
cursor: caret,
|
|
2829
|
+
cwd,
|
|
2830
|
+
signal: controller.signal
|
|
2831
|
+
});
|
|
2832
|
+
if (controller.signal.aborted || suggests.get(sessionId) !== entry) return {
|
|
2833
|
+
suggestion: null,
|
|
2834
|
+
seq
|
|
2835
|
+
};
|
|
2836
|
+
const candidates = result ? [...result.suffixes] : [];
|
|
2837
|
+
const suggestion = candidates[0] ?? null;
|
|
2838
|
+
broadcastExcept(peer, {
|
|
2839
|
+
kind: "suggest:changed",
|
|
2840
|
+
sessionId,
|
|
2841
|
+
seq,
|
|
2842
|
+
basis: text,
|
|
2843
|
+
caret,
|
|
2844
|
+
suggestion,
|
|
2845
|
+
...candidates.length > 0 ? { candidates } : {}
|
|
2846
|
+
});
|
|
2847
|
+
return {
|
|
2848
|
+
suggestion,
|
|
2849
|
+
seq,
|
|
2850
|
+
...candidates.length > 0 ? { candidates } : {}
|
|
2851
|
+
};
|
|
2852
|
+
} finally {
|
|
2853
|
+
if (suggests.get(sessionId) === entry) entry.controller = null;
|
|
2854
|
+
}
|
|
2855
|
+
} },
|
|
2856
|
+
forget: (sessionId) => {
|
|
2857
|
+
suggests.get(sessionId)?.controller?.abort();
|
|
2858
|
+
suggests.delete(sessionId);
|
|
2859
|
+
}
|
|
2860
|
+
};
|
|
2861
|
+
}
|
|
2862
|
+
//#endregion
|
|
2863
|
+
//#region src/ipc/daemon/sync-apply.ts
|
|
2864
|
+
const MAX_SETTINGS_BYTES = 262144;
|
|
2865
|
+
const MAX_SYSTEM_MD_BYTES = 262144;
|
|
2866
|
+
const MAX_FILE_BYTES = 1048576;
|
|
2867
|
+
const MAX_SKILL_BYTES = 4194304;
|
|
2868
|
+
const MAX_FILES_BYTES = 16777216;
|
|
2869
|
+
const MAX_SKILL_FILES = 200;
|
|
2870
|
+
const MAX_SKILLS = 500;
|
|
2871
|
+
const MAX_PATH_DEPTH = 8;
|
|
2872
|
+
const MAX_SAFELIST_ENTRIES = 1e3;
|
|
2873
|
+
const MAX_ENTRY_LENGTH = 1e3;
|
|
2874
|
+
const NAME_RE = /^\w[\w@.-]{0,99}$/;
|
|
2875
|
+
const SEGMENT_RE = /^[^/\\\0]{1,255}$/;
|
|
2876
|
+
/** A skill file's relative path: plain segments, no `..`, bounded depth. */
|
|
2877
|
+
function checkRelativePath(path) {
|
|
2878
|
+
if (typeof path !== "string" || path === "" || isAbsolute(path) || path.includes("\\")) return null;
|
|
2879
|
+
const segments = path.split("/");
|
|
2880
|
+
if (segments.length > MAX_PATH_DEPTH || segments.some((s) => s === "." || s === ".." || !SEGMENT_RE.test(s))) return null;
|
|
2881
|
+
return segments.join("/");
|
|
2882
|
+
}
|
|
2883
|
+
/** Is `command` runnable here (an absolute path, or found on PATH)? Looks, never runs. */
|
|
2884
|
+
function commandAvailable(command) {
|
|
2885
|
+
const executable = (path) => {
|
|
2886
|
+
try {
|
|
2887
|
+
accessSync(path, constants.X_OK);
|
|
2888
|
+
return statSync(path).isFile();
|
|
2889
|
+
} catch {
|
|
2890
|
+
return false;
|
|
2891
|
+
}
|
|
2892
|
+
};
|
|
2893
|
+
if (command.includes("/")) return isAbsolute(command) && executable(command);
|
|
2894
|
+
return (process$1.env.PATH ?? "").split(delimiter).filter(Boolean).some((dir) => executable(join(dir, command)));
|
|
2895
|
+
}
|
|
2896
|
+
/** A folder's files, sorted, as one hash (symlinks hashed by target, never followed). Null when absent. */
|
|
2897
|
+
function hashDir(dir) {
|
|
2898
|
+
let stat;
|
|
2899
|
+
try {
|
|
2900
|
+
stat = lstatSync(dir);
|
|
2901
|
+
} catch {
|
|
2902
|
+
return null;
|
|
2903
|
+
}
|
|
2904
|
+
if (!stat.isDirectory()) return `not-a-folder:${stat.mode}`;
|
|
2905
|
+
const hash = createHash("sha256");
|
|
2906
|
+
const walk = (at, depth) => {
|
|
2907
|
+
if (depth > 9) return;
|
|
2908
|
+
for (const name of readdirSync(at).sort()) {
|
|
2909
|
+
const path = join(at, name);
|
|
2910
|
+
const rel = relative(dir, path).split(sep).join("/");
|
|
2911
|
+
const s = lstatSync(path);
|
|
2912
|
+
if (s.isDirectory()) walk(path, depth + 1);
|
|
2913
|
+
else if (s.isFile()) {
|
|
2914
|
+
hash.update(`f:${rel}\0`);
|
|
2915
|
+
hash.update(readFileSync(path));
|
|
2916
|
+
hash.update("\0");
|
|
2917
|
+
} else hash.update(`o:${rel}:${s.mode}\0`);
|
|
2918
|
+
}
|
|
2919
|
+
};
|
|
2920
|
+
walk(dir, 0);
|
|
2921
|
+
return hash.digest("hex");
|
|
2922
|
+
}
|
|
2923
|
+
/** The hash `hashDir` gives the folder these files make. */
|
|
2924
|
+
function hashFiles(files) {
|
|
2925
|
+
const hash = createHash("sha256");
|
|
2926
|
+
const sorted = [...files].sort((a, b) => compareWalkOrder(a.path, b.path));
|
|
2927
|
+
for (const file of sorted) {
|
|
2928
|
+
hash.update(`f:${file.path}\0`);
|
|
2929
|
+
hash.update(file.bytes);
|
|
2930
|
+
hash.update("\0");
|
|
2931
|
+
}
|
|
2932
|
+
return hash.digest("hex");
|
|
2933
|
+
}
|
|
2934
|
+
/** The order a sorted depth-first walk visits two relative paths. */
|
|
2935
|
+
function compareWalkOrder(a, b) {
|
|
2936
|
+
const as = a.split("/");
|
|
2937
|
+
const bs = b.split("/");
|
|
2938
|
+
for (let i = 0; i < Math.min(as.length, bs.length); i++) if (as[i] !== bs[i]) return as[i] < bs[i] ? -1 : 1;
|
|
2939
|
+
return as.length - bs.length;
|
|
2940
|
+
}
|
|
2941
|
+
function userSkillsRoot(ctx) {
|
|
2942
|
+
return projectUserPaths({
|
|
2943
|
+
subPath: "skills",
|
|
2944
|
+
cwd: ctx.home(),
|
|
2945
|
+
home: ctx.home(),
|
|
2946
|
+
prefix: ctx.prefix()
|
|
2947
|
+
}).find((p) => p.source === "user" && p.path.includes(`.${ctx.prefix().replace(/^\./, "")}`)).path;
|
|
2948
|
+
}
|
|
2949
|
+
function userMcpsPath(ctx) {
|
|
2950
|
+
return mcpsWritePath("user", {
|
|
2951
|
+
home: ctx.home(),
|
|
2952
|
+
cwd: ctx.home(),
|
|
2953
|
+
prefix: ctx.prefix()
|
|
2954
|
+
});
|
|
2955
|
+
}
|
|
2956
|
+
function userExtensionDirs(ctx, name) {
|
|
2957
|
+
return projectUserPaths({
|
|
2958
|
+
subPath: "extensions",
|
|
2959
|
+
cwd: ctx.home(),
|
|
2960
|
+
home: ctx.home(),
|
|
2961
|
+
prefix: ctx.prefix()
|
|
2962
|
+
}).filter((p) => p.source === "user").map((p) => join(p.path, name));
|
|
2963
|
+
}
|
|
2964
|
+
/** Is `field` free to write? An override (now or found by a changed value) is not. */
|
|
2965
|
+
function writable(record, field, currentHash, result) {
|
|
2966
|
+
if (record.overrides[field]) {
|
|
2967
|
+
result.skipped.push({
|
|
2968
|
+
field,
|
|
2969
|
+
reason: "override"
|
|
2970
|
+
});
|
|
2971
|
+
return false;
|
|
2972
|
+
}
|
|
2973
|
+
const known = record.fields[field];
|
|
2974
|
+
if (known?.hash !== void 0 && known.hash !== (currentHash ?? "absent")) {
|
|
2975
|
+
record.overrides[field] = { at: Date.now() };
|
|
2976
|
+
result.skipped.push({
|
|
2977
|
+
field,
|
|
2978
|
+
reason: "override"
|
|
2979
|
+
});
|
|
2980
|
+
return false;
|
|
2981
|
+
}
|
|
2982
|
+
return true;
|
|
2983
|
+
}
|
|
2984
|
+
function mark(record, field, category, fields) {
|
|
2985
|
+
record.fields[field] = {
|
|
2986
|
+
category,
|
|
2987
|
+
syncedAt: Date.now(),
|
|
2988
|
+
...fields
|
|
2989
|
+
};
|
|
2990
|
+
}
|
|
2991
|
+
function pushSettings(ctx, record, payload, result) {
|
|
2992
|
+
if (!isRecord(payload?.values)) throw new InvalidParamsError("settings: payload.values must be an object.");
|
|
2993
|
+
if (JSON.stringify(payload.values).length > MAX_SETTINGS_BYTES) throw new InvalidParamsError(`settings: larger than ${MAX_SETTINGS_BYTES} bytes.`);
|
|
2994
|
+
const current = ctx.env.readSettings();
|
|
2995
|
+
const patch = {};
|
|
2996
|
+
for (const [key, value] of Object.entries(payload.values)) {
|
|
2997
|
+
const field = settingField(key);
|
|
2998
|
+
if (!Object.hasOwn(DEFAULT_SETTINGS, key)) {
|
|
2999
|
+
result.skipped.push({
|
|
3000
|
+
field,
|
|
3001
|
+
reason: "not a setting"
|
|
3002
|
+
});
|
|
3003
|
+
continue;
|
|
3004
|
+
}
|
|
3005
|
+
if (!isSyncableSetting(key)) {
|
|
3006
|
+
result.skipped.push({
|
|
3007
|
+
field,
|
|
3008
|
+
reason: isWindowOnlySetting(key) ? "window-only setting" : "not synced"
|
|
3009
|
+
});
|
|
3010
|
+
continue;
|
|
3011
|
+
}
|
|
3012
|
+
const currentHash = hashValue(current[key]);
|
|
3013
|
+
if (!writable(record, field, currentHash, result)) continue;
|
|
3014
|
+
if (hashValue(value) === currentHash) {
|
|
3015
|
+
mark(record, field, "settings", {
|
|
3016
|
+
hash: currentHash,
|
|
3017
|
+
sent: currentHash
|
|
3018
|
+
});
|
|
3019
|
+
result.unchanged.push(field);
|
|
3020
|
+
continue;
|
|
3021
|
+
}
|
|
3022
|
+
patch[key] = value;
|
|
3023
|
+
}
|
|
3024
|
+
if (Object.keys(patch).length === 0) return;
|
|
3025
|
+
const next = ctx.setters.setSettings(patch);
|
|
3026
|
+
for (const key of Object.keys(patch)) {
|
|
3027
|
+
mark(record, settingField(key), "settings", {
|
|
3028
|
+
hash: hashValue(next[key]),
|
|
3029
|
+
sent: hashValue(patch[key])
|
|
3030
|
+
});
|
|
3031
|
+
result.applied.push(settingField(key));
|
|
3032
|
+
}
|
|
3033
|
+
}
|
|
3034
|
+
/** The project here a client's path names: the same folder inside a workspace root, else one folder of that name in a root. */
|
|
3035
|
+
function projectHere(ctx, clientPath) {
|
|
3036
|
+
const roots = (ctx.env.workspaceRoots ?? []).map((root) => {
|
|
3037
|
+
try {
|
|
3038
|
+
return realpathSync(root);
|
|
3039
|
+
} catch {
|
|
3040
|
+
return null;
|
|
3041
|
+
}
|
|
3042
|
+
}).filter((r) => r !== null);
|
|
3043
|
+
const inRoot = (path) => roots.some((root) => path === root || path.startsWith(root + sep));
|
|
3044
|
+
const isDir = (path) => statSync(path, { throwIfNoEntry: false })?.isDirectory() === true;
|
|
3045
|
+
if (isAbsolute(clientPath) && isDir(clientPath)) {
|
|
3046
|
+
const real = realpathSync(clientPath);
|
|
3047
|
+
if (inRoot(real)) return safelistProjectKey(real);
|
|
3048
|
+
}
|
|
3049
|
+
const name = basename(clientPath);
|
|
3050
|
+
if (!name || name === "." || name === "..") return null;
|
|
3051
|
+
const found = [...new Set(roots.map((root) => join(root, name)).filter(isDir).map((path) => realpathSync(path)))];
|
|
3052
|
+
return found.length === 1 ? safelistProjectKey(found[0]) : null;
|
|
3053
|
+
}
|
|
3054
|
+
function pushSafelists(ctx, record, payload, result) {
|
|
3055
|
+
if (!isRecord(payload?.projects)) throw new InvalidParamsError("safelists: payload.projects must map paths to entries.");
|
|
3056
|
+
const dataDir = ctx.env.userDataDir();
|
|
3057
|
+
const file = readProjects(dataDir);
|
|
3058
|
+
let dirty = false;
|
|
3059
|
+
for (const [clientPath, entries] of Object.entries(payload.projects)) {
|
|
3060
|
+
const field = `safelist:${clientPath}`;
|
|
3061
|
+
if (!Array.isArray(entries) || entries.length > MAX_SAFELIST_ENTRIES || !entries.every((e) => typeof e === "string" && e.length <= MAX_ENTRY_LENGTH)) {
|
|
3062
|
+
result.skipped.push({
|
|
3063
|
+
field,
|
|
3064
|
+
reason: "invalid entries"
|
|
3065
|
+
});
|
|
3066
|
+
continue;
|
|
3067
|
+
}
|
|
3068
|
+
const target = projectHere(ctx, clientPath);
|
|
3069
|
+
if (!target) {
|
|
3070
|
+
result.skipped.push({
|
|
3071
|
+
field,
|
|
3072
|
+
reason: "no project of that path or name on this server"
|
|
3073
|
+
});
|
|
3074
|
+
continue;
|
|
3075
|
+
}
|
|
3076
|
+
const previous = record.fields[field]?.target === target ? record.fields[field]?.entries ?? [] : [];
|
|
3077
|
+
const current = file[target]?.safelist ?? [];
|
|
3078
|
+
const next = current.filter((entry) => !previous.includes(entry) || entries.includes(entry));
|
|
3079
|
+
for (const entry of entries) if (!next.includes(entry)) next.push(entry);
|
|
3080
|
+
mark(record, field, "safelists", {
|
|
3081
|
+
target,
|
|
3082
|
+
entries: [...entries]
|
|
3083
|
+
});
|
|
3084
|
+
if (JSON.stringify(next) === JSON.stringify(current)) {
|
|
3085
|
+
result.unchanged.push(field);
|
|
3086
|
+
continue;
|
|
3087
|
+
}
|
|
3088
|
+
file[target] = {
|
|
3089
|
+
...file[target],
|
|
3090
|
+
safelist: next
|
|
3091
|
+
};
|
|
3092
|
+
dirty = true;
|
|
3093
|
+
result.applied.push(field);
|
|
3094
|
+
}
|
|
3095
|
+
if (dirty) writeProjects(dataDir, file);
|
|
3096
|
+
}
|
|
3097
|
+
/** A skill as checked bytes, or why it is refused. */
|
|
3098
|
+
function checkSkill(skill) {
|
|
3099
|
+
const name = isRecord(skill) && typeof skill.name === "string" ? skill.name : "";
|
|
3100
|
+
const field = `skill:${name}`;
|
|
3101
|
+
if (!NAME_RE.test(name)) return {
|
|
3102
|
+
field,
|
|
3103
|
+
reason: "not a plain skill name"
|
|
3104
|
+
};
|
|
3105
|
+
const files = isRecord(skill) && Array.isArray(skill.files) ? skill.files : null;
|
|
3106
|
+
if (!files || files.length === 0 || files.length > MAX_SKILL_FILES) return {
|
|
3107
|
+
field,
|
|
3108
|
+
reason: `needs 1 to ${MAX_SKILL_FILES} files`
|
|
3109
|
+
};
|
|
3110
|
+
const out = [];
|
|
3111
|
+
let total = 0;
|
|
3112
|
+
for (const file of files) {
|
|
3113
|
+
const path = checkRelativePath(isRecord(file) ? file.path : void 0);
|
|
3114
|
+
if (!path) return {
|
|
3115
|
+
field,
|
|
3116
|
+
reason: "a file path is not a plain relative path"
|
|
3117
|
+
};
|
|
3118
|
+
if (typeof file.data !== "string") return {
|
|
3119
|
+
field,
|
|
3120
|
+
reason: `${path}: no data`
|
|
3121
|
+
};
|
|
3122
|
+
const bytes = Buffer.from(file.data, "base64");
|
|
3123
|
+
if (bytes.length > MAX_FILE_BYTES) return {
|
|
3124
|
+
field,
|
|
3125
|
+
reason: `${path} is larger than ${MAX_FILE_BYTES} bytes`
|
|
3126
|
+
};
|
|
3127
|
+
total += bytes.length;
|
|
3128
|
+
if (out.some((f) => f.path === path || f.path.startsWith(`${path}/`) || path.startsWith(`${f.path}/`))) return {
|
|
3129
|
+
field,
|
|
3130
|
+
reason: `${path} appears twice`
|
|
3131
|
+
};
|
|
3132
|
+
out.push({
|
|
3133
|
+
path,
|
|
3134
|
+
bytes
|
|
3135
|
+
});
|
|
3136
|
+
}
|
|
3137
|
+
if (total > MAX_SKILL_BYTES) return {
|
|
3138
|
+
field,
|
|
3139
|
+
reason: `larger than ${MAX_SKILL_BYTES} bytes`
|
|
3140
|
+
};
|
|
3141
|
+
return {
|
|
3142
|
+
name,
|
|
3143
|
+
files: out
|
|
3144
|
+
};
|
|
3145
|
+
}
|
|
3146
|
+
/** Replace `dir` with exactly these files: staged next to it, then swapped in. */
|
|
3147
|
+
function writeSkillDir(root, name, files) {
|
|
3148
|
+
mkdirSync(root, { recursive: true });
|
|
3149
|
+
const dir = join(root, name);
|
|
3150
|
+
const stage = join(root, `.${name}.sync-${process$1.pid}-${Date.now()}`);
|
|
3151
|
+
mkdirSync(stage);
|
|
3152
|
+
try {
|
|
3153
|
+
for (const file of files) {
|
|
3154
|
+
const target = resolve(stage, file.path);
|
|
3155
|
+
if (!target.startsWith(stage + sep)) throw new Error(`${file.path} is outside the skill folder.`);
|
|
3156
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
3157
|
+
writeFileSync(target, file.bytes);
|
|
3158
|
+
}
|
|
3159
|
+
const backup = existsSync(dir) ? `${stage}.old` : null;
|
|
3160
|
+
if (backup) renameSync(dir, backup);
|
|
3161
|
+
renameSync(stage, dir);
|
|
3162
|
+
if (backup) rmSync(backup, {
|
|
3163
|
+
recursive: true,
|
|
3164
|
+
force: true
|
|
3165
|
+
});
|
|
3166
|
+
} finally {
|
|
3167
|
+
rmSync(stage, {
|
|
3168
|
+
recursive: true,
|
|
3169
|
+
force: true
|
|
3170
|
+
});
|
|
3171
|
+
}
|
|
3172
|
+
}
|
|
3173
|
+
function pushFiles(ctx, record, payload, result) {
|
|
3174
|
+
if (!isRecord(payload) || !Array.isArray(payload.skills) || payload.skills.length > MAX_SKILLS || payload.systemMd !== null && typeof payload.systemMd !== "string") throw new InvalidParamsError("files: payload must be { systemMd: string | null, skills: [...] }.");
|
|
3175
|
+
let touched = false;
|
|
3176
|
+
let total = 0;
|
|
3177
|
+
const mdPath = userSystemMdPath({
|
|
3178
|
+
home: ctx.home(),
|
|
3179
|
+
prefix: ctx.prefix()
|
|
3180
|
+
});
|
|
3181
|
+
const currentMd = existsSync(mdPath) ? readFileSync(mdPath, "utf8") : null;
|
|
3182
|
+
const currentMdHash = currentMd === null ? null : hashValue(currentMd);
|
|
3183
|
+
if (payload.systemMd !== null && Buffer.byteLength(payload.systemMd) > MAX_SYSTEM_MD_BYTES) result.skipped.push({
|
|
3184
|
+
field: SYSTEM_MD_FIELD,
|
|
3185
|
+
reason: `larger than ${MAX_SYSTEM_MD_BYTES} bytes`
|
|
3186
|
+
});
|
|
3187
|
+
else if (payload.systemMd !== null || record.fields["system-md"]) {
|
|
3188
|
+
if (writable(record, "system-md", currentMdHash, result)) {
|
|
3189
|
+
if (payload.systemMd === null) {
|
|
3190
|
+
rmSync(mdPath, { force: true });
|
|
3191
|
+
delete record.fields[SYSTEM_MD_FIELD];
|
|
3192
|
+
result.applied.push(SYSTEM_MD_FIELD);
|
|
3193
|
+
touched = true;
|
|
3194
|
+
} else if (hashValue(payload.systemMd) === currentMdHash) {
|
|
3195
|
+
mark(record, SYSTEM_MD_FIELD, "files", { hash: currentMdHash });
|
|
3196
|
+
result.unchanged.push(SYSTEM_MD_FIELD);
|
|
3197
|
+
} else {
|
|
3198
|
+
total += Buffer.byteLength(payload.systemMd);
|
|
3199
|
+
writeFileAtomic(mdPath, payload.systemMd, { ensureDir: true });
|
|
3200
|
+
mark(record, SYSTEM_MD_FIELD, "files", { hash: hashValue(payload.systemMd) });
|
|
3201
|
+
result.applied.push(SYSTEM_MD_FIELD);
|
|
3202
|
+
touched = true;
|
|
3203
|
+
}
|
|
3204
|
+
}
|
|
3205
|
+
}
|
|
3206
|
+
const root = userSkillsRoot(ctx);
|
|
3207
|
+
const sent = /* @__PURE__ */ new Set();
|
|
3208
|
+
for (const raw of payload.skills) {
|
|
3209
|
+
const skill = checkSkill(raw);
|
|
3210
|
+
if ("reason" in skill) {
|
|
3211
|
+
result.skipped.push(skill);
|
|
3212
|
+
continue;
|
|
3213
|
+
}
|
|
3214
|
+
const field = `skill:${skill.name}`;
|
|
3215
|
+
sent.add(field);
|
|
3216
|
+
const bytes = skill.files.reduce((n, f) => n + f.bytes.length, 0);
|
|
3217
|
+
if (total + bytes > MAX_FILES_BYTES) {
|
|
3218
|
+
result.skipped.push({
|
|
3219
|
+
field,
|
|
3220
|
+
reason: `over the ${MAX_FILES_BYTES}-byte bound of one push`
|
|
3221
|
+
});
|
|
3222
|
+
continue;
|
|
3223
|
+
}
|
|
3224
|
+
const dir = join(root, skill.name);
|
|
3225
|
+
if (existsSync(dir) && lstatSync(dir).isSymbolicLink()) {
|
|
3226
|
+
result.skipped.push({
|
|
3227
|
+
field,
|
|
3228
|
+
reason: "a link on this server, left alone"
|
|
3229
|
+
});
|
|
3230
|
+
continue;
|
|
3231
|
+
}
|
|
3232
|
+
const currentHash = hashDir(dir);
|
|
3233
|
+
if (!writable(record, field, currentHash, result)) continue;
|
|
3234
|
+
const wanted = hashFiles(skill.files);
|
|
3235
|
+
if (wanted === currentHash) {
|
|
3236
|
+
mark(record, field, "files", { hash: wanted });
|
|
3237
|
+
result.unchanged.push(field);
|
|
3238
|
+
continue;
|
|
3239
|
+
}
|
|
3240
|
+
total += bytes;
|
|
3241
|
+
writeSkillDir(root, skill.name, skill.files);
|
|
3242
|
+
mark(record, field, "files", { hash: hashDir(dir) ?? wanted });
|
|
3243
|
+
result.applied.push(field);
|
|
3244
|
+
touched = true;
|
|
3245
|
+
}
|
|
3246
|
+
for (const [field, known] of Object.entries(record.fields)) {
|
|
3247
|
+
if (known.category !== "files" || !field.startsWith("skill:") || sent.has(field)) continue;
|
|
3248
|
+
const dir = join(root, field.slice(6));
|
|
3249
|
+
if (!writable(record, field, hashDir(dir), result)) continue;
|
|
3250
|
+
rmSync(dir, {
|
|
3251
|
+
recursive: true,
|
|
3252
|
+
force: true
|
|
3253
|
+
});
|
|
3254
|
+
delete record.fields[field];
|
|
3255
|
+
result.applied.push(field);
|
|
3256
|
+
touched = true;
|
|
3257
|
+
}
|
|
3258
|
+
return touched;
|
|
3259
|
+
}
|
|
3260
|
+
function readUserMcps(ctx) {
|
|
3261
|
+
const path = userMcpsPath(ctx);
|
|
3262
|
+
if (!existsSync(path)) return [];
|
|
3263
|
+
try {
|
|
3264
|
+
return parseMcpsFile(readFileSync(path, "utf8"));
|
|
3265
|
+
} catch {
|
|
3266
|
+
return [];
|
|
3267
|
+
}
|
|
3268
|
+
}
|
|
3269
|
+
async function pushMcps(ctx, record, payload, peer, result) {
|
|
3270
|
+
if (!isRecord(payload) || !Array.isArray(payload.servers)) throw new InvalidParamsError("mcps: payload.servers must be a list.");
|
|
3271
|
+
const hashOf = (name) => {
|
|
3272
|
+
const found = readUserMcps(ctx).find((s) => s.name === name);
|
|
3273
|
+
return found ? hashValue(found) : null;
|
|
3274
|
+
};
|
|
3275
|
+
const sent = /* @__PURE__ */ new Set();
|
|
3276
|
+
for (const config of payload.servers) {
|
|
3277
|
+
const name = isRecord(config) && typeof config.name === "string" ? config.name : "";
|
|
3278
|
+
const field = `mcp:${name}`;
|
|
3279
|
+
if (!NAME_RE.test(name)) {
|
|
3280
|
+
result.skipped.push({
|
|
3281
|
+
field,
|
|
3282
|
+
reason: "not a plain server name"
|
|
3283
|
+
});
|
|
3284
|
+
continue;
|
|
3285
|
+
}
|
|
3286
|
+
sent.add(field);
|
|
3287
|
+
const currentHash = hashOf(name);
|
|
3288
|
+
if (!writable(record, field, currentHash, result)) continue;
|
|
3289
|
+
const wanted = hashValue(config);
|
|
3290
|
+
if (record.fields[field]?.sent === wanted && currentHash !== null) {
|
|
3291
|
+
result.unchanged.push(field);
|
|
3292
|
+
continue;
|
|
3293
|
+
}
|
|
3294
|
+
try {
|
|
3295
|
+
await ctx.setters.mcpUpsert({ config }, peer);
|
|
3296
|
+
} catch (err) {
|
|
3297
|
+
result.skipped.push({
|
|
3298
|
+
field,
|
|
3299
|
+
reason: err instanceof Error ? err.message : String(err)
|
|
3300
|
+
});
|
|
3301
|
+
continue;
|
|
3302
|
+
}
|
|
3303
|
+
mark(record, field, "mcps", {
|
|
3304
|
+
hash: hashOf(name) ?? wanted,
|
|
3305
|
+
sent: wanted
|
|
3306
|
+
});
|
|
3307
|
+
result.applied.push(field);
|
|
3308
|
+
if (typeof config.command === "string" && config.command.trim() !== "" && !commandAvailable(config.command.trim())) result.warnings.push({
|
|
3309
|
+
field,
|
|
3310
|
+
message: `"${config.command}" is not on this server's PATH: the server may not run there.`
|
|
3311
|
+
});
|
|
3312
|
+
}
|
|
3313
|
+
for (const [field, known] of Object.entries(record.fields)) {
|
|
3314
|
+
if (known.category !== "mcps" || sent.has(field)) continue;
|
|
3315
|
+
const name = field.slice(4);
|
|
3316
|
+
if (!writable(record, field, hashOf(name), result)) continue;
|
|
3317
|
+
try {
|
|
3318
|
+
await ctx.setters.mcpRemove({ name }, peer);
|
|
3319
|
+
} catch (err) {
|
|
3320
|
+
result.skipped.push({
|
|
3321
|
+
field,
|
|
3322
|
+
reason: err instanceof Error ? err.message : String(err)
|
|
3323
|
+
});
|
|
3324
|
+
continue;
|
|
3325
|
+
}
|
|
3326
|
+
delete record.fields[field];
|
|
3327
|
+
result.applied.push(field);
|
|
3328
|
+
}
|
|
3329
|
+
}
|
|
3330
|
+
async function pushExtensions(ctx, record, payload, peer, result) {
|
|
3331
|
+
if (!isRecord(payload) || !Array.isArray(payload.extensions)) throw new InvalidParamsError("extensions: payload.extensions must be a list.");
|
|
3332
|
+
for (const entry of payload.extensions) {
|
|
3333
|
+
const name = isRecord(entry) && typeof entry.name === "string" ? entry.name : "";
|
|
3334
|
+
const field = `extension:${name}`;
|
|
3335
|
+
if (!NAME_RE.test(name) || typeof entry.spec !== "string") {
|
|
3336
|
+
result.skipped.push({
|
|
3337
|
+
field,
|
|
3338
|
+
reason: "needs a plain name and a source spec"
|
|
3339
|
+
});
|
|
3340
|
+
continue;
|
|
3341
|
+
}
|
|
3342
|
+
if (record.overrides[field]) {
|
|
3343
|
+
result.skipped.push({
|
|
3344
|
+
field,
|
|
3345
|
+
reason: "override"
|
|
3346
|
+
});
|
|
3347
|
+
continue;
|
|
3348
|
+
}
|
|
3349
|
+
if (userExtensionDirs(ctx, name).some((dir) => existsSync(dir))) {
|
|
3350
|
+
if (!record.fields[field]) mark(record, field, "extensions", {});
|
|
3351
|
+
result.unchanged.push(field);
|
|
3352
|
+
continue;
|
|
3353
|
+
}
|
|
3354
|
+
try {
|
|
3355
|
+
await ctx.setters.install({
|
|
3356
|
+
kind: "extension",
|
|
3357
|
+
spec: entry.spec,
|
|
3358
|
+
scope: "user",
|
|
3359
|
+
...typeof entry.extension === "string" ? { extension: entry.extension } : {}
|
|
3360
|
+
}, peer);
|
|
3361
|
+
} catch (err) {
|
|
3362
|
+
result.skipped.push({
|
|
3363
|
+
field,
|
|
3364
|
+
reason: err instanceof Error ? err.message : String(err)
|
|
3365
|
+
});
|
|
3366
|
+
continue;
|
|
3367
|
+
}
|
|
3368
|
+
mark(record, field, "extensions", {});
|
|
3369
|
+
result.applied.push(field);
|
|
3370
|
+
}
|
|
3371
|
+
}
|
|
3372
|
+
function pushKeys(ctx, record, payload, result) {
|
|
3373
|
+
if (!isRecord(payload) || !Array.isArray(payload.keys)) throw new InvalidParamsError("keys: payload.keys must be a list.");
|
|
3374
|
+
for (const entry of payload.keys) {
|
|
3375
|
+
const provider = isRecord(entry) && typeof entry.provider === "string" ? entry.provider : "";
|
|
3376
|
+
const field = `key:${provider}`;
|
|
3377
|
+
if (!record.keys.includes(provider)) {
|
|
3378
|
+
result.skipped.push({
|
|
3379
|
+
field,
|
|
3380
|
+
reason: "not opted in for this key"
|
|
3381
|
+
});
|
|
3382
|
+
continue;
|
|
3383
|
+
}
|
|
3384
|
+
if (record.overrides[field]) {
|
|
3385
|
+
result.skipped.push({
|
|
3386
|
+
field,
|
|
3387
|
+
reason: "override"
|
|
3388
|
+
});
|
|
3389
|
+
continue;
|
|
3390
|
+
}
|
|
3391
|
+
try {
|
|
3392
|
+
if (typeof entry.key !== "string" || entry.key.trim() === "") throw new Error("empty key");
|
|
3393
|
+
if (ctx.auth.storedKeys(ctx.auth.descriptorOf(provider)).includes(entry.key.trim())) {
|
|
3394
|
+
if (!record.fields[field]) mark(record, field, "keys", {});
|
|
3395
|
+
result.unchanged.push(field);
|
|
3396
|
+
continue;
|
|
3397
|
+
}
|
|
3398
|
+
ctx.setters.setApiKey({
|
|
3399
|
+
provider,
|
|
3400
|
+
key: entry.key
|
|
3401
|
+
});
|
|
3402
|
+
} catch (err) {
|
|
3403
|
+
result.skipped.push({
|
|
3404
|
+
field,
|
|
3405
|
+
reason: err instanceof Error ? err.message : String(err)
|
|
3406
|
+
});
|
|
3407
|
+
continue;
|
|
3408
|
+
}
|
|
3409
|
+
mark(record, field, "keys", {});
|
|
3410
|
+
result.applied.push(field);
|
|
3411
|
+
}
|
|
3412
|
+
}
|
|
3413
|
+
//#endregion
|
|
3414
|
+
//#region src/ipc/daemon/sync-store.ts
|
|
3415
|
+
const SYNC_FILE = "sync.json";
|
|
3416
|
+
function emptySyncRecord() {
|
|
3417
|
+
return {
|
|
3418
|
+
version: 1,
|
|
3419
|
+
source: null,
|
|
3420
|
+
categories: [],
|
|
3421
|
+
keys: [],
|
|
3422
|
+
fields: {},
|
|
3423
|
+
overrides: {}
|
|
3424
|
+
};
|
|
3425
|
+
}
|
|
3426
|
+
function createSyncStore(dataDir) {
|
|
3427
|
+
const file = join(dataDir, SYNC_FILE);
|
|
3428
|
+
return {
|
|
3429
|
+
file,
|
|
3430
|
+
read: () => {
|
|
3431
|
+
if (!existsSync(file)) return emptySyncRecord();
|
|
3432
|
+
try {
|
|
3433
|
+
const raw = JSON.parse(readFileSync(file, "utf8"));
|
|
3434
|
+
if (!isRecord(raw)) return emptySyncRecord();
|
|
3435
|
+
return {
|
|
3436
|
+
version: 1,
|
|
3437
|
+
source: isRecord(raw.source) && typeof raw.source.deviceId === "string" && typeof raw.source.deviceName === "string" ? raw.source : null,
|
|
3438
|
+
categories: Array.isArray(raw.categories) ? raw.categories.filter((c) => SYNC_CATEGORIES.includes(c)) : [],
|
|
3439
|
+
keys: Array.isArray(raw.keys) ? raw.keys.filter((k) => typeof k === "string") : [],
|
|
3440
|
+
fields: isRecord(raw.fields) ? raw.fields : {},
|
|
3441
|
+
overrides: isRecord(raw.overrides) ? raw.overrides : {},
|
|
3442
|
+
...typeof raw.lastPushAt === "number" ? { lastPushAt: raw.lastPushAt } : {}
|
|
3443
|
+
};
|
|
3444
|
+
} catch {
|
|
3445
|
+
return emptySyncRecord();
|
|
3446
|
+
}
|
|
3447
|
+
},
|
|
3448
|
+
write: (record) => writeJsonFileAtomic(file, record, OWNER_ONLY)
|
|
3449
|
+
};
|
|
3450
|
+
}
|
|
3451
|
+
/** What `sync.status` says: the record without hashes or safelist entries. */
|
|
3452
|
+
function syncStatusOf(record) {
|
|
3453
|
+
return {
|
|
3454
|
+
source: record.source,
|
|
3455
|
+
categories: [...record.categories],
|
|
3456
|
+
keys: [...record.keys],
|
|
3457
|
+
fields: Object.fromEntries(Object.entries(record.fields).map(([field, f]) => [field, {
|
|
3458
|
+
category: f.category,
|
|
3459
|
+
syncedAt: f.syncedAt
|
|
3460
|
+
}])),
|
|
3461
|
+
overrides: { ...record.overrides },
|
|
3462
|
+
...record.lastPushAt !== void 0 ? { lastPushAt: record.lastPushAt } : {}
|
|
3463
|
+
};
|
|
3464
|
+
}
|
|
3465
|
+
//#endregion
|
|
3466
|
+
//#region src/ipc/daemon/handlers/sync.ts
|
|
3467
|
+
const MAX_DEVICE_FIELD = 200;
|
|
3468
|
+
const NOT_A_SERVER = "This host takes no settings sync (a zidane-server does).";
|
|
3469
|
+
function checkDevice(p, withName) {
|
|
3470
|
+
if (typeof p.deviceId !== "string" || p.deviceId.trim() === "" || p.deviceId.length > MAX_DEVICE_FIELD) throw new InvalidParamsError("deviceId must be a non-empty string.");
|
|
3471
|
+
if (withName && (typeof p.deviceName !== "string" || p.deviceName.trim() === "" || p.deviceName.length > MAX_DEVICE_FIELD)) throw new InvalidParamsError("deviceName must be a non-empty string.");
|
|
3472
|
+
return {
|
|
3473
|
+
deviceId: p.deviceId,
|
|
3474
|
+
deviceName: typeof p.deviceName === "string" ? p.deviceName.trim() : ""
|
|
3475
|
+
};
|
|
3476
|
+
}
|
|
3477
|
+
function checkCategories(value) {
|
|
3478
|
+
if (value === void 0) return void 0;
|
|
3479
|
+
if (!Array.isArray(value) || !value.every((c) => SYNC_CATEGORIES.includes(c))) throw new InvalidParamsError(`categories must be some of: ${SYNC_CATEGORIES.join(", ")}.`);
|
|
3480
|
+
return SYNC_CATEGORIES.filter((c) => value.includes(c));
|
|
3481
|
+
}
|
|
3482
|
+
function createSyncHandlers({ host, env, broadcast, setters, auth, options }) {
|
|
3483
|
+
const store = createSyncStore(env.userDataDir());
|
|
3484
|
+
const ctx = {
|
|
3485
|
+
env,
|
|
3486
|
+
setters,
|
|
3487
|
+
auth,
|
|
3488
|
+
home: () => homedir(),
|
|
3489
|
+
prefix: () => env.config.prefix
|
|
3490
|
+
};
|
|
3491
|
+
/** Sync calls (and override marks) one at a time: each reads the record, works, writes it. */
|
|
3492
|
+
let queue = Promise.resolve();
|
|
3493
|
+
const exclusive = (work) => {
|
|
3494
|
+
const next = queue.then(work, work);
|
|
3495
|
+
queue = next.catch(() => void 0);
|
|
3496
|
+
return next;
|
|
3497
|
+
};
|
|
3498
|
+
const changed = () => broadcast({ kind: "sync:changed" });
|
|
3499
|
+
const audit = (peer, entry) => options?.audit?.(peer, entry);
|
|
3500
|
+
function requireOptions() {
|
|
3501
|
+
if (!options) throw new Error(NOT_A_SERVER);
|
|
3502
|
+
}
|
|
3503
|
+
/** Mark fields the source set as edited here; nothing for fields nobody synced. */
|
|
3504
|
+
const markOverrides = (fields) => {
|
|
3505
|
+
if (!options || fields.length === 0) return Promise.resolve();
|
|
3506
|
+
return exclusive(() => {
|
|
3507
|
+
const record = store.read();
|
|
3508
|
+
const marked = fields.filter((field) => record.fields[field] && !record.overrides[field]);
|
|
3509
|
+
if (marked.length === 0) return;
|
|
3510
|
+
const at = Date.now();
|
|
3511
|
+
for (const field of marked) record.overrides[field] = { at };
|
|
3512
|
+
store.write(record);
|
|
3513
|
+
changed();
|
|
3514
|
+
});
|
|
3515
|
+
};
|
|
3516
|
+
/** Whether `deviceId` over `peer`'s token is the recorded source. */
|
|
3517
|
+
const isSource = (record, deviceId, peer) => record.source !== null && record.source.deviceId === deviceId && record.source.tokenId === peer.principal;
|
|
3518
|
+
/** The source's own call, or a refusal naming the device that syncs. */
|
|
3519
|
+
function requireSource(record, deviceId, peer) {
|
|
3520
|
+
if (!record.source) throw new Error("No device syncs to this server: claim it first (sync.claim).");
|
|
3521
|
+
if (!isSource(record, deviceId, peer)) throw new Error(`This server is synced from ${record.source.deviceName}${record.source.deviceId === deviceId ? " with another token" : ""}: take over from that device first.`);
|
|
3522
|
+
}
|
|
3523
|
+
const rebuild = () => {
|
|
3524
|
+
host.rebuildAllAgents({ rediscover: true }).catch(() => void 0);
|
|
3525
|
+
};
|
|
3526
|
+
return {
|
|
3527
|
+
markOverrides,
|
|
3528
|
+
"sync.status": () => {
|
|
3529
|
+
requireOptions();
|
|
3530
|
+
return syncStatusOf(store.read());
|
|
3531
|
+
},
|
|
3532
|
+
"sync.claim": (p, peer) => exclusive(() => {
|
|
3533
|
+
requireOptions();
|
|
3534
|
+
const { deviceId, deviceName } = checkDevice(p, true);
|
|
3535
|
+
const categories = checkCategories(p.categories);
|
|
3536
|
+
if (p.keys !== void 0 && (!Array.isArray(p.keys) || !p.keys.every((k) => typeof k === "string"))) throw new InvalidParamsError("keys must be provider names.");
|
|
3537
|
+
const takeover = p.takeover === true;
|
|
3538
|
+
const record = store.read();
|
|
3539
|
+
const other = record.source !== null && !isSource(record, deviceId, peer);
|
|
3540
|
+
if (other && !takeover) {
|
|
3541
|
+
audit(peer, {
|
|
3542
|
+
event: "sync.claim",
|
|
3543
|
+
deviceId,
|
|
3544
|
+
deviceName,
|
|
3545
|
+
takeover,
|
|
3546
|
+
claimed: false,
|
|
3547
|
+
ok: true
|
|
3548
|
+
});
|
|
3549
|
+
return {
|
|
3550
|
+
claimed: false,
|
|
3551
|
+
status: syncStatusOf(record)
|
|
3552
|
+
};
|
|
3553
|
+
}
|
|
3554
|
+
const next = other ? {
|
|
3555
|
+
version: 1,
|
|
3556
|
+
source: null,
|
|
3557
|
+
categories: [],
|
|
3558
|
+
keys: [],
|
|
3559
|
+
fields: {},
|
|
3560
|
+
overrides: {}
|
|
3561
|
+
} : record;
|
|
3562
|
+
next.source = {
|
|
3563
|
+
deviceId,
|
|
3564
|
+
deviceName,
|
|
3565
|
+
claimedAt: other || !record.source ? Date.now() : record.source.claimedAt,
|
|
3566
|
+
...peer.principal ? { tokenId: peer.principal } : {}
|
|
3567
|
+
};
|
|
3568
|
+
if (categories) next.categories = categories;
|
|
3569
|
+
if (p.keys !== void 0) next.keys = [...new Set(p.keys)];
|
|
3570
|
+
store.write(next);
|
|
3571
|
+
changed();
|
|
3572
|
+
audit(peer, {
|
|
3573
|
+
event: "sync.claim",
|
|
3574
|
+
deviceId,
|
|
3575
|
+
deviceName,
|
|
3576
|
+
takeover,
|
|
3577
|
+
claimed: true,
|
|
3578
|
+
categories: next.categories,
|
|
3579
|
+
keys: next.keys,
|
|
3580
|
+
ok: true
|
|
3581
|
+
});
|
|
3582
|
+
return {
|
|
3583
|
+
claimed: true,
|
|
3584
|
+
status: syncStatusOf(next)
|
|
3585
|
+
};
|
|
3586
|
+
}),
|
|
3587
|
+
"sync.push": (p, peer) => exclusive(async () => {
|
|
3588
|
+
requireOptions();
|
|
3589
|
+
const { deviceId } = checkDevice(p, false);
|
|
3590
|
+
const category = p.category;
|
|
3591
|
+
const fields = {
|
|
3592
|
+
event: "sync.push",
|
|
3593
|
+
category,
|
|
3594
|
+
deviceId
|
|
3595
|
+
};
|
|
3596
|
+
try {
|
|
3597
|
+
if (!SYNC_CATEGORIES.includes(category)) throw new InvalidParamsError(`category must be one of: ${SYNC_CATEGORIES.join(", ")}.`);
|
|
3598
|
+
const record = store.read();
|
|
3599
|
+
requireSource(record, deviceId, peer);
|
|
3600
|
+
if (!record.categories.includes(category)) throw new Error(`"${category}" is not synced to this server (claim it with that category).`);
|
|
3601
|
+
const result = {
|
|
3602
|
+
applied: [],
|
|
3603
|
+
unchanged: [],
|
|
3604
|
+
skipped: [],
|
|
3605
|
+
warnings: []
|
|
3606
|
+
};
|
|
3607
|
+
const payload = p.payload;
|
|
3608
|
+
if (category === "settings") pushSettings(ctx, record, payload, result);
|
|
3609
|
+
else if (category === "safelists") pushSafelists(ctx, record, payload, result);
|
|
3610
|
+
else if (category === "files") {
|
|
3611
|
+
if (pushFiles(ctx, record, payload, result)) {
|
|
3612
|
+
rebuild();
|
|
3613
|
+
broadcast({
|
|
3614
|
+
kind: "catalog:changed",
|
|
3615
|
+
catalog: "skills"
|
|
3616
|
+
});
|
|
3617
|
+
}
|
|
3618
|
+
} else if (category === "mcps") await pushMcps(ctx, record, payload, peer, result);
|
|
3619
|
+
else if (category === "extensions") await pushExtensions(ctx, record, payload, peer, result);
|
|
3620
|
+
else pushKeys(ctx, record, payload, result);
|
|
3621
|
+
record.lastPushAt = Date.now();
|
|
3622
|
+
store.write(record);
|
|
3623
|
+
changed();
|
|
3624
|
+
audit(peer, {
|
|
3625
|
+
...fields,
|
|
3626
|
+
applied: result.applied.length,
|
|
3627
|
+
unchanged: result.unchanged.length,
|
|
3628
|
+
skipped: result.skipped.length,
|
|
3629
|
+
ok: true
|
|
3630
|
+
});
|
|
3631
|
+
return result;
|
|
3632
|
+
} catch (err) {
|
|
3633
|
+
audit(peer, {
|
|
3634
|
+
...fields,
|
|
3635
|
+
ok: false,
|
|
3636
|
+
error: err instanceof Error ? err.message : String(err)
|
|
3637
|
+
});
|
|
3638
|
+
throw err;
|
|
3639
|
+
}
|
|
3640
|
+
}),
|
|
3641
|
+
"sync.reset": (p, peer) => exclusive(() => {
|
|
3642
|
+
requireOptions();
|
|
3643
|
+
if (typeof p.field !== "string" || p.field === "") throw new InvalidParamsError("field must be a sync field name.");
|
|
3644
|
+
const record = store.read();
|
|
3645
|
+
const reset = Boolean(record.overrides[p.field]);
|
|
3646
|
+
if (reset) {
|
|
3647
|
+
delete record.overrides[p.field];
|
|
3648
|
+
const known = record.fields[p.field];
|
|
3649
|
+
if (known) {
|
|
3650
|
+
delete known.hash;
|
|
3651
|
+
delete known.sent;
|
|
3652
|
+
}
|
|
3653
|
+
store.write(record);
|
|
3654
|
+
changed();
|
|
3655
|
+
}
|
|
3656
|
+
audit(peer, {
|
|
3657
|
+
event: "sync.reset",
|
|
3658
|
+
field: p.field,
|
|
3659
|
+
reset,
|
|
3660
|
+
ok: true
|
|
3661
|
+
});
|
|
3662
|
+
return { reset };
|
|
3663
|
+
}),
|
|
3664
|
+
"sync.release": (p, peer) => exclusive(() => {
|
|
3665
|
+
requireOptions();
|
|
3666
|
+
const { deviceId } = checkDevice(p, false);
|
|
3667
|
+
const record = store.read();
|
|
3668
|
+
if (!record.source) return { released: false };
|
|
3669
|
+
requireSource(record, deviceId, peer);
|
|
3670
|
+
store.write({
|
|
3671
|
+
version: 1,
|
|
3672
|
+
source: null,
|
|
3673
|
+
categories: [],
|
|
3674
|
+
keys: [],
|
|
3675
|
+
fields: {},
|
|
3676
|
+
overrides: {}
|
|
3677
|
+
});
|
|
3678
|
+
changed();
|
|
3679
|
+
audit(peer, {
|
|
3680
|
+
event: "sync.release",
|
|
3681
|
+
deviceId,
|
|
3682
|
+
ok: true
|
|
3683
|
+
});
|
|
3684
|
+
return { released: true };
|
|
3685
|
+
})
|
|
3686
|
+
};
|
|
3687
|
+
}
|
|
3688
|
+
/**
|
|
3689
|
+
* The window-facing setters, marking what they change as an override when
|
|
3690
|
+
* the field came from sync (the sync handlers call the unwrapped ones).
|
|
3691
|
+
*/
|
|
3692
|
+
function withSyncOverrides(handlers, { readSettings, markOverrides }) {
|
|
3693
|
+
const after = async (work, fields) => {
|
|
3694
|
+
const result = await work();
|
|
3695
|
+
await markOverrides(fields(result));
|
|
3696
|
+
return result;
|
|
3697
|
+
};
|
|
3698
|
+
return {
|
|
3699
|
+
"settings.set": async (p, peer) => {
|
|
3700
|
+
const before = readSettings();
|
|
3701
|
+
return after(() => handlers["settings.set"](p, peer), (next) => Object.keys(isRecord(p?.patch) ? p.patch : {}).filter((key) => JSON.stringify(before[key]) !== JSON.stringify(next[key])).map(settingField));
|
|
3702
|
+
},
|
|
3703
|
+
"mcp.upsert": (p, peer) => after(() => handlers["mcp.upsert"](p, peer), (r) => [`mcp:${r.name}`]),
|
|
3704
|
+
"mcp.remove": (p, peer) => after(() => handlers["mcp.remove"](p, peer), (r) => r.removed ? [`mcp:${p.name}`] : []),
|
|
3705
|
+
"catalog.remove": (p, peer) => after(() => handlers["catalog.remove"](p, peer), () => [`${p.kind}:${p.name}`]),
|
|
3706
|
+
"catalog.update": (p, peer) => after(() => handlers["catalog.update"](p, peer), () => [`${p.kind}:${p.name}`]),
|
|
3707
|
+
"auth.setApiKey": (p, peer) => after(() => handlers["auth.setApiKey"](p, peer), () => [`key:${p.provider}`]),
|
|
3708
|
+
"auth.remove": (p, peer) => after(() => handlers["auth.remove"](p, peer), (r) => r.removed ? [`key:${p.provider}`] : [])
|
|
3709
|
+
};
|
|
3710
|
+
}
|
|
3711
|
+
//#endregion
|
|
3712
|
+
//#region src/ipc/daemon/handlers/terminal.ts
|
|
3713
|
+
function createTerminalHandlers({ host, isConnected }) {
|
|
3714
|
+
return {
|
|
3715
|
+
methods: {
|
|
3716
|
+
"terminal.state": (p) => host.terminals.state(p.sessionId),
|
|
3717
|
+
"terminal.open": (p) => host.terminals.open(p),
|
|
3718
|
+
"terminal.attach": async (p, peer) => {
|
|
3719
|
+
const result = await host.terminals.attach(p.terminalId, p.clientId);
|
|
3720
|
+
if (!isConnected(peer)) {
|
|
3721
|
+
host.terminals.detach(p.terminalId, p.clientId);
|
|
3722
|
+
return result;
|
|
3723
|
+
}
|
|
3724
|
+
const views = peer.terminalViews ??= /* @__PURE__ */ new Map();
|
|
3725
|
+
views.set(p.terminalId, (views.get(p.terminalId) ?? /* @__PURE__ */ new Set()).add(p.clientId));
|
|
3726
|
+
return result;
|
|
3727
|
+
},
|
|
3728
|
+
"terminal.detach": (p, peer) => {
|
|
3729
|
+
const views = peer.terminalViews?.get(p.terminalId);
|
|
3730
|
+
views?.delete(p.clientId);
|
|
3731
|
+
if (views?.size === 0) peer.terminalViews?.delete(p.terminalId);
|
|
3732
|
+
host.terminals.detach(p.terminalId, p.clientId);
|
|
3733
|
+
},
|
|
3734
|
+
"terminal.write": (p) => host.terminals.write(p.terminalId, p.data, p.clientId ? {
|
|
3735
|
+
clientId: p.clientId,
|
|
3736
|
+
...p.response ? { response: true } : {}
|
|
3737
|
+
} : void 0),
|
|
3738
|
+
"terminal.resize": (p) => host.terminals.resize(p.terminalId, p.clientId, p.cols, p.rows, p.claim),
|
|
3739
|
+
"terminal.ack": (p) => host.terminals.ack(p.terminalId, p.clientId, p.offset),
|
|
3740
|
+
"terminal.restart": (p) => host.terminals.restart(p.terminalId),
|
|
3741
|
+
"terminal.close": (p) => host.terminals.close(p.terminalId),
|
|
3742
|
+
"terminal.setHidden": (p) => host.terminals.setHidden(p.terminalId, p.hidden),
|
|
3743
|
+
"terminal.registerViewer": (p, peer) => {
|
|
3744
|
+
(peer.terminalViewers ??= /* @__PURE__ */ new Set()).add(p.viewerId);
|
|
3745
|
+
host.terminals.registerViewer(p.viewerId);
|
|
3746
|
+
return { ok: true };
|
|
3747
|
+
},
|
|
3748
|
+
"terminal.unregisterViewer": (p, peer) => {
|
|
3749
|
+
peer.terminalViewers?.delete(p.viewerId);
|
|
3750
|
+
host.terminals.unregisterViewer(p.viewerId);
|
|
3751
|
+
return { ok: true };
|
|
3752
|
+
}
|
|
3753
|
+
},
|
|
3754
|
+
releasePeer(peer) {
|
|
3755
|
+
for (const viewerId of peer.terminalViewers ?? []) host.terminals.unregisterViewer(viewerId);
|
|
3756
|
+
for (const [terminalId, clients] of peer.terminalViews ?? []) for (const clientId of clients) host.terminals.detach(terminalId, clientId);
|
|
3757
|
+
}
|
|
3758
|
+
};
|
|
3759
|
+
}
|
|
3760
|
+
//#endregion
|
|
3761
|
+
//#region src/ipc/daemon/handlers/titles.ts
|
|
3762
|
+
function createTitleHandlers({ host, broadcast }) {
|
|
3763
|
+
return { "sessions.generateTitle": async (p) => {
|
|
3764
|
+
const sessionId = sessionIdOf(p);
|
|
3765
|
+
const providerKey = optionalString(p.providerKey, "providerKey");
|
|
3766
|
+
const model = optionalString(p.model, "model");
|
|
3767
|
+
const title = await host.generateSessionTitle(sessionId, {
|
|
3768
|
+
...providerKey !== void 0 ? { providerKey } : {},
|
|
3769
|
+
...model !== void 0 ? { model } : {}
|
|
3770
|
+
});
|
|
3771
|
+
broadcast({
|
|
3772
|
+
kind: "sessions:changed",
|
|
3773
|
+
op: "update",
|
|
3774
|
+
sessionId
|
|
3775
|
+
});
|
|
3776
|
+
return { title };
|
|
3777
|
+
} };
|
|
3778
|
+
}
|
|
3779
|
+
//#endregion
|
|
3780
|
+
//#region src/ipc/daemon/handlers/todos.ts
|
|
3781
|
+
function createTodoHandlers({ host, store }) {
|
|
3782
|
+
return {
|
|
3783
|
+
"todos.get": (p) => {
|
|
3784
|
+
const state = host.getTodosSnapshot(p.sessionId);
|
|
3785
|
+
return state ? toTodosSnapshot(state) : null;
|
|
3786
|
+
},
|
|
3787
|
+
"todos.set": async (p) => {
|
|
3788
|
+
const state = await host.setTodos(p.sessionId, p.items);
|
|
3789
|
+
if (state) return toTodosSnapshot(state);
|
|
3790
|
+
if (p.persistIdle !== true || host.getLiveSession(p.sessionId)) return null;
|
|
3791
|
+
return persistIdleTodos(store, p.sessionId, Array.isArray(p.items) ? p.items : []);
|
|
3792
|
+
}
|
|
3793
|
+
};
|
|
3794
|
+
}
|
|
3795
|
+
/** The idle-session todo edit, as a window applied it to its own copy: the active run's list, archive cleared. */
|
|
3796
|
+
async function persistIdleTodos(store, sessionId, items) {
|
|
3797
|
+
const session = await loadSession(store, sessionId, { persistHeal: false });
|
|
3798
|
+
const runId = session ? selectActiveTodos(session).runId : null;
|
|
3799
|
+
if (!session || !runId) return null;
|
|
3800
|
+
setTodosForRun(session, runId, items, { clearArchive: true });
|
|
3801
|
+
await session.save();
|
|
3802
|
+
return toTodosSnapshot(selectActiveTodos(session));
|
|
3803
|
+
}
|
|
3804
|
+
//#endregion
|
|
3805
|
+
//#region src/ipc/daemon/handlers/transcript.ts
|
|
3806
|
+
function createTranscriptHandlers({ host, store, broadcast, replays }) {
|
|
3807
|
+
return {
|
|
3808
|
+
"sessions.loadEvents": async (p) => {
|
|
3809
|
+
const paging = p;
|
|
3810
|
+
if (isPagedLoad(paging)) return await replays.page(store, p.sessionId, paging);
|
|
3811
|
+
const data = await loadOrThrow(store, p.sessionId);
|
|
3812
|
+
return eventsFromTurns(data.turns, data.runs);
|
|
3813
|
+
},
|
|
3814
|
+
"sessions.getTurn": async (p) => {
|
|
3815
|
+
return (await store.load(p.sessionId))?.turns.find((t) => t.id === p.turnId) ?? null;
|
|
3816
|
+
},
|
|
3817
|
+
"sessions.touch": (p) => {
|
|
3818
|
+
broadcast({
|
|
3819
|
+
kind: "sessions:changed",
|
|
3820
|
+
op: "update",
|
|
3821
|
+
sessionId: p.sessionId
|
|
3822
|
+
});
|
|
3823
|
+
},
|
|
3824
|
+
"sessions.fork": async (p) => {
|
|
3825
|
+
const result = await host.forkSession({
|
|
3826
|
+
source: p.sessionId,
|
|
3827
|
+
turnId: p.turnId,
|
|
3828
|
+
...typeof p.title === "string" && p.title ? { title: p.title.slice(0, 256) } : {}
|
|
3829
|
+
});
|
|
3830
|
+
if (!result.ok) throw new Error(describeForkFailure(result.reason, p.sessionId, p.turnId));
|
|
3831
|
+
broadcast({
|
|
3832
|
+
kind: "sessions:changed",
|
|
3833
|
+
op: "create",
|
|
3834
|
+
sessionId: result.id
|
|
3835
|
+
});
|
|
3836
|
+
return { id: result.id };
|
|
3837
|
+
},
|
|
3838
|
+
"sessions.deleteTurn": (p) => host.deleteTurn(p.sessionId, p.turnId),
|
|
3839
|
+
"sessions.editTurnText": (p) => host.editTurnText(p.sessionId, p.turnId, typeof p.text === "string" ? p.text.slice(0, 65536) : ""),
|
|
3840
|
+
"sessions.appendTurns": (p) => host.appendTurns(p.sessionId, Array.isArray(p.turns) ? p.turns : []),
|
|
3841
|
+
"sessions.appendNote": (p) => host.appendNote(p.sessionId, Array.isArray(p.blocks) ? p.blocks.filter((block) => typeof block?.text === "string") : []),
|
|
3842
|
+
"sessions.compact": (p) => host.compactSessionNow(p.sessionId)
|
|
3843
|
+
};
|
|
3844
|
+
}
|
|
3845
|
+
//#endregion
|
|
3846
|
+
//#region src/ipc/daemon/handlers/transcript-cache.ts
|
|
3847
|
+
const DEFAULT_MAX_SESSIONS = 4;
|
|
3848
|
+
const DEFAULT_MAX_BYTES = 67108864;
|
|
3849
|
+
/** Signals that follow a change to a session's turns or runs. */
|
|
3850
|
+
const CHANGE_SIGNALS = /* @__PURE__ */ new Set([
|
|
3851
|
+
"sessions:changed",
|
|
3852
|
+
"run:start",
|
|
3853
|
+
"run:end",
|
|
3854
|
+
"turn:end",
|
|
3855
|
+
"compacting:end",
|
|
3856
|
+
"prompt-turns"
|
|
3857
|
+
]);
|
|
3858
|
+
/** One kind of entry (replays, documents): LRU by insertion order, bounded by count and bytes. */
|
|
3859
|
+
function createLru(maxSessions, maxBytes) {
|
|
3860
|
+
const entries = /* @__PURE__ */ new Map();
|
|
3861
|
+
let totalBytes = 0;
|
|
3862
|
+
const drop = (sessionId) => {
|
|
3863
|
+
const entry = entries.get(sessionId);
|
|
3864
|
+
if (entry) {
|
|
3865
|
+
totalBytes -= entry.bytes;
|
|
3866
|
+
entries.delete(sessionId);
|
|
3867
|
+
}
|
|
3868
|
+
};
|
|
3869
|
+
return {
|
|
3870
|
+
size: () => entries.size,
|
|
3871
|
+
hit(sessionId, version) {
|
|
3872
|
+
const entry = entries.get(sessionId);
|
|
3873
|
+
if (!entry || entry.version !== version) return void 0;
|
|
3874
|
+
entries.delete(sessionId);
|
|
3875
|
+
entries.set(sessionId, entry);
|
|
3876
|
+
return entry;
|
|
3877
|
+
},
|
|
3878
|
+
drop,
|
|
3879
|
+
keep(sessionId, entry) {
|
|
3880
|
+
if (entry.bytes > maxBytes) return;
|
|
3881
|
+
drop(sessionId);
|
|
3882
|
+
entries.set(sessionId, entry);
|
|
3883
|
+
totalBytes += entry.bytes;
|
|
3884
|
+
for (const [id, oldest] of entries) {
|
|
3885
|
+
if (entries.size <= maxSessions && totalBytes <= maxBytes) break;
|
|
3886
|
+
entries.delete(id);
|
|
3887
|
+
totalBytes -= oldest.bytes;
|
|
3888
|
+
}
|
|
3889
|
+
}
|
|
3890
|
+
};
|
|
3891
|
+
}
|
|
3892
|
+
function createTranscriptReplayCache(options = {}) {
|
|
3893
|
+
const maxSessions = options.maxSessions ?? DEFAULT_MAX_SESSIONS;
|
|
3894
|
+
const maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;
|
|
3895
|
+
const replay = options.replay ?? replayTurns;
|
|
3896
|
+
const replays = createLru(maxSessions, maxBytes);
|
|
3897
|
+
const documents = createLru(maxSessions, maxBytes);
|
|
3898
|
+
const inFlight = /* @__PURE__ */ new Map();
|
|
3899
|
+
/** Labels a read of no known version: never equal to another read's. */
|
|
3900
|
+
let unversioned = 0;
|
|
3901
|
+
function drop(sessionId) {
|
|
3902
|
+
for (const read of inFlight.get(sessionId) ?? []) read.dropped = true;
|
|
3903
|
+
replays.drop(sessionId);
|
|
3904
|
+
documents.drop(sessionId);
|
|
3905
|
+
}
|
|
3906
|
+
/** Run `fn` as an in-flight read of `sessionId`, tracked only while it runs. */
|
|
3907
|
+
async function tracked(sessionId, fn) {
|
|
3908
|
+
const read = { dropped: false };
|
|
3909
|
+
const reads = inFlight.get(sessionId) ?? /* @__PURE__ */ new Set();
|
|
3910
|
+
inFlight.set(sessionId, reads.add(read));
|
|
3911
|
+
try {
|
|
3912
|
+
return await fn(read);
|
|
3913
|
+
} finally {
|
|
3914
|
+
reads.delete(read);
|
|
3915
|
+
if (reads.size === 0 && inFlight.get(sessionId) === reads) inFlight.delete(sessionId);
|
|
3916
|
+
}
|
|
3917
|
+
}
|
|
3918
|
+
/**
|
|
3919
|
+
* A stored session's entry of `lru`, built from `load` on a miss. The
|
|
3920
|
+
* version comes from `getMeta` when the store has it (see the header),
|
|
3921
|
+
* else from the loaded data (`fingerprintOf`).
|
|
3922
|
+
*/
|
|
3923
|
+
function stored(lru, store, sessionId, load, build, fingerprintOf) {
|
|
3924
|
+
return tracked(sessionId, async (read) => {
|
|
3925
|
+
if (store.getMeta) {
|
|
3926
|
+
const readAt = Date.now();
|
|
3927
|
+
const meta = await store.getMeta(sessionId);
|
|
3928
|
+
if (!meta) return null;
|
|
3929
|
+
const version = storeVersion(meta);
|
|
3930
|
+
const cached = lru.hit(sessionId, version);
|
|
3931
|
+
if (cached) return cached;
|
|
3932
|
+
const data = await load();
|
|
3933
|
+
if (data === null) return null;
|
|
3934
|
+
let settled;
|
|
3935
|
+
if (meta.rev !== void 0) {
|
|
3936
|
+
const after = await store.getMeta(sessionId);
|
|
3937
|
+
settled = after !== null && storeVersion(after) === version;
|
|
3938
|
+
} else settled = meta.updatedAt < readAt;
|
|
3939
|
+
const entry = build(data, settled ? version : `${version}~${++unversioned}`);
|
|
3940
|
+
if (settled && !read.dropped) lru.keep(sessionId, entry);
|
|
3941
|
+
return entry;
|
|
3942
|
+
}
|
|
3943
|
+
const data = await load();
|
|
3944
|
+
if (data === null) return null;
|
|
3945
|
+
const version = fingerprintOf(data);
|
|
3946
|
+
const cached = lru.hit(sessionId, version);
|
|
3947
|
+
if (cached) return cached;
|
|
3948
|
+
const entry = build(data, version);
|
|
3949
|
+
if (!read.dropped) lru.keep(sessionId, entry);
|
|
3950
|
+
return entry;
|
|
3951
|
+
});
|
|
3952
|
+
}
|
|
3953
|
+
function buildReplay(data, version) {
|
|
3954
|
+
const { events, turnStarts } = replay(data.turns, data.runs);
|
|
3955
|
+
return {
|
|
3956
|
+
version,
|
|
3957
|
+
events,
|
|
3958
|
+
turnStarts,
|
|
3959
|
+
boundaries: exchangeBoundaries(data.turns, data.runs),
|
|
3960
|
+
cuts: turnCutPoints(data.turns),
|
|
3961
|
+
bytes: JSON.stringify(events).length
|
|
3962
|
+
};
|
|
3963
|
+
}
|
|
3964
|
+
const snapshotOf = (data, version) => ({
|
|
3965
|
+
version,
|
|
3966
|
+
data,
|
|
3967
|
+
bytes: JSON.stringify(data).length
|
|
3968
|
+
});
|
|
3969
|
+
return {
|
|
3970
|
+
async page(store, sessionId, paging) {
|
|
3971
|
+
const entry = await stored(replays, store, sessionId, () => loadOrThrow(store, sessionId), buildReplay, (data) => fingerprint(data.updatedAt, data.turns, data.runs));
|
|
3972
|
+
if (!entry) throw new Error(`Session "${sessionId}" not found.`);
|
|
3973
|
+
const { from, to, cursor } = transcriptPage(entry.boundaries, paging, entry.cuts);
|
|
3974
|
+
return {
|
|
3975
|
+
events: entry.events.slice(entry.turnStarts[from], entry.turnStarts[to]),
|
|
3976
|
+
exact: true,
|
|
3977
|
+
start: entry.turnStarts[from],
|
|
3978
|
+
...cursor !== void 0 ? { cursor } : {}
|
|
3979
|
+
};
|
|
3980
|
+
},
|
|
3981
|
+
async document(store, sessionId, live) {
|
|
3982
|
+
if (live) {
|
|
3983
|
+
const version = `live:${fingerprint(0, live.turns, live.runs)}:${hashText(JSON.stringify([live.status, live.metadata]))}`;
|
|
3984
|
+
return tracked(sessionId, async (read) => {
|
|
3985
|
+
const cached = documents.hit(sessionId, version);
|
|
3986
|
+
if (cached) return cached;
|
|
3987
|
+
const entry = snapshotOf(live.toJSON(), version);
|
|
3988
|
+
if (!read.dropped) documents.keep(sessionId, entry);
|
|
3989
|
+
return entry;
|
|
3990
|
+
});
|
|
3991
|
+
}
|
|
3992
|
+
const load = async () => (await loadSession(store, sessionId, { persistHeal: false }))?.toJSON() ?? null;
|
|
3993
|
+
return stored(documents, store, sessionId, load, snapshotOf, (data) => fingerprint(data.updatedAt, data.turns, data.runs));
|
|
3994
|
+
},
|
|
3995
|
+
observe(signal) {
|
|
3996
|
+
if (CHANGE_SIGNALS.has(signal.kind) && "sessionId" in signal && typeof signal.sessionId === "string") drop(signal.sessionId);
|
|
3997
|
+
},
|
|
3998
|
+
forget: drop,
|
|
3999
|
+
sizes: () => ({
|
|
4000
|
+
entries: replays.size() + documents.size(),
|
|
4001
|
+
inFlight: inFlight.size
|
|
4002
|
+
})
|
|
4003
|
+
};
|
|
4004
|
+
}
|
|
4005
|
+
function storeVersion(meta) {
|
|
4006
|
+
return `meta:${meta.rev ?? "-"}:${meta.updatedAt}:${meta.turnCount}:${meta.runCount}`;
|
|
4007
|
+
}
|
|
4008
|
+
/** A 32-bit FNV-1a: `mix` values in, `digest` (base 36) out. */
|
|
4009
|
+
function fnv1a() {
|
|
4010
|
+
let hash = 2166136261;
|
|
4011
|
+
const add = (text) => {
|
|
4012
|
+
for (let i = 0; i < text.length; i++) hash = Math.imul(hash ^ text.charCodeAt(i), 16777619);
|
|
4013
|
+
};
|
|
4014
|
+
return {
|
|
4015
|
+
add,
|
|
4016
|
+
/** One value, then a separator: `ab` + `c` never hashes as `a` + `bc`. */
|
|
4017
|
+
mix: (value) => {
|
|
4018
|
+
add(String(value));
|
|
4019
|
+
hash = Math.imul(hash ^ 31, 16777619);
|
|
4020
|
+
},
|
|
4021
|
+
digest: () => (hash >>> 0).toString(36)
|
|
4022
|
+
};
|
|
4023
|
+
}
|
|
4024
|
+
function hashText(text) {
|
|
4025
|
+
const hash = fnv1a();
|
|
4026
|
+
hash.add(text);
|
|
4027
|
+
return hash.digest();
|
|
4028
|
+
}
|
|
4029
|
+
/** FNV-1a over the document's shape: what changes when a turn or run is added, removed, or rewritten. */
|
|
4030
|
+
function fingerprint(updatedAt, turns, runs) {
|
|
4031
|
+
const { mix, digest } = fnv1a();
|
|
4032
|
+
for (const turn of turns) {
|
|
4033
|
+
mix(turn.id);
|
|
4034
|
+
mix(turn.role);
|
|
4035
|
+
mix(turn.runId);
|
|
4036
|
+
mix(typeof turn.display === "string" ? turn.display : "");
|
|
4037
|
+
mix(turn.content.length);
|
|
4038
|
+
for (const block of turn.content) {
|
|
4039
|
+
mix(block.type);
|
|
4040
|
+
const fields = block;
|
|
4041
|
+
for (const field of [
|
|
4042
|
+
fields.text,
|
|
4043
|
+
fields.output,
|
|
4044
|
+
fields.summary
|
|
4045
|
+
]) mix(typeof field === "string" ? field.length : Array.isArray(field) ? field.length : "");
|
|
4046
|
+
}
|
|
4047
|
+
}
|
|
4048
|
+
mix(JSON.stringify(runs));
|
|
4049
|
+
return `doc:${updatedAt}:${turns.length}:${runs.length}:${digest()}`;
|
|
4050
|
+
}
|
|
4051
|
+
//#endregion
|
|
4052
|
+
//#region src/ipc/daemon/handlers/workspace.ts
|
|
4053
|
+
function createWorkspaceHandlers({ host }) {
|
|
4054
|
+
return {
|
|
4055
|
+
"sessions.workspaceExec": (p) => host.workspaceExec(p.sessionId, p.command, {
|
|
4056
|
+
...p.cwd !== void 0 ? { cwd: p.cwd } : {},
|
|
4057
|
+
...p.env !== void 0 ? { env: p.env } : {},
|
|
4058
|
+
...p.timeoutSeconds !== void 0 ? { timeoutSeconds: p.timeoutSeconds } : {}
|
|
4059
|
+
}),
|
|
4060
|
+
"sessions.workspaceReadFile": (p) => host.workspaceReadFile(p.sessionId, p.path),
|
|
4061
|
+
"sessions.workspaceWriteFile": (p) => host.workspaceWriteFile(p.sessionId, p.path, p.content),
|
|
4062
|
+
"sessions.workspaceListFiles": (p) => host.workspaceListFiles(p.sessionId, p.path, p.options),
|
|
4063
|
+
"sessions.workspaceFileSize": (p) => host.workspaceFileSize(p.sessionId, p.path)
|
|
4064
|
+
};
|
|
4065
|
+
}
|
|
4066
|
+
//#endregion
|
|
4067
|
+
//#region src/ipc/daemon/handlers/workspace-exec-stream.ts
|
|
4068
|
+
const WORKSPACE_EXEC_STREAM_LIMITS = {
|
|
4069
|
+
/** Streamed commands one connection may run at once. */
|
|
4070
|
+
maxConcurrent: 4,
|
|
4071
|
+
/** Largest `chunk` of one data signal, in bytes. */
|
|
4072
|
+
chunkBytes: 65536,
|
|
4073
|
+
/** Output past this (stdout + stderr, bytes) stops the process: the same ceiling as a local `!`. */
|
|
4074
|
+
maxOutputBytes: 1048576,
|
|
4075
|
+
/** Longest command text. */
|
|
4076
|
+
maxCommandLength: 65536,
|
|
4077
|
+
/** The buffered fallback's bound (seconds), as the buffered `!` had. */
|
|
4078
|
+
fallbackTimeoutSeconds: 600
|
|
4079
|
+
};
|
|
4080
|
+
/** Cut `text` into pieces of at most `max` UTF-8 bytes, never inside a code point. */
|
|
4081
|
+
function utf8Pieces(text, max) {
|
|
4082
|
+
if (Buffer.byteLength(text) <= max) return [text];
|
|
4083
|
+
const pieces = [];
|
|
4084
|
+
let current = "";
|
|
4085
|
+
let bytes = 0;
|
|
4086
|
+
for (const char of text) {
|
|
4087
|
+
const size = Buffer.byteLength(char);
|
|
4088
|
+
if (bytes + size > max) {
|
|
4089
|
+
pieces.push(current);
|
|
4090
|
+
current = "";
|
|
4091
|
+
bytes = 0;
|
|
4092
|
+
}
|
|
4093
|
+
current += char;
|
|
4094
|
+
bytes += size;
|
|
4095
|
+
}
|
|
4096
|
+
if (current) pieces.push(current);
|
|
4097
|
+
return pieces;
|
|
4098
|
+
}
|
|
4099
|
+
/** The longest prefix of `text` within `max` UTF-8 bytes. */
|
|
4100
|
+
function utf8Prefix(text, max) {
|
|
4101
|
+
let bytes = 0;
|
|
4102
|
+
let end = 0;
|
|
4103
|
+
for (const char of text) {
|
|
4104
|
+
const size = Buffer.byteLength(char);
|
|
4105
|
+
if (bytes + size > max) break;
|
|
4106
|
+
bytes += size;
|
|
4107
|
+
end += char.length;
|
|
4108
|
+
}
|
|
4109
|
+
return text.slice(0, end);
|
|
4110
|
+
}
|
|
4111
|
+
function createWorkspaceExecStreamHandlers({ host, isConnected, limits = WORKSPACE_EXEC_STREAM_LIMITS }) {
|
|
4112
|
+
const running = /* @__PURE__ */ new Map();
|
|
4113
|
+
const perPeer = /* @__PURE__ */ new WeakMap();
|
|
4114
|
+
const send = (peer, signal) => {
|
|
4115
|
+
if (isConnected(peer)) peer.send(signal);
|
|
4116
|
+
};
|
|
4117
|
+
/** Output accounting for one exec: emits what fits, reports when the cap is reached. */
|
|
4118
|
+
function outputSink(execId, peer, onCap) {
|
|
4119
|
+
let total = 0;
|
|
4120
|
+
let capped = false;
|
|
4121
|
+
return {
|
|
4122
|
+
capped: () => capped,
|
|
4123
|
+
emit(stream, text) {
|
|
4124
|
+
if (capped || text === "") return;
|
|
4125
|
+
let piece = text;
|
|
4126
|
+
if (total + Buffer.byteLength(piece) > limits.maxOutputBytes) {
|
|
4127
|
+
piece = utf8Prefix(piece, limits.maxOutputBytes - total);
|
|
4128
|
+
capped = true;
|
|
4129
|
+
}
|
|
4130
|
+
total += Buffer.byteLength(piece);
|
|
4131
|
+
for (const chunk of piece ? utf8Pieces(piece, limits.chunkBytes) : []) send(peer, {
|
|
4132
|
+
kind: "workspace:exec:data",
|
|
4133
|
+
execId,
|
|
4134
|
+
stream,
|
|
4135
|
+
chunk
|
|
4136
|
+
});
|
|
4137
|
+
if (capped) onCap();
|
|
4138
|
+
}
|
|
4139
|
+
};
|
|
4140
|
+
}
|
|
4141
|
+
async function pump(execId, peer, stream, entry) {
|
|
4142
|
+
const sink = outputSink(execId, peer, () => entry.kill());
|
|
4143
|
+
const drain = async (source, name) => {
|
|
4144
|
+
if (!source) return;
|
|
4145
|
+
const decoder = new TextDecoder();
|
|
4146
|
+
try {
|
|
4147
|
+
for await (const bytes of source) sink.emit(name, decoder.decode(bytes, { stream: true }));
|
|
4148
|
+
sink.emit(name, decoder.decode());
|
|
4149
|
+
} catch {}
|
|
4150
|
+
};
|
|
4151
|
+
const [code] = await Promise.all([
|
|
4152
|
+
stream.exited,
|
|
4153
|
+
drain(stream.stdout, "stdout"),
|
|
4154
|
+
drain(stream.stderr, "stderr")
|
|
4155
|
+
]);
|
|
4156
|
+
const killed = entry.killedLive === true;
|
|
4157
|
+
const exit = {
|
|
4158
|
+
kind: "workspace:exec:exit",
|
|
4159
|
+
execId,
|
|
4160
|
+
code: killed ? null : code,
|
|
4161
|
+
signal: killed ? "SIGTERM" : null,
|
|
4162
|
+
...sink.capped() ? { truncated: true } : {},
|
|
4163
|
+
...entry.cancelled && killed ? { cancelled: true } : {}
|
|
4164
|
+
};
|
|
4165
|
+
send(peer, exit);
|
|
4166
|
+
}
|
|
4167
|
+
/** A context without `execStream`: the buffered exec, reported as one stdout and one stderr chunk. */
|
|
4168
|
+
async function buffered(execId, peer, entry, run) {
|
|
4169
|
+
let stopped = false;
|
|
4170
|
+
entry.kill = () => {
|
|
4171
|
+
stopped = true;
|
|
4172
|
+
};
|
|
4173
|
+
const sink = outputSink(execId, peer, () => void 0);
|
|
4174
|
+
try {
|
|
4175
|
+
const result = await run();
|
|
4176
|
+
if (!stopped) {
|
|
4177
|
+
sink.emit("stdout", result.stdout);
|
|
4178
|
+
sink.emit("stderr", result.stderr);
|
|
4179
|
+
}
|
|
4180
|
+
send(peer, {
|
|
4181
|
+
kind: "workspace:exec:exit",
|
|
4182
|
+
execId,
|
|
4183
|
+
code: stopped ? null : result.exitCode,
|
|
4184
|
+
signal: stopped ? "SIGTERM" : null,
|
|
4185
|
+
...sink.capped() ? { truncated: true } : {},
|
|
4186
|
+
...entry.cancelled ? { cancelled: true } : {}
|
|
4187
|
+
});
|
|
4188
|
+
} catch (err) {
|
|
4189
|
+
sink.emit("stderr", err instanceof Error ? err.message : String(err));
|
|
4190
|
+
send(peer, {
|
|
4191
|
+
kind: "workspace:exec:exit",
|
|
4192
|
+
execId,
|
|
4193
|
+
code: null,
|
|
4194
|
+
signal: null,
|
|
4195
|
+
...entry.cancelled ? { cancelled: true } : {}
|
|
4196
|
+
});
|
|
4197
|
+
}
|
|
4198
|
+
}
|
|
4199
|
+
return {
|
|
4200
|
+
methods: {
|
|
4201
|
+
"sessions.workspaceExecStream": async (p, peer) => {
|
|
4202
|
+
if (typeof p.sessionId !== "string" || p.sessionId === "") throw new InvalidParamsError("sessionId must be a non-empty string.");
|
|
4203
|
+
if (typeof p.command !== "string" || p.command.trim() === "") throw new InvalidParamsError("command must be a non-empty string.");
|
|
4204
|
+
if (p.command.length > limits.maxCommandLength) throw new InvalidParamsError(`command is longer than ${limits.maxCommandLength} characters.`);
|
|
4205
|
+
if (p.cwd !== void 0 && typeof p.cwd !== "string") throw new InvalidParamsError("cwd must be a string.");
|
|
4206
|
+
const own = perPeer.get(peer) ?? /* @__PURE__ */ new Set();
|
|
4207
|
+
perPeer.set(peer, own);
|
|
4208
|
+
if (own.size >= limits.maxConcurrent) throw new Error(`This connection already runs ${own.size} shell commands (at most ${limits.maxConcurrent}): stop one first.`);
|
|
4209
|
+
const execId = randomUUID();
|
|
4210
|
+
own.add(execId);
|
|
4211
|
+
const entry = {
|
|
4212
|
+
peer,
|
|
4213
|
+
kill: () => void 0,
|
|
4214
|
+
cancelled: false
|
|
4215
|
+
};
|
|
4216
|
+
running.set(execId, entry);
|
|
4217
|
+
const done = () => {
|
|
4218
|
+
own.delete(execId);
|
|
4219
|
+
running.delete(execId);
|
|
4220
|
+
};
|
|
4221
|
+
const options = p.cwd !== void 0 ? { cwd: p.cwd } : {};
|
|
4222
|
+
let stream;
|
|
4223
|
+
try {
|
|
4224
|
+
stream = await host.workspaceExecStream(p.sessionId, p.command, options);
|
|
4225
|
+
} catch (err) {
|
|
4226
|
+
done();
|
|
4227
|
+
throw err;
|
|
4228
|
+
}
|
|
4229
|
+
if (stream) {
|
|
4230
|
+
const live = stream;
|
|
4231
|
+
let exited = false;
|
|
4232
|
+
live.exited.then(() => {
|
|
4233
|
+
exited = true;
|
|
4234
|
+
}, () => {
|
|
4235
|
+
exited = true;
|
|
4236
|
+
});
|
|
4237
|
+
entry.kill = () => {
|
|
4238
|
+
if (!exited) entry.killedLive = true;
|
|
4239
|
+
live.kill();
|
|
4240
|
+
};
|
|
4241
|
+
if (!isConnected(peer)) entry.kill();
|
|
4242
|
+
pump(execId, peer, live, entry).finally(done);
|
|
4243
|
+
} else buffered(execId, peer, entry, () => host.workspaceExec(p.sessionId, p.command, {
|
|
4244
|
+
...options,
|
|
4245
|
+
timeoutSeconds: limits.fallbackTimeoutSeconds
|
|
4246
|
+
})).finally(done);
|
|
4247
|
+
return { execId };
|
|
4248
|
+
},
|
|
4249
|
+
"sessions.workspaceExecCancel": (p, peer) => {
|
|
4250
|
+
const entry = typeof p.execId === "string" ? running.get(p.execId) : void 0;
|
|
4251
|
+
if (!entry || entry.peer !== peer) return { cancelled: false };
|
|
4252
|
+
entry.cancelled = true;
|
|
4253
|
+
entry.kill();
|
|
4254
|
+
return { cancelled: true };
|
|
4255
|
+
}
|
|
4256
|
+
},
|
|
4257
|
+
releasePeer(peer) {
|
|
4258
|
+
for (const execId of perPeer.get(peer) ?? []) {
|
|
4259
|
+
const entry = running.get(execId);
|
|
4260
|
+
if (entry) {
|
|
4261
|
+
entry.cancelled = true;
|
|
4262
|
+
entry.kill();
|
|
4263
|
+
}
|
|
4264
|
+
}
|
|
4265
|
+
}
|
|
4266
|
+
};
|
|
4267
|
+
}
|
|
4268
|
+
//#endregion
|
|
4269
|
+
//#region src/ipc/daemon/handshake.ts
|
|
4270
|
+
function evaluateServerHello(params) {
|
|
4271
|
+
if (params.protocolVersion === 13) return {
|
|
4272
|
+
accept: true,
|
|
4273
|
+
retire: false
|
|
4274
|
+
};
|
|
4275
|
+
const client = typeof params.protocolVersion === "number" ? params.protocolVersion : void 0;
|
|
4276
|
+
return {
|
|
4277
|
+
accept: false,
|
|
4278
|
+
message: `Protocol version mismatch: the server speaks v13, the client v${client ?? "?"}. Upgrade the ${client !== void 0 && client > 13 ? "server" : "client"}.`
|
|
4279
|
+
};
|
|
4280
|
+
}
|
|
4281
|
+
function evaluateHello(params, ctx) {
|
|
4282
|
+
if (ctx.hostKind === "server") return evaluateServerHello(params);
|
|
4283
|
+
const clientBuild = Number(params.buildId);
|
|
4284
|
+
const daemonBuild = Number(ctx.buildId);
|
|
4285
|
+
const staleBuild = params.buildId !== void 0 && params.buildId !== "unknown" && ctx.buildId !== "unknown" && Number.isFinite(clientBuild) && Number.isFinite(daemonBuild) && clientBuild > daemonBuild;
|
|
4286
|
+
const staleRelease = typeof params.codeVersion === "string" && compareVersions(params.codeVersion, ctx.codeVersion) > 0;
|
|
4287
|
+
const protocolMatches = params.protocolVersion === 13;
|
|
4288
|
+
if (protocolMatches && (staleBuild || staleRelease) && ctx.hasActiveWork) return {
|
|
4289
|
+
accept: true,
|
|
4290
|
+
retire: true
|
|
4291
|
+
};
|
|
4292
|
+
if (!protocolMatches || staleBuild || staleRelease) return {
|
|
4293
|
+
accept: false,
|
|
4294
|
+
message: staleRelease ? `Stale daemon: it runs zidane ${ctx.codeVersion} but the client runs ${params.codeVersion}. Draining for respawn.` : staleBuild ? `Build mismatch: the daemon was started from an older build (daemon ${ctx.buildId}, client ${params.buildId}). Draining for respawn.` : `Protocol version mismatch: daemon speaks v13, client sent v${params.protocolVersion ?? "?"}.`
|
|
4295
|
+
};
|
|
4296
|
+
return {
|
|
4297
|
+
accept: true,
|
|
4298
|
+
retire: false
|
|
4299
|
+
};
|
|
4300
|
+
}
|
|
4301
|
+
//#endregion
|
|
4302
|
+
//#region src/ipc/daemon/identity.ts
|
|
4303
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
4304
|
+
const FILE = "host-id";
|
|
4305
|
+
function readId(path) {
|
|
4306
|
+
try {
|
|
4307
|
+
const id = readFileSync(path, "utf8").trim();
|
|
4308
|
+
return UUID.test(id) ? id : void 0;
|
|
4309
|
+
} catch {
|
|
4310
|
+
return;
|
|
4311
|
+
}
|
|
4312
|
+
}
|
|
4313
|
+
/**
|
|
4314
|
+
* The id persisted in `dataDir`, created on first use. Two processes starting
|
|
4315
|
+
* at once agree: the file is created exclusively (`wx`) and the loser reads
|
|
4316
|
+
* the winner's id. A missing or corrupt file gets a fresh id.
|
|
4317
|
+
*/
|
|
4318
|
+
function readOrCreateHostId(dataDir) {
|
|
4319
|
+
const path = join(dataDir, FILE);
|
|
4320
|
+
const existing = readId(path);
|
|
4321
|
+
if (existing) return existing;
|
|
4322
|
+
const id = randomUUID();
|
|
4323
|
+
try {
|
|
4324
|
+
mkdirSync(dataDir, { recursive: true });
|
|
4325
|
+
writeFileSync(path, `${id}\n`, {
|
|
4326
|
+
flag: "wx",
|
|
4327
|
+
mode: 384
|
|
4328
|
+
});
|
|
4329
|
+
return id;
|
|
4330
|
+
} catch (err) {
|
|
4331
|
+
if (err.code === "EEXIST") {
|
|
4332
|
+
const raced = readId(path);
|
|
4333
|
+
if (raced) return raced;
|
|
4334
|
+
try {
|
|
4335
|
+
writeFileSync(path, `${id}\n`, { mode: 384 });
|
|
4336
|
+
return readId(path) ?? id;
|
|
4337
|
+
} catch {
|
|
4338
|
+
return;
|
|
4339
|
+
}
|
|
4340
|
+
}
|
|
4341
|
+
return;
|
|
4342
|
+
}
|
|
4343
|
+
}
|
|
4344
|
+
function resolveHostIdentity(dataDir, options) {
|
|
4345
|
+
return {
|
|
4346
|
+
id: readOrCreateHostId(dataDir),
|
|
4347
|
+
kind: options.kind,
|
|
4348
|
+
name: options.name ?? hostname()
|
|
4349
|
+
};
|
|
4350
|
+
}
|
|
4351
|
+
//#endregion
|
|
4352
|
+
//#region src/ipc/daemon/peers.ts
|
|
4353
|
+
function createPeerRegistry(options = {}) {
|
|
4354
|
+
const peers = /* @__PURE__ */ new Set();
|
|
4355
|
+
let boundHost = null;
|
|
4356
|
+
const closeCleanups = [];
|
|
4357
|
+
/** Sessions with a run in flight, tracked from the run signals themselves. */
|
|
4358
|
+
const runningSessions = /* @__PURE__ */ new Set();
|
|
4359
|
+
const broadcast = (signal) => {
|
|
4360
|
+
if (signal.kind === "terminal:output" || signal.kind === "terminal:reset") {
|
|
4361
|
+
for (const peer of peers) if (peer.ready && peer.terminalViews?.has(signal.terminalId)) peer.send(signal);
|
|
4362
|
+
return;
|
|
4363
|
+
}
|
|
4364
|
+
fanOut(null, signal, options.sequence?.(signal));
|
|
4365
|
+
switch (signal.kind) {
|
|
4366
|
+
case "run:start":
|
|
4367
|
+
runningSessions.add(signal.sessionId);
|
|
4368
|
+
broadcastPresence();
|
|
4369
|
+
break;
|
|
4370
|
+
case "run:end":
|
|
4371
|
+
runningSessions.delete(signal.sessionId);
|
|
4372
|
+
broadcastPresence();
|
|
4373
|
+
break;
|
|
4374
|
+
case "approval:request":
|
|
4375
|
+
case "approval:resolved":
|
|
4376
|
+
case "interaction:request":
|
|
4377
|
+
case "interaction:resolved": broadcastPresence();
|
|
4378
|
+
}
|
|
4379
|
+
};
|
|
4380
|
+
function fanOut(origin, signal, seq) {
|
|
4381
|
+
for (const peer of peers) {
|
|
4382
|
+
if (!peer.ready || peer === origin) continue;
|
|
4383
|
+
peer.send(signal, seq);
|
|
4384
|
+
}
|
|
4385
|
+
}
|
|
4386
|
+
function broadcastExcept(origin, signal) {
|
|
4387
|
+
fanOut(origin, signal, void 0);
|
|
4388
|
+
}
|
|
4389
|
+
/**
|
|
4390
|
+
* Live presence: which sessions have attached windows, and their
|
|
4391
|
+
* run / awaiting-input state. Union of watched, live-agent, and
|
|
4392
|
+
* pending-HITL sessions — small by construction.
|
|
4393
|
+
*/
|
|
4394
|
+
function computePresence() {
|
|
4395
|
+
const host = boundHost;
|
|
4396
|
+
if (!host) throw new Error("Presence read before the host was bound.");
|
|
4397
|
+
const windowCounts = /* @__PURE__ */ new Map();
|
|
4398
|
+
for (const peer of peers) if (peer.ready && peer.watching) windowCounts.set(peer.watching, (windowCounts.get(peer.watching) ?? 0) + 1);
|
|
4399
|
+
const attention = /* @__PURE__ */ new Set();
|
|
4400
|
+
for (const request of host.approvals.listPending()) attention.add(request.sessionId);
|
|
4401
|
+
for (const pending of host.interactions.list()) attention.add(pending.sessionId);
|
|
4402
|
+
const live = new Map(host.listLiveSessions().map((entry) => [entry.sessionId, entry.running]));
|
|
4403
|
+
return [.../* @__PURE__ */ new Set([
|
|
4404
|
+
...windowCounts.keys(),
|
|
4405
|
+
...attention,
|
|
4406
|
+
...live.keys(),
|
|
4407
|
+
...runningSessions
|
|
4408
|
+
])].map((sessionId) => ({
|
|
4409
|
+
sessionId,
|
|
4410
|
+
windows: windowCounts.get(sessionId) ?? 0,
|
|
4411
|
+
running: runningSessions.has(sessionId) || live.get(sessionId) === true,
|
|
4412
|
+
attention: attention.has(sessionId)
|
|
4413
|
+
}));
|
|
4414
|
+
}
|
|
4415
|
+
function broadcastPresence() {
|
|
4416
|
+
broadcastExcept(null, {
|
|
4417
|
+
kind: "presence:changed",
|
|
4418
|
+
sessions: computePresence()
|
|
4419
|
+
});
|
|
4420
|
+
}
|
|
4421
|
+
return {
|
|
4422
|
+
add(peer) {
|
|
4423
|
+
peers.add(peer);
|
|
4424
|
+
},
|
|
4425
|
+
has: (peer) => peers.has(peer),
|
|
4426
|
+
remove(peer) {
|
|
4427
|
+
if (!peers.delete(peer)) return;
|
|
4428
|
+
for (const cleanup of closeCleanups) cleanup(peer);
|
|
4429
|
+
if (peer.watching) broadcastPresence();
|
|
4430
|
+
},
|
|
4431
|
+
onClose(cleanup) {
|
|
4432
|
+
closeCleanups.push(cleanup);
|
|
4433
|
+
},
|
|
4434
|
+
clear() {
|
|
4435
|
+
peers.clear();
|
|
4436
|
+
},
|
|
4437
|
+
size: () => peers.size,
|
|
4438
|
+
broadcast,
|
|
4439
|
+
broadcastExcept,
|
|
4440
|
+
computePresence,
|
|
4441
|
+
broadcastPresence,
|
|
4442
|
+
bindHost(host) {
|
|
4443
|
+
boundHost = host;
|
|
4444
|
+
}
|
|
4445
|
+
};
|
|
4446
|
+
}
|
|
4447
|
+
//#endregion
|
|
4448
|
+
//#region src/ipc/daemon/signal-log.ts
|
|
4449
|
+
function sessionOf(signal) {
|
|
4450
|
+
const id = signal.sessionId;
|
|
4451
|
+
return typeof id === "string" ? id : void 0;
|
|
4452
|
+
}
|
|
4453
|
+
function createSignalLog(options = {}) {
|
|
4454
|
+
const maxSignals = options.maxSignals ?? 2e3;
|
|
4455
|
+
const maxBytes = options.maxBytes ?? 4194304;
|
|
4456
|
+
const maxTotalBytes = options.maxTotalBytes ?? 33554432;
|
|
4457
|
+
const maxForgotten = options.maxForgotten ?? 1e4;
|
|
4458
|
+
const epoch = randomUUID();
|
|
4459
|
+
/** Insertion order = least recently written first. */
|
|
4460
|
+
const logs = /* @__PURE__ */ new Map();
|
|
4461
|
+
/** Next number of forgotten sessions; insertion order = least recently forgotten first. */
|
|
4462
|
+
const forgotten = /* @__PURE__ */ new Map();
|
|
4463
|
+
/** Every number below this may have been given out by a session whose counter is gone. */
|
|
4464
|
+
let floor = 1;
|
|
4465
|
+
let totalBytes = 0;
|
|
4466
|
+
const dropOldest = (log) => {
|
|
4467
|
+
totalBytes -= log.entries[log.head].bytes;
|
|
4468
|
+
log.bytes -= log.entries[log.head].bytes;
|
|
4469
|
+
log.head++;
|
|
4470
|
+
if (log.head > 1024 && log.head * 2 > log.entries.length) {
|
|
4471
|
+
log.entries = log.entries.slice(log.head);
|
|
4472
|
+
log.head = 0;
|
|
4473
|
+
}
|
|
4474
|
+
};
|
|
4475
|
+
const clear = (log) => {
|
|
4476
|
+
totalBytes -= log.bytes;
|
|
4477
|
+
log.entries = [];
|
|
4478
|
+
log.head = 0;
|
|
4479
|
+
log.bytes = 0;
|
|
4480
|
+
};
|
|
4481
|
+
return {
|
|
4482
|
+
epoch,
|
|
4483
|
+
record(signal) {
|
|
4484
|
+
const sessionId = sessionOf(signal);
|
|
4485
|
+
if (sessionId === void 0) return void 0;
|
|
4486
|
+
let log = logs.get(sessionId);
|
|
4487
|
+
if (log) logs.delete(sessionId);
|
|
4488
|
+
else {
|
|
4489
|
+
log = {
|
|
4490
|
+
next: forgotten.get(sessionId) ?? floor,
|
|
4491
|
+
entries: [],
|
|
4492
|
+
head: 0,
|
|
4493
|
+
bytes: 0
|
|
4494
|
+
};
|
|
4495
|
+
forgotten.delete(sessionId);
|
|
4496
|
+
}
|
|
4497
|
+
logs.set(sessionId, log);
|
|
4498
|
+
const seq = log.next++;
|
|
4499
|
+
let json;
|
|
4500
|
+
try {
|
|
4501
|
+
json = JSON.stringify(signal);
|
|
4502
|
+
} catch {
|
|
4503
|
+
clear(log);
|
|
4504
|
+
return seq;
|
|
4505
|
+
}
|
|
4506
|
+
const bytes = Buffer.byteLength(json);
|
|
4507
|
+
log.entries.push({
|
|
4508
|
+
seq,
|
|
4509
|
+
json,
|
|
4510
|
+
bytes
|
|
4511
|
+
});
|
|
4512
|
+
log.bytes += bytes;
|
|
4513
|
+
totalBytes += bytes;
|
|
4514
|
+
while (log.entries.length - log.head > maxSignals || log.bytes > maxBytes && log.entries.length - log.head > 0) dropOldest(log);
|
|
4515
|
+
for (const [id, other] of logs) {
|
|
4516
|
+
if (totalBytes <= maxTotalBytes || id === sessionId) break;
|
|
4517
|
+
clear(other);
|
|
4518
|
+
}
|
|
4519
|
+
return seq;
|
|
4520
|
+
},
|
|
4521
|
+
replay(sessionId, afterSeq) {
|
|
4522
|
+
const log = logs.get(sessionId);
|
|
4523
|
+
if (!log || !Number.isSafeInteger(afterSeq) || afterSeq < 0) return null;
|
|
4524
|
+
const last = log.next - 1;
|
|
4525
|
+
if (afterSeq > last || afterSeq < 0) return null;
|
|
4526
|
+
if (afterSeq === last) return [];
|
|
4527
|
+
const first = log.entries[log.head]?.seq;
|
|
4528
|
+
if (first === void 0 || first > afterSeq + 1) return null;
|
|
4529
|
+
const out = [];
|
|
4530
|
+
for (let i = log.head; i < log.entries.length; i++) {
|
|
4531
|
+
const entry = log.entries[i];
|
|
4532
|
+
if (entry.seq > afterSeq) out.push({
|
|
4533
|
+
seq: entry.seq,
|
|
4534
|
+
signal: JSON.parse(entry.json)
|
|
4535
|
+
});
|
|
4536
|
+
}
|
|
4537
|
+
return out;
|
|
4538
|
+
},
|
|
4539
|
+
forget(sessionId) {
|
|
4540
|
+
const log = logs.get(sessionId);
|
|
4541
|
+
if (!log) return;
|
|
4542
|
+
clear(log);
|
|
4543
|
+
logs.delete(sessionId);
|
|
4544
|
+
forgotten.set(sessionId, log.next);
|
|
4545
|
+
while (forgotten.size > maxForgotten) {
|
|
4546
|
+
const [oldest, next] = forgotten.entries().next().value;
|
|
4547
|
+
forgotten.delete(oldest);
|
|
4548
|
+
floor = Math.max(floor, next);
|
|
4549
|
+
}
|
|
4550
|
+
},
|
|
4551
|
+
sessionCount: () => logs.size + forgotten.size
|
|
4552
|
+
};
|
|
4553
|
+
}
|
|
4554
|
+
//#endregion
|
|
4555
|
+
//#region src/ipc/daemon/workspace-roots.ts
|
|
4556
|
+
/**
|
|
4557
|
+
* The folders a host lets sessions use, as `fs.*` lists them: the host's own
|
|
4558
|
+
* confinement (`ChatHost.workspace`, `src/chat/workspace-confinement.ts`,
|
|
4559
|
+
* which the host enforces at its entry points), plus the folder listing. A
|
|
4560
|
+
* server is confined to its workspace roots; a local host is not, and its
|
|
4561
|
+
* listing root is the user's home, like the window's own folder picker.
|
|
4562
|
+
*/
|
|
4563
|
+
/** Most folders one listing returns. */
|
|
4564
|
+
const MAX_DIR_ENTRIES = 2e3;
|
|
4565
|
+
function createWorkspaceRoots(confinement) {
|
|
4566
|
+
const { confined, roots, resolveFolder } = confinement;
|
|
4567
|
+
return {
|
|
4568
|
+
describe: () => ({
|
|
4569
|
+
roots: [...roots],
|
|
4570
|
+
confined
|
|
4571
|
+
}),
|
|
4572
|
+
resolveFolder,
|
|
4573
|
+
checkFolder: confinement.checkFolder,
|
|
4574
|
+
sessionFolder: confinement.sessionFolder,
|
|
4575
|
+
list(path) {
|
|
4576
|
+
const dir = resolveFolder(path ?? roots[0]);
|
|
4577
|
+
const entries = [];
|
|
4578
|
+
let truncated = false;
|
|
4579
|
+
const names = readdirSync(dir, { withFileTypes: true }).filter((e) => !e.name.startsWith(".") && (e.isDirectory() || e.isSymbolicLink())).map((e) => e.name).sort((a, b) => a.localeCompare(b));
|
|
4580
|
+
for (const name of names) {
|
|
4581
|
+
let real;
|
|
4582
|
+
try {
|
|
4583
|
+
real = realpathSync.native(join(dir, name));
|
|
4584
|
+
if (!statSync(real).isDirectory()) continue;
|
|
4585
|
+
} catch {
|
|
4586
|
+
continue;
|
|
4587
|
+
}
|
|
4588
|
+
if (!roots.some((root) => isInside(root, real))) continue;
|
|
4589
|
+
if (entries.length >= MAX_DIR_ENTRIES) {
|
|
4590
|
+
truncated = true;
|
|
4591
|
+
break;
|
|
4592
|
+
}
|
|
4593
|
+
entries.push({
|
|
4594
|
+
name,
|
|
4595
|
+
path: real,
|
|
4596
|
+
isGitRepo: existsSync(join(real, ".git"))
|
|
4597
|
+
});
|
|
4598
|
+
}
|
|
4599
|
+
const atRoot = roots.includes(dir);
|
|
4600
|
+
return {
|
|
4601
|
+
roots: [...roots],
|
|
4602
|
+
path: dir,
|
|
4603
|
+
parent: atRoot ? null : dirname(dir),
|
|
4604
|
+
entries,
|
|
4605
|
+
...truncated ? { truncated } : {}
|
|
4606
|
+
};
|
|
4607
|
+
}
|
|
4608
|
+
};
|
|
4609
|
+
}
|
|
4610
|
+
//#endregion
|
|
4611
|
+
//#region src/ipc/daemon/dispatcher.ts
|
|
4612
|
+
/** Codes a machine error keeps over the wire (`MachineErrorCode`). */
|
|
4613
|
+
const MACHINE_ERROR_CODES = new Set(Object.values(MachineErrorCode));
|
|
4614
|
+
async function createHostDispatcher(options) {
|
|
4615
|
+
const { appVersion, startedAt, lifecycle } = options;
|
|
4616
|
+
const signalLog = createSignalLog(options.replay);
|
|
4617
|
+
const replays = createTranscriptReplayCache();
|
|
4618
|
+
const peers = createPeerRegistry({ sequence: (signal) => {
|
|
4619
|
+
replays.observe(signal);
|
|
4620
|
+
return signalLog.record(signal);
|
|
4621
|
+
} });
|
|
4622
|
+
const { broadcast, broadcastExcept } = peers;
|
|
4623
|
+
const env = await (options.createEnvironment ? options.createEnvironment(broadcast) : createDaemonEnvironment({ broadcast }));
|
|
4624
|
+
const host = createChatHost(env);
|
|
4625
|
+
peers.bindHost(host);
|
|
4626
|
+
host.approvals.rehydrate();
|
|
4627
|
+
const store = env.store;
|
|
4628
|
+
const identity = resolveHostIdentity(env.userDataDir(), options.identity ?? { kind: "daemon" });
|
|
4629
|
+
const identityFields = {
|
|
4630
|
+
...identity.id ? { hostId: identity.id } : {},
|
|
4631
|
+
hostKind: identity.kind,
|
|
4632
|
+
hostName: identity.name
|
|
4633
|
+
};
|
|
4634
|
+
const workspace = createWorkspaceRoots(host.workspace);
|
|
4635
|
+
const drafts = createDraftHandlers({ broadcastExcept });
|
|
4636
|
+
const suggest = createSuggestHandlers({
|
|
4637
|
+
env,
|
|
4638
|
+
broadcastExcept
|
|
4639
|
+
});
|
|
4640
|
+
const launchOverrides = createLaunchOverrideHandlers({ host });
|
|
4641
|
+
const browser = createBrowserHandlers({ host });
|
|
4642
|
+
const terminal = createTerminalHandlers({
|
|
4643
|
+
host,
|
|
4644
|
+
isConnected: peers.has
|
|
4645
|
+
});
|
|
4646
|
+
const execStreams = createWorkspaceExecStreamHandlers({
|
|
4647
|
+
host,
|
|
4648
|
+
isConnected: peers.has
|
|
4649
|
+
});
|
|
4650
|
+
const auth = createProviderAuth({
|
|
4651
|
+
host,
|
|
4652
|
+
env
|
|
4653
|
+
});
|
|
4654
|
+
const oauth = createOAuthFlowHandlers({
|
|
4655
|
+
host,
|
|
4656
|
+
env,
|
|
4657
|
+
auth,
|
|
4658
|
+
isConnected: peers.has,
|
|
4659
|
+
...options.oauthFlowTtlMs !== void 0 ? { ttlMs: options.oauthFlowTtlMs } : {}
|
|
4660
|
+
});
|
|
4661
|
+
const settings = createSettingsHandlers({
|
|
4662
|
+
host,
|
|
4663
|
+
env,
|
|
4664
|
+
broadcast
|
|
4665
|
+
});
|
|
4666
|
+
const installs = createCatalogInstallHandlers({
|
|
4667
|
+
host,
|
|
4668
|
+
env,
|
|
4669
|
+
broadcast,
|
|
4670
|
+
setSettings: (patch) => settings["settings.set"]({ patch }, null),
|
|
4671
|
+
...options.remoteInstalls ? { options: options.remoteInstalls } : {}
|
|
4672
|
+
});
|
|
4673
|
+
const authHandlers = createAuthHandlers({ auth });
|
|
4674
|
+
const { markOverrides, ...syncMethods } = createSyncHandlers({
|
|
4675
|
+
host,
|
|
4676
|
+
env,
|
|
4677
|
+
broadcast,
|
|
4678
|
+
auth,
|
|
4679
|
+
setters: {
|
|
4680
|
+
setSettings: (patch) => settings["settings.set"]({ patch }, null),
|
|
4681
|
+
install: (p, peer) => installs["catalog.install"](p, peer),
|
|
4682
|
+
mcpUpsert: (p, peer) => installs["mcp.upsert"](p, peer),
|
|
4683
|
+
mcpRemove: (p, peer) => installs["mcp.remove"](p, peer),
|
|
4684
|
+
setApiKey: (p) => authHandlers["auth.setApiKey"](p, null)
|
|
4685
|
+
},
|
|
4686
|
+
...options.sync ? { options: options.sync } : {}
|
|
4687
|
+
});
|
|
4688
|
+
const machineServe = createMachineServeHandlers({
|
|
4689
|
+
host,
|
|
4690
|
+
dataDir: () => env.userDataDir(),
|
|
4691
|
+
hostName: identity.name,
|
|
4692
|
+
settings: () => env.readSettings(),
|
|
4693
|
+
isConnected: peers.has,
|
|
4694
|
+
...options.machineServe ? { options: options.machineServe } : {}
|
|
4695
|
+
});
|
|
4696
|
+
peers.onClose(browser.releasePeer);
|
|
4697
|
+
peers.onClose(terminal.releasePeer);
|
|
4698
|
+
peers.onClose(launchOverrides.releasePeer);
|
|
4699
|
+
peers.onClose(machineServe.releasePeer);
|
|
4700
|
+
peers.onClose(execStreams.releasePeer);
|
|
4701
|
+
peers.onClose(oauth.releasePeer);
|
|
4702
|
+
const forgetSession = (sessionId) => {
|
|
4703
|
+
drafts.forget(sessionId);
|
|
4704
|
+
launchOverrides.forget(sessionId);
|
|
4705
|
+
suggest.forget(sessionId);
|
|
4706
|
+
signalLog.forget(sessionId);
|
|
4707
|
+
replays.forget(sessionId);
|
|
4708
|
+
};
|
|
4709
|
+
const methods = {
|
|
4710
|
+
...createChatHandlers({
|
|
4711
|
+
host,
|
|
4712
|
+
clearDraft: drafts.clearDraft
|
|
4713
|
+
}),
|
|
4714
|
+
...createQueueHandlers({
|
|
4715
|
+
host,
|
|
4716
|
+
clearDraft: drafts.clearDraft
|
|
4717
|
+
}),
|
|
4718
|
+
...drafts.methods,
|
|
4719
|
+
...suggest.methods,
|
|
4720
|
+
...createExtensionHandlers({
|
|
4721
|
+
env,
|
|
4722
|
+
broadcastExcept
|
|
4723
|
+
}),
|
|
4724
|
+
...launchOverrides.methods,
|
|
4725
|
+
...createSessionHandlers({
|
|
4726
|
+
host,
|
|
4727
|
+
store,
|
|
4728
|
+
broadcast,
|
|
4729
|
+
forgetSession,
|
|
4730
|
+
hostId: identity.id,
|
|
4731
|
+
workspace
|
|
4732
|
+
}),
|
|
4733
|
+
...createTitleHandlers({
|
|
4734
|
+
host,
|
|
4735
|
+
broadcast
|
|
4736
|
+
}),
|
|
4737
|
+
...createSessionReadHandlers({
|
|
4738
|
+
host,
|
|
4739
|
+
store,
|
|
4740
|
+
snapshots: replays
|
|
4741
|
+
}),
|
|
4742
|
+
...createTranscriptHandlers({
|
|
4743
|
+
host,
|
|
4744
|
+
store,
|
|
4745
|
+
broadcast,
|
|
4746
|
+
replays
|
|
4747
|
+
}),
|
|
4748
|
+
...createResumeHandlers({ log: signalLog }),
|
|
4749
|
+
...createContextHandlers({ host }),
|
|
4750
|
+
...createEnvironmentHandlers({
|
|
4751
|
+
host,
|
|
4752
|
+
env,
|
|
4753
|
+
setSettings: settings["settings.set"]
|
|
4754
|
+
}),
|
|
4755
|
+
...createWorkspaceHandlers({ host }),
|
|
4756
|
+
...execStreams.methods,
|
|
4757
|
+
...createFsHandlers({ workspace }),
|
|
4758
|
+
...authHandlers,
|
|
4759
|
+
...oauth.methods,
|
|
4760
|
+
...installs,
|
|
4761
|
+
...createCatalogHandlers({
|
|
4762
|
+
env,
|
|
4763
|
+
host
|
|
4764
|
+
}),
|
|
4765
|
+
...createArtifactHandlers({
|
|
4766
|
+
store,
|
|
4767
|
+
broadcast
|
|
4768
|
+
}),
|
|
4769
|
+
...createHistoryHandlers({
|
|
4770
|
+
host,
|
|
4771
|
+
store
|
|
4772
|
+
}),
|
|
4773
|
+
...createTodoHandlers({
|
|
4774
|
+
host,
|
|
4775
|
+
store
|
|
4776
|
+
}),
|
|
4777
|
+
...createOrchestrationHandlers({ host }),
|
|
4778
|
+
...createHitlHandlers({ host }),
|
|
4779
|
+
...browser.methods,
|
|
4780
|
+
...terminal.methods,
|
|
4781
|
+
...settings,
|
|
4782
|
+
...createPresenceHandlers({
|
|
4783
|
+
env,
|
|
4784
|
+
peers
|
|
4785
|
+
}),
|
|
4786
|
+
...createMachineHandlers({
|
|
4787
|
+
host,
|
|
4788
|
+
broadcast
|
|
4789
|
+
}),
|
|
4790
|
+
...machineServe.methods,
|
|
4791
|
+
...createMachineLinkHandlers({
|
|
4792
|
+
host,
|
|
4793
|
+
env,
|
|
4794
|
+
broadcast,
|
|
4795
|
+
hostId: identity.id,
|
|
4796
|
+
server: identity.kind === "server",
|
|
4797
|
+
appVersion,
|
|
4798
|
+
...options.machineLinks ? { pacing: options.machineLinks } : {}
|
|
4799
|
+
}),
|
|
4800
|
+
"tokens.createMachineToken": (p, peer) => {
|
|
4801
|
+
if (!options.tokens) throw new Error("This host has no tokens to mint (only a zidane-server does).");
|
|
4802
|
+
if (typeof p.name !== "string" || p.name.trim() === "") throw new InvalidParamsError("tokens.createMachineToken: \"name\" must be a non-empty string.");
|
|
4803
|
+
return options.tokens.createMachineToken(p.name.trim().slice(0, 100), peer.principal);
|
|
4804
|
+
},
|
|
4805
|
+
...createDaemonHandlers({
|
|
4806
|
+
host,
|
|
4807
|
+
appVersion,
|
|
4808
|
+
startedAt,
|
|
4809
|
+
lifecycle,
|
|
4810
|
+
identity: identityFields
|
|
4811
|
+
}),
|
|
4812
|
+
...syncMethods,
|
|
4813
|
+
...options.sync ? withSyncOverrides({
|
|
4814
|
+
"settings.set": settings["settings.set"],
|
|
4815
|
+
"mcp.upsert": installs["mcp.upsert"],
|
|
4816
|
+
"mcp.remove": installs["mcp.remove"],
|
|
4817
|
+
"catalog.remove": installs["catalog.remove"],
|
|
4818
|
+
"catalog.update": installs["catalog.update"],
|
|
4819
|
+
"auth.setApiKey": authHandlers["auth.setApiKey"],
|
|
4820
|
+
"auth.remove": authHandlers["auth.remove"]
|
|
4821
|
+
}, {
|
|
4822
|
+
readSettings: () => env.readSettings(),
|
|
4823
|
+
markOverrides
|
|
4824
|
+
}) : {}
|
|
4825
|
+
};
|
|
4826
|
+
/**
|
|
4827
|
+
* Widened view for the string-keyed lookup: the table is typed exhaustively
|
|
4828
|
+
* above; unknown wire methods fall out here. Own keys only: a wire method
|
|
4829
|
+
* named `constructor` / `__proto__` / `toString` is unknown, not a call into
|
|
4830
|
+
* `Object.prototype`.
|
|
4831
|
+
*/
|
|
4832
|
+
const handlerFor = (method) => Object.hasOwn(methods, method) ? methods[method] : void 0;
|
|
4833
|
+
function hello(params, peer) {
|
|
4834
|
+
const verdict = evaluateHello(params, {
|
|
4835
|
+
buildId: IPC_BUILD_ID,
|
|
4836
|
+
codeVersion: ZIDANE_VERSION,
|
|
4837
|
+
hasActiveWork: host.hasActiveWork(),
|
|
4838
|
+
hostKind: identity.kind
|
|
4839
|
+
});
|
|
4840
|
+
if (!verdict.accept) return {
|
|
4841
|
+
ok: false,
|
|
4842
|
+
code: IpcErrorCode.VersionMismatch,
|
|
4843
|
+
message: verdict.message,
|
|
4844
|
+
data: { daemon: {
|
|
4845
|
+
protocolVersion: 13,
|
|
4846
|
+
appVersion,
|
|
4847
|
+
pid: process.pid,
|
|
4848
|
+
startedAt,
|
|
4849
|
+
buildId: IPC_BUILD_ID,
|
|
4850
|
+
codeVersion: ZIDANE_VERSION,
|
|
4851
|
+
...identityFields
|
|
4852
|
+
} }
|
|
4853
|
+
};
|
|
4854
|
+
if (verdict.retire) lifecycle.retireWhenIdle();
|
|
4855
|
+
peer.ready = true;
|
|
4856
|
+
return {
|
|
4857
|
+
ok: true,
|
|
4858
|
+
result: {
|
|
4859
|
+
protocolVersion: 13,
|
|
4860
|
+
appVersion,
|
|
4861
|
+
pid: process.pid,
|
|
4862
|
+
startedAt,
|
|
4863
|
+
buildId: IPC_BUILD_ID,
|
|
4864
|
+
codeVersion: ZIDANE_VERSION,
|
|
4865
|
+
...identityFields,
|
|
4866
|
+
signalEpoch: signalLog.epoch,
|
|
4867
|
+
...lifecycle.isRetiring() ? { retiring: true } : {},
|
|
4868
|
+
...machineServe.allowed() ? { machineServe: true } : {},
|
|
4869
|
+
...remoteInstallsAllowed(options.remoteInstalls) ? { remoteInstalls: true } : {},
|
|
4870
|
+
...options.sync ? { sync: true } : {}
|
|
4871
|
+
}
|
|
4872
|
+
};
|
|
4873
|
+
}
|
|
4874
|
+
return {
|
|
4875
|
+
host,
|
|
4876
|
+
addPeer: peers.add,
|
|
4877
|
+
removePeer: peers.remove,
|
|
4878
|
+
detachAll: peers.clear,
|
|
4879
|
+
peerCount: peers.size,
|
|
4880
|
+
async handleRequest(method, params, peer) {
|
|
4881
|
+
if (method === "$/ping") return {
|
|
4882
|
+
ok: true,
|
|
4883
|
+
result: {}
|
|
4884
|
+
};
|
|
4885
|
+
if (method === "ipc.hello") return hello(params ?? {}, peer);
|
|
4886
|
+
if (!peer.ready && method !== "daemon.shutdown" && method !== "daemon.info") return {
|
|
4887
|
+
ok: false,
|
|
4888
|
+
code: IpcErrorCode.NotHandshaken,
|
|
4889
|
+
message: "Handshake required: send ipc.hello first."
|
|
4890
|
+
};
|
|
4891
|
+
const handler = handlerFor(method);
|
|
4892
|
+
if (!handler && method.startsWith("machine.")) return {
|
|
4893
|
+
ok: false,
|
|
4894
|
+
code: MachineErrorCode.MethodNotAllowed,
|
|
4895
|
+
message: `"${method}" needs a machine connection (a machine-scoped token); this is a client connection.`
|
|
4896
|
+
};
|
|
4897
|
+
if (!handler) return {
|
|
4898
|
+
ok: false,
|
|
4899
|
+
code: -32601,
|
|
4900
|
+
message: `Unknown method "${method}".`
|
|
4901
|
+
};
|
|
4902
|
+
try {
|
|
4903
|
+
checkParams(params);
|
|
4904
|
+
return {
|
|
4905
|
+
ok: true,
|
|
4906
|
+
result: await handler(params, peer) ?? null
|
|
4907
|
+
};
|
|
4908
|
+
} catch (err) {
|
|
4909
|
+
if (err instanceof InvalidParamsError) return {
|
|
4910
|
+
ok: false,
|
|
4911
|
+
code: -32602,
|
|
4912
|
+
message: err.message
|
|
4913
|
+
};
|
|
4914
|
+
const code = err?.code;
|
|
4915
|
+
if (typeof code === "number" && MACHINE_ERROR_CODES.has(code)) return {
|
|
4916
|
+
ok: false,
|
|
4917
|
+
code,
|
|
4918
|
+
message: err.message
|
|
4919
|
+
};
|
|
4920
|
+
return {
|
|
4921
|
+
ok: false,
|
|
4922
|
+
code: IpcErrorCode.HandlerFailed,
|
|
4923
|
+
message: err instanceof Error ? err.message : String(err)
|
|
4924
|
+
};
|
|
4925
|
+
}
|
|
4926
|
+
},
|
|
4927
|
+
async handleNotification(method, params, peer) {
|
|
4928
|
+
if (!peer.ready) return;
|
|
4929
|
+
const handler = handlerFor(method);
|
|
4930
|
+
if (!handler) return;
|
|
4931
|
+
try {
|
|
4932
|
+
checkParams(params);
|
|
4933
|
+
await handler(params, peer);
|
|
4934
|
+
} catch {}
|
|
4935
|
+
}
|
|
4936
|
+
};
|
|
4937
|
+
}
|
|
4938
|
+
//#endregion
|
|
4939
|
+
export { turnCutPoints as a, createDaemonEnvironment as c, connectIpcClient as d, connectOrSpawnDaemon as f, transcriptPage as i, createFileApprovalsStore as l, WHOLE_RUN_PAGES as n, createMachineHandlers as o, createPacedWriter as p, exchangeBoundaries as r, createMachineLinkHandlers as s, createHostDispatcher as t, createHostEnvironment as u };
|
|
4940
|
+
|
|
4941
|
+
//# sourceMappingURL=dispatcher-BMlu2BmZ.js.map
|