@youweichen/pi-harness 0.0.0-stage → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +74 -2
- package/bin/pi-harness.mjs +1819 -0
- package/deploy/com.youweichen.pi-harness.plist +48 -0
- package/deploy/nginx-subpath.conf +88 -0
- package/deploy/pi-harness-task.xml +71 -0
- package/deploy/pi-harness.service +31 -0
- package/dist/server/agent-service.js +3131 -0
- package/dist/server/app-version.js +19 -0
- package/dist/server/attachments.js +526 -0
- package/dist/server/auth-file.js +61 -0
- package/dist/server/bg-servers.js +154 -0
- package/dist/server/client-eviction.js +19 -0
- package/dist/server/client-id.js +9 -0
- package/dist/server/client-state.js +202 -0
- package/dist/server/codemode-image-routes.js +36 -0
- package/dist/server/codemode-presentation.js +19 -0
- package/dist/server/component-updates.js +135 -0
- package/dist/server/context-breakdown.js +56 -0
- package/dist/server/control-socket.js +178 -0
- package/dist/server/conversation.js +65 -0
- package/dist/server/dir-browser.js +77 -0
- package/dist/server/editor-snapshot.js +38 -0
- package/dist/server/ensure-bash.js +80 -0
- package/dist/server/extension-display.js +23 -0
- package/dist/server/extensions-catalog.js +64 -0
- package/dist/server/extensions-model.js +46 -0
- package/dist/server/extensions-routes.js +255 -0
- package/dist/server/extensions-worker.js +188 -0
- package/dist/server/files-service.js +787 -0
- package/dist/server/index.js +1033 -0
- package/dist/server/markdown-images.js +42 -0
- package/dist/server/model-admin.js +801 -0
- package/dist/server/model-config-merge.js +32 -0
- package/dist/server/native-mcp-config.js +103 -0
- package/dist/server/native-mcp-presentation.js +12 -0
- package/dist/server/native-prompt-template.js +12 -0
- package/dist/server/native-tools.js +32 -0
- package/dist/server/node-agent.js +262 -0
- package/dist/server/node-sources.js +142 -0
- package/dist/server/node-workbench.js +729 -0
- package/dist/server/origin-policy.js +50 -0
- package/dist/server/patch-node-pty.js +96 -0
- package/dist/server/pi-environment.js +203 -0
- package/dist/server/plan/extension.js +74 -0
- package/dist/server/plan/progress.js +90 -0
- package/dist/server/plan/settings.js +41 -0
- package/dist/server/plan/state.js +123 -0
- package/dist/server/plugin-facilities.js +304 -0
- package/dist/server/plugin-updater.js +245 -0
- package/dist/server/plugins.js +712 -0
- package/dist/server/private-file.js +28 -0
- package/dist/server/process-utils.js +112 -0
- package/dist/server/prompt-delivery.js +80 -0
- package/dist/server/protocol-version.js +11 -0
- package/dist/server/protocol.js +6 -0
- package/dist/server/provider-auth.js +99 -0
- package/dist/server/query-cache.js +45 -0
- package/dist/server/recovery-state.js +37 -0
- package/dist/server/scm.js +459 -0
- package/dist/server/security-headers.js +24 -0
- package/dist/server/serialize.js +254 -0
- package/dist/server/session-file-read.js +225 -0
- package/dist/server/session-record-worker.js +21 -0
- package/dist/server/session-reload.js +13 -0
- package/dist/server/session-tree-controller.js +250 -0
- package/dist/server/session-tree.js +191 -0
- package/dist/server/settings-service.js +53 -0
- package/dist/server/skills-service.js +87 -0
- package/dist/server/slash-commands.js +286 -0
- package/dist/server/sqlite-preview.js +58 -0
- package/dist/server/sqlite-query.js +67 -0
- package/dist/server/sqlite-worker.js +12 -0
- package/dist/server/system-prompt-files.js +158 -0
- package/dist/server/system-prompt-routes.js +40 -0
- package/dist/server/system-prompt-view.js +49 -0
- package/dist/server/task-progress.js +233 -0
- package/dist/server/terminals.js +833 -0
- package/dist/server/text-sniff.js +269 -0
- package/dist/server/thinking-timing.js +91 -0
- package/dist/server/token-auth.js +37 -0
- package/dist/server/tool-output.js +97 -0
- package/dist/server/tool-text-incidents.js +82 -0
- package/dist/server/tool-text.js +67 -0
- package/dist/server/uploads.js +112 -0
- package/dist/server/user-attachments.js +96 -0
- package/dist/server/webui-context.js +257 -0
- package/dist/server/wiki-links.js +136 -0
- package/dist/server/wiki-pdf-worker.js +23 -0
- package/dist/server/wiki-pdf.js +36 -0
- package/dist/server/wiki-routes.js +184 -0
- package/dist/server/wiki-service.js +524 -0
- package/electron/main.mjs +607 -0
- package/extensions/webui.ts +192 -0
- package/package.json +122 -4
- package/web/dist/apple-touch-icon.png +0 -0
- package/web/dist/assets/NodeWorkbench-BiP2ktQP.js +3 -0
- package/web/dist/assets/TerminalPanel-CKaSUsN-.js +2 -0
- package/web/dist/assets/abnfDiagram-VCTEODGH-CnwRJDRL.js +1 -0
- package/web/dist/assets/arc-CG4Q7-W_.js +1 -0
- package/web/dist/assets/architectureDiagram-5GKGNRK7-BrGqX_Rg.js +36 -0
- package/web/dist/assets/blockDiagram-I7D4REHJ-C2t0PjZT.js +129 -0
- package/web/dist/assets/c4Diagram-7LVT6UL2-CadrcgGL.js +38 -0
- package/web/dist/assets/channel-C27Jizb6.js +1 -0
- package/web/dist/assets/chunk-2Q5K7J3B-CC3V2ITZ.js +1 -0
- package/web/dist/assets/chunk-5VM5RSS4-DqKSbGmE.js +15 -0
- package/web/dist/assets/chunk-F27PBJKO-_CDr3JbY.js +1 -0
- package/web/dist/assets/chunk-IMKFNOWR-BIR4H4fX.js +231 -0
- package/web/dist/assets/chunk-JWPE2WC7-CrsdBGXT.js +1 -0
- package/web/dist/assets/chunk-POPQ4Y6H-C6q9kQmf.js +1 -0
- package/web/dist/assets/chunk-SVP7TREG-l2XXTr2v.js +88 -0
- package/web/dist/assets/chunk-TICWLB2K-29I_mZgB.js +206 -0
- package/web/dist/assets/chunk-XXDRQBXY-htV9FBqi.js +1 -0
- package/web/dist/assets/classDiagram-ZZMXUADV-DYTBrAEP.js +1 -0
- package/web/dist/assets/classDiagram-v2-VYDZK3BY-DYTBrAEP.js +1 -0
- package/web/dist/assets/cose-bilkent-JH36ORCC-Cop-ihyx.js +1 -0
- package/web/dist/assets/cynefin-OW5HDTMX-IEk8qBRq.js +178 -0
- package/web/dist/assets/cynefinDiagram-5FMLGOSQ-Bmzt6Qb5.js +62 -0
- package/web/dist/assets/cytoscape.esm-Bch-eiPH.js +331 -0
- package/web/dist/assets/dagre-GXQ25YYZ-DiXTJYYU.js +4 -0
- package/web/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
- package/web/dist/assets/diagram-S7CK7UJ4-BT7CLuNk.js +30 -0
- package/web/dist/assets/diagram-UQ7AKVKN-BZZ3nTCO.js +41 -0
- package/web/dist/assets/diagram-VSXAHHWV-Dal8LeZh.js +3 -0
- package/web/dist/assets/diagram-VX7I27RA-D6V9FfNA.js +24 -0
- package/web/dist/assets/diagram-Z3DM3KII-KUAIcFiD.js +24 -0
- package/web/dist/assets/ebnfDiagram-PWID7BFC-s0FEPZrV.js +1 -0
- package/web/dist/assets/erDiagram-RLTQ6QDP-CiIQPFCD.js +99 -0
- package/web/dist/assets/flowDiagram-HODETNUW-CvG4MYWn.js +168 -0
- package/web/dist/assets/ganttDiagram-EL5Y4UJY-CUf115ii.js +292 -0
- package/web/dist/assets/gitGraphDiagram-WWUBYQGX-o3K1r6fL.js +106 -0
- package/web/dist/assets/index-C3vDFSEV.css +10 -0
- package/web/dist/assets/index-C5or_GAq.js +276 -0
- package/web/dist/assets/infoDiagram-27XIBGKW-BxiBeQ8E.js +2 -0
- package/web/dist/assets/init-Gi6I4Gst.js +1 -0
- package/web/dist/assets/ishikawaDiagram-5VMMS53U-B1ONuCkH.js +70 -0
- package/web/dist/assets/journeyDiagram-3NMN7TZE-DwmuoXcX.js +139 -0
- package/web/dist/assets/kanban-definition-UXKFOSKX-pMKHubxH.js +89 -0
- package/web/dist/assets/katex-C5jXJg4s.js +257 -0
- package/web/dist/assets/layout-CEwfjQR0.js +1 -0
- package/web/dist/assets/linear-D22Gdbt1.js +1 -0
- package/web/dist/assets/markdown-BhJ_LInD.js +51 -0
- package/web/dist/assets/mermaid.core-CO2nrD04.js +312 -0
- package/web/dist/assets/mindmap-definition-YA3MSWOX-9MQn9ZAp.js +96 -0
- package/web/dist/assets/ordinal-Cboi1Yqb.js +1 -0
- package/web/dist/assets/pegDiagram-XKGWAZYB-Dy6fLjM-.js +1 -0
- package/web/dist/assets/pieDiagram-E7YTZNPT-EDLez6nA.js +39 -0
- package/web/dist/assets/quadrantDiagram-AXDQQJYC-Cgcd9mvy.js +7 -0
- package/web/dist/assets/railroadDiagram-O6MQD6OU-B4Td0tjj.js +1 -0
- package/web/dist/assets/react-BddUxV-9.js +24 -0
- package/web/dist/assets/requirementDiagram-BXWQKSXE-aF2QNZ78.js +84 -0
- package/web/dist/assets/sankeyDiagram-P5KCCOFB-DgTYwYMa.js +40 -0
- package/web/dist/assets/sequenceDiagram-WJ2MYXX4-EUcddWUc.js +162 -0
- package/web/dist/assets/sizeCapture-INFHLROL-CXHOv9Sx.js +1 -0
- package/web/dist/assets/stateDiagram-D77RDMKH-B-xlgfaK.js +1 -0
- package/web/dist/assets/stateDiagram-v2-MP3YSRHH-kwAwAlWX.js +1 -0
- package/web/dist/assets/swimlanes-42K2YHIH-BtFBOCDh.js +1 -0
- package/web/dist/assets/swimlanesDiagram-VR7AAH4N-D5j5Cj3S.js +8 -0
- package/web/dist/assets/timeline-definition-24CTP7MA-CZlQY6pb.js +120 -0
- package/web/dist/assets/vennDiagram-4TSXK5OY-Cn61dJvL.js +34 -0
- package/web/dist/assets/wardleyDiagram-VM6X3IG4-fiMNrWpr.js +78 -0
- package/web/dist/assets/xterm-6GBZ9nXN.css +32 -0
- package/web/dist/assets/xterm-D1D2FVe3.js +38 -0
- package/web/dist/assets/xychartDiagram-S5SC5T6Z-CuzM_sof.js +7 -0
- package/web/dist/brand-mark.svg +5 -0
- package/web/dist/favicon-32.png +0 -0
- package/web/dist/favicon.svg +5 -0
- package/web/dist/fonts/WIKI-FONTS.md +5 -0
- package/web/dist/fonts/ibm-plex-mono/ibm-plex-mono-latin-400-normal.woff2 +0 -0
- package/web/dist/fonts/ibm-plex-mono/ibm-plex-mono-latin-500-normal.woff2 +0 -0
- package/web/dist/fonts/ibm-plex-mono/ibm-plex-mono-latin-600-normal.woff2 +0 -0
- package/web/dist/fonts/ibm-plex-mono/ibm-plex-mono-latin-ext-400-normal.woff2 +0 -0
- package/web/dist/fonts/ibm-plex-mono/ibm-plex-mono-latin-ext-500-normal.woff2 +0 -0
- package/web/dist/fonts/ibm-plex-mono/ibm-plex-mono-latin-ext-600-normal.woff2 +0 -0
- package/web/dist/fonts/ibm-plex-sans/LICENSE +93 -0
- package/web/dist/fonts/ibm-plex-sans/ibm-plex-sans-latin-400-normal.woff2 +0 -0
- package/web/dist/fonts/ibm-plex-sans/ibm-plex-sans-latin-500-normal.woff2 +0 -0
- package/web/dist/fonts/ibm-plex-sans/ibm-plex-sans-latin-600-normal.woff2 +0 -0
- package/web/dist/fonts/ibm-plex-sans/ibm-plex-sans-latin-ext-400-normal.woff2 +0 -0
- package/web/dist/fonts/ibm-plex-sans/ibm-plex-sans-latin-ext-500-normal.woff2 +0 -0
- package/web/dist/fonts/ibm-plex-sans/ibm-plex-sans-latin-ext-600-normal.woff2 +0 -0
- package/web/dist/fonts/inter/inter-latin-400-normal.woff2 +0 -0
- package/web/dist/fonts/inter/inter-latin-500-normal.woff2 +0 -0
- package/web/dist/fonts/inter/inter-latin-600-normal.woff2 +0 -0
- package/web/dist/fonts/inter/inter-latin-700-normal.woff2 +0 -0
- package/web/dist/fonts/inter/inter-latin-800-normal.woff2 +0 -0
- package/web/dist/fonts/inter/inter-latin-ext-400-normal.woff2 +0 -0
- package/web/dist/fonts/inter/inter-latin-ext-500-normal.woff2 +0 -0
- package/web/dist/fonts/inter/inter-latin-ext-600-normal.woff2 +0 -0
- package/web/dist/fonts/inter/inter-latin-ext-700-normal.woff2 +0 -0
- package/web/dist/fonts/inter/inter-latin-ext-800-normal.woff2 +0 -0
- package/web/dist/fonts/jetbrains-mono/LICENSE +93 -0
- package/web/dist/fonts/jetbrains-mono/jetbrains-mono-latin-400-normal.woff2 +0 -0
- package/web/dist/fonts/jetbrains-mono/jetbrains-mono-latin-500-normal.woff2 +0 -0
- package/web/dist/fonts/jetbrains-mono/jetbrains-mono-latin-600-normal.woff2 +0 -0
- package/web/dist/fonts/jetbrains-mono/jetbrains-mono-latin-ext-400-normal.woff2 +0 -0
- package/web/dist/fonts/jetbrains-mono/jetbrains-mono-latin-ext-500-normal.woff2 +0 -0
- package/web/dist/fonts/jetbrains-mono/jetbrains-mono-latin-ext-600-normal.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/LICENSE +94 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-0.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-1.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-10.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-100.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-11.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-12.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-13.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-14.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-15.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-16.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-17.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-18.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-19.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-2.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-20.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-21.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-22.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-23.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-24.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-25.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-26.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-27.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-28.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-29.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-3.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-30.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-31.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-32.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-33.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-34.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-35.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-36.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-37.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-38.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-39.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-4.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-40.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-41.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-42.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-43.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-44.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-45.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-46.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-47.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-48.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-49.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-5.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-50.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-51.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-52.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-53.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-54.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-55.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-56.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-57.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-58.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-59.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-6.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-60.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-61.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-62.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-63.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-64.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-65.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-66.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-67.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-68.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-69.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-7.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-70.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-71.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-72.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-73.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-74.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-75.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-76.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-77.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-78.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-79.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-8.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-80.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-81.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-82.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-83.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-84.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-85.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-86.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-87.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-88.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-89.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-9.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-90.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-91.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-92.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-93.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-94.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-95.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-96.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-97.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-98.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-99.woff2 +0 -0
- package/web/dist/fonts/source-serif-4/LICENSE +93 -0
- package/web/dist/fonts/source-serif-4/source-serif-4-500-0.woff2 +0 -0
- package/web/dist/fonts/source-serif-4/source-serif-4-500-1.woff2 +0 -0
- package/web/dist/fonts/source-serif-4/source-serif-4-500-2.woff2 +0 -0
- package/web/dist/fonts/source-serif-4/source-serif-4-500-3.woff2 +0 -0
- package/web/dist/fonts/source-serif-4/source-serif-4-500-4.woff2 +0 -0
- package/web/dist/fonts/source-serif-4/source-serif-4-500-5.woff2 +0 -0
- package/web/dist/icon/1a-mark.svg +1 -0
- package/web/dist/icon.ico +0 -0
- package/web/dist/index.html +18 -0
- package/web/public/apple-touch-icon.png +0 -0
- package/web/public/brand-mark.svg +5 -0
- package/web/public/favicon-32.png +0 -0
- package/web/public/favicon.svg +5 -0
- package/web/public/fonts/WIKI-FONTS.md +5 -0
- package/web/public/fonts/ibm-plex-mono/ibm-plex-mono-latin-400-normal.woff2 +0 -0
- package/web/public/fonts/ibm-plex-mono/ibm-plex-mono-latin-500-normal.woff2 +0 -0
- package/web/public/fonts/ibm-plex-mono/ibm-plex-mono-latin-600-normal.woff2 +0 -0
- package/web/public/fonts/ibm-plex-mono/ibm-plex-mono-latin-ext-400-normal.woff2 +0 -0
- package/web/public/fonts/ibm-plex-mono/ibm-plex-mono-latin-ext-500-normal.woff2 +0 -0
- package/web/public/fonts/ibm-plex-mono/ibm-plex-mono-latin-ext-600-normal.woff2 +0 -0
- package/web/public/fonts/ibm-plex-sans/LICENSE +93 -0
- package/web/public/fonts/ibm-plex-sans/ibm-plex-sans-latin-400-normal.woff2 +0 -0
- package/web/public/fonts/ibm-plex-sans/ibm-plex-sans-latin-500-normal.woff2 +0 -0
- package/web/public/fonts/ibm-plex-sans/ibm-plex-sans-latin-600-normal.woff2 +0 -0
- package/web/public/fonts/ibm-plex-sans/ibm-plex-sans-latin-ext-400-normal.woff2 +0 -0
- package/web/public/fonts/ibm-plex-sans/ibm-plex-sans-latin-ext-500-normal.woff2 +0 -0
- package/web/public/fonts/ibm-plex-sans/ibm-plex-sans-latin-ext-600-normal.woff2 +0 -0
- package/web/public/fonts/inter/inter-latin-400-normal.woff2 +0 -0
- package/web/public/fonts/inter/inter-latin-500-normal.woff2 +0 -0
- package/web/public/fonts/inter/inter-latin-600-normal.woff2 +0 -0
- package/web/public/fonts/inter/inter-latin-700-normal.woff2 +0 -0
- package/web/public/fonts/inter/inter-latin-800-normal.woff2 +0 -0
- package/web/public/fonts/inter/inter-latin-ext-400-normal.woff2 +0 -0
- package/web/public/fonts/inter/inter-latin-ext-500-normal.woff2 +0 -0
- package/web/public/fonts/inter/inter-latin-ext-600-normal.woff2 +0 -0
- package/web/public/fonts/inter/inter-latin-ext-700-normal.woff2 +0 -0
- package/web/public/fonts/inter/inter-latin-ext-800-normal.woff2 +0 -0
- package/web/public/fonts/jetbrains-mono/LICENSE +93 -0
- package/web/public/fonts/jetbrains-mono/jetbrains-mono-latin-400-normal.woff2 +0 -0
- package/web/public/fonts/jetbrains-mono/jetbrains-mono-latin-500-normal.woff2 +0 -0
- package/web/public/fonts/jetbrains-mono/jetbrains-mono-latin-600-normal.woff2 +0 -0
- package/web/public/fonts/jetbrains-mono/jetbrains-mono-latin-ext-400-normal.woff2 +0 -0
- package/web/public/fonts/jetbrains-mono/jetbrains-mono-latin-ext-500-normal.woff2 +0 -0
- package/web/public/fonts/jetbrains-mono/jetbrains-mono-latin-ext-600-normal.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/LICENSE +94 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-0.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-1.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-10.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-100.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-11.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-12.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-13.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-14.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-15.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-16.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-17.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-18.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-19.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-2.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-20.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-21.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-22.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-23.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-24.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-25.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-26.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-27.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-28.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-29.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-3.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-30.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-31.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-32.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-33.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-34.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-35.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-36.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-37.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-38.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-39.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-4.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-40.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-41.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-42.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-43.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-44.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-45.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-46.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-47.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-48.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-49.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-5.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-50.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-51.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-52.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-53.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-54.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-55.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-56.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-57.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-58.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-59.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-6.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-60.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-61.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-62.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-63.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-64.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-65.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-66.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-67.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-68.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-69.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-7.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-70.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-71.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-72.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-73.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-74.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-75.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-76.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-77.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-78.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-79.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-8.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-80.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-81.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-82.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-83.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-84.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-85.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-86.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-87.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-88.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-89.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-9.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-90.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-91.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-92.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-93.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-94.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-95.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-96.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-97.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-98.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-99.woff2 +0 -0
- package/web/public/fonts/source-serif-4/LICENSE +93 -0
- package/web/public/fonts/source-serif-4/source-serif-4-500-0.woff2 +0 -0
- package/web/public/fonts/source-serif-4/source-serif-4-500-1.woff2 +0 -0
- package/web/public/fonts/source-serif-4/source-serif-4-500-2.woff2 +0 -0
- package/web/public/fonts/source-serif-4/source-serif-4-500-3.woff2 +0 -0
- package/web/public/fonts/source-serif-4/source-serif-4-500-4.woff2 +0 -0
- package/web/public/fonts/source-serif-4/source-serif-4-500-5.woff2 +0 -0
- package/web/public/icon/1a-mark.svg +1 -0
- package/web/public/icon.ico +0 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export const isChatModelConfig = (model) => model.type === undefined || model.type === "chat";
|
|
2
|
+
/** A chat editor owns chat fields only; keep provider operations and typed models intact. */
|
|
3
|
+
export function mergeChatProvider(previous, config) {
|
|
4
|
+
const existing = Array.isArray(previous.models) ? previous.models : [];
|
|
5
|
+
const typed = existing.filter(model => !isChatModelConfig(model));
|
|
6
|
+
const ids = new Set();
|
|
7
|
+
const models = config.models.filter(model => model.id.trim()).map(model => {
|
|
8
|
+
const id = model.id.trim();
|
|
9
|
+
if (ids.has(id))
|
|
10
|
+
throw new Error(`Duplicate chat model ID: ${id}`);
|
|
11
|
+
ids.add(id);
|
|
12
|
+
const result = { ...existing.find(entry => entry.id === id && isChatModelConfig(entry)), id };
|
|
13
|
+
for (const key of ["name", "reasoning", "input", "contextWindow", "maxTokens"]) {
|
|
14
|
+
delete result[key];
|
|
15
|
+
const value = model[key];
|
|
16
|
+
if (value !== undefined && value !== "")
|
|
17
|
+
result[key] = value;
|
|
18
|
+
}
|
|
19
|
+
return result;
|
|
20
|
+
});
|
|
21
|
+
const result = { ...previous, models: [...models, ...typed] };
|
|
22
|
+
for (const key of ["name", "api", "baseUrl", "authHeader"]) {
|
|
23
|
+
delete result[key];
|
|
24
|
+
const value = config[key];
|
|
25
|
+
if (value !== undefined && value !== "")
|
|
26
|
+
result[key] = value;
|
|
27
|
+
}
|
|
28
|
+
// An empty key in an existing editor means retain the server-owned credential.
|
|
29
|
+
if (config.apiKey?.trim())
|
|
30
|
+
result.apiKey = config.apiKey.trim();
|
|
31
|
+
return result;
|
|
32
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { pathToFileURL } from "node:url";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { existsSync, readFileSync, mkdirSync, writeFileSync, renameSync } from "node:fs";
|
|
5
|
+
import { dirname, join } from "node:path";
|
|
6
|
+
import { getAgentDir, ProjectTrustStore } from "@earendil-works/pi-coding-agent";
|
|
7
|
+
const sdkDirectory = createRequire(import.meta.url).resolve.paths("@earendil-works/pi-coding-agent")?.map(path => join(path, "@earendil-works/pi-coding-agent/dist/core/mcp-servers.js")).find(path => existsSync(path));
|
|
8
|
+
if (!sdkDirectory)
|
|
9
|
+
throw new Error("Pi MCP configuration validator missing");
|
|
10
|
+
const { validateMcpServerConfig } = await import(pathToFileURL(sdkDirectory).href);
|
|
11
|
+
const MASK = "••••••••";
|
|
12
|
+
function read(path) { try {
|
|
13
|
+
return readFileSync(path, "utf8");
|
|
14
|
+
}
|
|
15
|
+
catch (error) {
|
|
16
|
+
if (error.code === "ENOENT")
|
|
17
|
+
return "{}";
|
|
18
|
+
throw error;
|
|
19
|
+
} }
|
|
20
|
+
function version(raw) { return createHash("sha256").update(raw).digest("hex"); }
|
|
21
|
+
function transform(value, old, mask, secret = false) {
|
|
22
|
+
if (Array.isArray(value))
|
|
23
|
+
return value.map((v, i) => transform(v, Array.isArray(old) ? old[i] : undefined, mask, secret));
|
|
24
|
+
if (value && typeof value === "object")
|
|
25
|
+
return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, transform(v, old && typeof old === "object" ? old[k] : undefined, mask, secret || /secret|token|password|apiKey|headers|env/i.test(k))]));
|
|
26
|
+
if (secret && typeof value === "string") {
|
|
27
|
+
if (mask)
|
|
28
|
+
return value ? MASK : "";
|
|
29
|
+
if (value === MASK) {
|
|
30
|
+
if (typeof old !== "string")
|
|
31
|
+
throw new Error("Masked value has no existing secret");
|
|
32
|
+
return old;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return value;
|
|
36
|
+
}
|
|
37
|
+
export class NativeMcpConfigService {
|
|
38
|
+
isTrusted(cwd) { return new ProjectTrustStore(getAgentDir()).get(cwd) === true; }
|
|
39
|
+
path(cwd, scope) { if (scope !== "global" && scope !== "project")
|
|
40
|
+
throw new Error("Invalid MCP configuration scope"); return scope === "global" ? join(getAgentDir(), "mcp.json") : join(cwd, ".pi", "mcp.json"); }
|
|
41
|
+
get(cwd, scope) {
|
|
42
|
+
const path = this.path(cwd, scope), raw = read(path), document = JSON.parse(raw);
|
|
43
|
+
if (!document || typeof document !== "object" || Array.isArray(document))
|
|
44
|
+
throw new Error("MCP configuration must be an object");
|
|
45
|
+
const inheritedAutoEnableCodemode = scope === "global" || JSON.parse(read(this.path(cwd, "global")))?.autoEnableCodemode !== false;
|
|
46
|
+
return { path, paths: { global: this.path(cwd, "global"), project: this.path(cwd, "project") }, scope, version: version(raw), document: transform(document, undefined, true), trusted: this.isTrusted(cwd), inheritedAutoEnableCodemode };
|
|
47
|
+
}
|
|
48
|
+
save(cwd, scope, expected, incoming) {
|
|
49
|
+
if (!incoming || typeof incoming !== "object" || Array.isArray(incoming))
|
|
50
|
+
throw new Error("MCP configuration must be an object");
|
|
51
|
+
const path = this.path(cwd, scope), raw = read(path);
|
|
52
|
+
if (version(raw) !== expected)
|
|
53
|
+
throw new Error("MCP configuration changed externally; refresh before saving");
|
|
54
|
+
const old = JSON.parse(raw);
|
|
55
|
+
const document = transform({ ...old, ...incoming }, old, false);
|
|
56
|
+
document.mcpServers ??= {};
|
|
57
|
+
if (!document.mcpServers || Array.isArray(document.mcpServers) || typeof document.mcpServers !== "object")
|
|
58
|
+
throw new Error("mcpServers must be an object");
|
|
59
|
+
if (document.autoEnableCodemode !== undefined && typeof document.autoEnableCodemode !== "boolean")
|
|
60
|
+
throw new Error("autoEnableCodemode must be a boolean");
|
|
61
|
+
for (const [name, config] of Object.entries(document.mcpServers)) {
|
|
62
|
+
let candidate = config;
|
|
63
|
+
if (scope === "project" && config && typeof config === "object" && !Array.isArray(config) && !["command", "url", "type"].some(key => Object.hasOwn(config, key))) {
|
|
64
|
+
if (Object.keys(config).some(key => !["enabled", "exposure", "toolExposure"].includes(key)))
|
|
65
|
+
throw new Error("Project overrides only allow enabled, exposure, and toolExposure");
|
|
66
|
+
const base = JSON.parse(read(this.path(cwd, "global"))).mcpServers?.[name];
|
|
67
|
+
if (!base)
|
|
68
|
+
throw new Error(`No global MCP server to override: ${name}`);
|
|
69
|
+
candidate = { ...base, ...config };
|
|
70
|
+
}
|
|
71
|
+
const result = validateMcpServerConfig(name, candidate);
|
|
72
|
+
if (typeof result === "string")
|
|
73
|
+
throw new Error(result);
|
|
74
|
+
if (scope === "project" && config.auth)
|
|
75
|
+
throw new Error("Provider authentication is allowed only in global MCP configuration");
|
|
76
|
+
}
|
|
77
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
78
|
+
writeFileSync(`${path}.webui.tmp`, JSON.stringify(document, null, 2) + "\n", { mode: 0o600 });
|
|
79
|
+
renameSync(`${path}.webui.tmp`, path);
|
|
80
|
+
return this.get(cwd, scope);
|
|
81
|
+
}
|
|
82
|
+
codemode(cwd, scope, effective = {}) {
|
|
83
|
+
const path = join(scope === "global" ? getAgentDir() : join(cwd, ".pi"), "settings.json"), raw = read(path);
|
|
84
|
+
const settings = JSON.parse(raw).codemode ?? {};
|
|
85
|
+
return { path, version: version(raw), mode: settings.mode ?? "on", inlineBudget: settings.inlineBudget ?? 3000, effectiveMode: effective.mode ?? "on", effectiveInlineBudget: effective.inlineBudget ?? 3000 };
|
|
86
|
+
}
|
|
87
|
+
saveCodemode(cwd, scope, expected, value) {
|
|
88
|
+
this.path(cwd, scope);
|
|
89
|
+
const path = this.codemode(cwd, scope).path, raw = read(path);
|
|
90
|
+
if (version(raw) !== expected)
|
|
91
|
+
throw new Error("Pi settings changed externally; refresh before saving");
|
|
92
|
+
const patch = value;
|
|
93
|
+
if (!patch || !["on", "only"].includes(String(patch.mode)) || !Number.isInteger(patch.inlineBudget) || Number(patch.inlineBudget) < 0 || Number(patch.inlineBudget) > 100000)
|
|
94
|
+
throw new Error("Invalid codemode settings");
|
|
95
|
+
const document = JSON.parse(raw);
|
|
96
|
+
document.codemode = { ...document.codemode, mode: patch.mode, inlineBudget: patch.inlineBudget };
|
|
97
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
98
|
+
writeFileSync(`${path}.webui.tmp`, JSON.stringify(document, null, 2) + "\n", { mode: 0o600 });
|
|
99
|
+
renameSync(`${path}.webui.tmp`, path);
|
|
100
|
+
}
|
|
101
|
+
log() { const path = join(getAgentDir(), "mcp.log"); return existsSync(path) ? read(path).slice(-32000) : ""; }
|
|
102
|
+
trust(cwd) { new ProjectTrustStore(getAgentDir()).set(cwd, true); }
|
|
103
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** Pi's official non-TUI /mcp output is the status API in 1.0.4. Keep unknown lines visible. */
|
|
2
|
+
export function parseNativeMcpStatus(text) {
|
|
3
|
+
const servers = [];
|
|
4
|
+
for (const line of text.split("\n")) {
|
|
5
|
+
const match = /^([\w-]+): (needs sign-in|disabled|disconnected|connecting|connected|starting|failed|error)(.*)$/.exec(line);
|
|
6
|
+
if (match)
|
|
7
|
+
servers.push({ name: match[1], state: match[2] === "needs sign-in" ? "needs-auth" : match[2], toolCount: Number(/, (\d+) tools/.exec(match[3])?.[1] ?? 0), detail: line });
|
|
8
|
+
else if (/^\s+/.test(line) && servers.length)
|
|
9
|
+
servers[servers.length - 1].detail += `\n${line.trim()}`;
|
|
10
|
+
}
|
|
11
|
+
return servers;
|
|
12
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { pathToFileURL } from "node:url";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { getPackageDir, VERSION } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
/** Internal API of pinned 1.0.4, located by the SDK itself in npm and desktop layouts. */
|
|
5
|
+
export async function expandNativeTemplate(text, templates) {
|
|
6
|
+
if (VERSION !== "1.0.4")
|
|
7
|
+
throw new Error("Unsupported native prompt template contract");
|
|
8
|
+
const module = await import(pathToFileURL(join(getPackageDir(), "dist/core/prompt-templates.js")).href);
|
|
9
|
+
if (typeof module.expandPromptTemplate !== "function")
|
|
10
|
+
throw new Error("Native prompt template adapter unavailable");
|
|
11
|
+
return module.expandPromptTemplate(text, templates);
|
|
12
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { createPlanExtension } from "./plan/extension.js";
|
|
2
|
+
import { SettingsManager, createCodemodeExtension, createMcpExtension, createToolSearchExtension } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
const runOverrides = new WeakMap();
|
|
4
|
+
export function setConversationRunSettings(settings, values) {
|
|
5
|
+
const overrides = { ...runOverrides.get(settings), ...(typeof values.autoCompaction === "boolean" ? { compaction: { enabled: values.autoCompaction } } : {}), ...(typeof values.autoRetry === "boolean" ? { retry: { enabled: values.autoRetry } } : {}) };
|
|
6
|
+
runOverrides.set(settings, overrides);
|
|
7
|
+
}
|
|
8
|
+
/** A native manager with conversation-local overrides; native writes stay native. */
|
|
9
|
+
export function conversationSettings(cwd, agentDir) {
|
|
10
|
+
const settings = SettingsManager.create(cwd, agentDir);
|
|
11
|
+
const getCompactionEnabled = settings.getCompactionEnabled.bind(settings);
|
|
12
|
+
const getRetryEnabled = settings.getRetryEnabled.bind(settings);
|
|
13
|
+
const getDefaultTools = settings.getDefaultTools.bind(settings);
|
|
14
|
+
const defaultTools = SettingsManager.inMemory({ defaultTools: ["+codemode", "+tool_search"] }).getDefaultTools();
|
|
15
|
+
settings.getCompactionEnabled = () => runOverrides.get(settings)?.compaction?.enabled ?? getCompactionEnabled();
|
|
16
|
+
settings.getRetryEnabled = () => runOverrides.get(settings)?.retry?.enabled ?? getRetryEnabled();
|
|
17
|
+
settings.getDefaultTools = () => [...(getDefaultTools() ?? defaultTools)];
|
|
18
|
+
return settings;
|
|
19
|
+
}
|
|
20
|
+
/** Same native extensions as the Pi CLI; activation follows the user's settings. */
|
|
21
|
+
export function nativeToolExtensions() {
|
|
22
|
+
return [
|
|
23
|
+
{ name: "pi-harness-plan", replaceable: true, factory: createPlanExtension() },
|
|
24
|
+
{ name: "codemode", builtin: true, replaceable: true, factory: createCodemodeExtension() },
|
|
25
|
+
{ name: "tool-search", builtin: true, replaceable: true, factory: createToolSearchExtension() },
|
|
26
|
+
{ name: "mcp", builtin: true, replaceable: true, factory: createMcpExtension() },
|
|
27
|
+
];
|
|
28
|
+
}
|
|
29
|
+
/** Stable settings aliases for the SDK's named inline extension paths. */
|
|
30
|
+
export function nativeExtensionPath(path) {
|
|
31
|
+
return /^builtin:(mcp|tool-search|codemode)$/.test(path) ? `<inline:${path.slice(8)}>` : path;
|
|
32
|
+
}
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
/** Native Pi RPC over one SSH exec channel. No local model or terminal injection. */
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { StringDecoder } from "node:string_decoder";
|
|
4
|
+
import { serializeMessage } from "./serialize.js";
|
|
5
|
+
const MAX_RECORD = 16 * 1024 * 1024;
|
|
6
|
+
const quote = (value) => `'${value.replace(/'/g, `'\\''`)}'`;
|
|
7
|
+
export function nodeAgentCommand(cwd) {
|
|
8
|
+
if (!cwd.startsWith("/") || /[\0\r\n]/.test(cwd) || cwd.length > 4096)
|
|
9
|
+
throw new Error("Invalid remote working directory");
|
|
10
|
+
return `sh -lc ${quote(`cd ${quote(cwd)} && exec pi --mode rpc`)}`;
|
|
11
|
+
}
|
|
12
|
+
export class NodeAgent {
|
|
13
|
+
stream;
|
|
14
|
+
changed;
|
|
15
|
+
id = randomUUID();
|
|
16
|
+
buffer = "";
|
|
17
|
+
decoder = new StringDecoder("utf8");
|
|
18
|
+
stderr = "";
|
|
19
|
+
seq = 0;
|
|
20
|
+
revision = 0;
|
|
21
|
+
dialogTimers = new Map();
|
|
22
|
+
timer;
|
|
23
|
+
pending = new Map();
|
|
24
|
+
state;
|
|
25
|
+
constructor(stream, cwd, changed) {
|
|
26
|
+
this.stream = stream;
|
|
27
|
+
this.changed = changed;
|
|
28
|
+
this.state = { id: this.id, cwd, phase: "starting", running: false, messages: [], tools: [], models: [], dialogs: [] };
|
|
29
|
+
stream.on("data", (data) => this.receive(this.decoder.write(Buffer.isBuffer(data) ? data : Buffer.from(data))));
|
|
30
|
+
stream.stderr.on("data", data => { this.stderr = (this.stderr + data.toString()).slice(-8000); });
|
|
31
|
+
stream.on("error", (error) => this.finish(error.message));
|
|
32
|
+
stream.on("close", () => this.finish(this.stderr || "Remote Pi exited"));
|
|
33
|
+
}
|
|
34
|
+
get busy() { return this.state.phase !== "closed" && (this.state.phase === "starting" || this.state.running || this.pending.size > 0 || this.state.dialogs.length > 0); }
|
|
35
|
+
snapshot() { return this.state; }
|
|
36
|
+
publish(immediate = false) {
|
|
37
|
+
if (this.timer) {
|
|
38
|
+
if (!immediate)
|
|
39
|
+
return;
|
|
40
|
+
clearTimeout(this.timer);
|
|
41
|
+
this.timer = undefined;
|
|
42
|
+
}
|
|
43
|
+
if (immediate)
|
|
44
|
+
this.changed(this.state);
|
|
45
|
+
else
|
|
46
|
+
this.timer = setTimeout(() => { this.timer = undefined; this.changed(this.state); }, 60);
|
|
47
|
+
}
|
|
48
|
+
set(patch, immediate = false) { this.state = { ...this.state, ...patch }; this.publish(immediate); }
|
|
49
|
+
finish(error) {
|
|
50
|
+
if (this.state.phase === "closed")
|
|
51
|
+
return;
|
|
52
|
+
for (const task of this.pending.values()) {
|
|
53
|
+
clearTimeout(task.timer);
|
|
54
|
+
task.reject(new Error(error || "Remote Pi closed"));
|
|
55
|
+
}
|
|
56
|
+
this.pending.clear();
|
|
57
|
+
for (const timer of this.dialogTimers.values())
|
|
58
|
+
clearTimeout(timer);
|
|
59
|
+
this.dialogTimers.clear();
|
|
60
|
+
this.set({ phase: "closed", running: false, dialogs: [], error }, true);
|
|
61
|
+
}
|
|
62
|
+
close() {
|
|
63
|
+
this.finish();
|
|
64
|
+
try {
|
|
65
|
+
this.stream.signal("TERM");
|
|
66
|
+
}
|
|
67
|
+
catch { /* SSH may already be gone. */ }
|
|
68
|
+
this.stream.end();
|
|
69
|
+
this.stream.destroy();
|
|
70
|
+
}
|
|
71
|
+
receive(chunk) {
|
|
72
|
+
if (this.state.phase === "closed")
|
|
73
|
+
return;
|
|
74
|
+
this.buffer += chunk;
|
|
75
|
+
let index;
|
|
76
|
+
while ((index = this.buffer.indexOf("\n")) >= 0) {
|
|
77
|
+
const line = this.buffer.slice(0, index);
|
|
78
|
+
this.buffer = this.buffer.slice(index + 1);
|
|
79
|
+
if (line.length > MAX_RECORD) {
|
|
80
|
+
this.finish("Remote Pi response exceeds 16 MiB");
|
|
81
|
+
this.close();
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (!line.trim())
|
|
85
|
+
continue;
|
|
86
|
+
try {
|
|
87
|
+
this.event(JSON.parse(line));
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
this.finish(`Invalid remote Pi RPC response: ${error.message}`);
|
|
91
|
+
this.close();
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if (this.buffer.length > MAX_RECORD) {
|
|
96
|
+
this.finish("Remote Pi response exceeds 16 MiB");
|
|
97
|
+
this.close();
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
request(type, fields = {}) {
|
|
101
|
+
if (this.state.phase === "closed")
|
|
102
|
+
return Promise.reject(new Error("Remote Pi is not running"));
|
|
103
|
+
if (this.pending.size >= 32)
|
|
104
|
+
return Promise.reject(new Error("Too many pending remote commands"));
|
|
105
|
+
const id = randomUUID();
|
|
106
|
+
return new Promise((resolve, reject) => {
|
|
107
|
+
const timer = setTimeout(() => { this.pending.delete(id); reject(new Error(`Remote Pi ${type} timed out`)); }, 30000);
|
|
108
|
+
this.pending.set(id, { resolve, reject, timer });
|
|
109
|
+
try {
|
|
110
|
+
this.stream.write(JSON.stringify({ ...fields, id, type }) + "\n");
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
clearTimeout(timer);
|
|
114
|
+
this.pending.delete(id);
|
|
115
|
+
reject(error);
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
async start() {
|
|
120
|
+
try {
|
|
121
|
+
await this.refresh();
|
|
122
|
+
const available = await this.request("get_available_models");
|
|
123
|
+
this.set({ phase: "ready", models: (available.models ?? []).map((m) => ({ id: String(m.id), provider: String(m.provider), name: String(m.name ?? m.id) })) }, true);
|
|
124
|
+
}
|
|
125
|
+
catch (error) {
|
|
126
|
+
this.finish(`${error.message}${this.stderr ? `\n${this.stderr}` : ""}`);
|
|
127
|
+
this.close();
|
|
128
|
+
throw error;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
retainMessages(messages) {
|
|
132
|
+
let bytes = 0, start = messages.length;
|
|
133
|
+
while (start > 0 && messages.length - start < 200) {
|
|
134
|
+
const size = Buffer.byteLength(JSON.stringify(messages[start - 1]));
|
|
135
|
+
if (start < messages.length && bytes + size > 4 * 1024 * 1024)
|
|
136
|
+
break;
|
|
137
|
+
bytes += size;
|
|
138
|
+
start--;
|
|
139
|
+
}
|
|
140
|
+
return messages.slice(start);
|
|
141
|
+
}
|
|
142
|
+
async refresh() {
|
|
143
|
+
const revision = this.revision;
|
|
144
|
+
const state = await this.request("get_state");
|
|
145
|
+
if (typeof state.sessionId !== "string" || typeof state.isStreaming !== "boolean")
|
|
146
|
+
throw new Error("Remote Pi must support the native 1.0.4 RPC protocol");
|
|
147
|
+
const history = await this.request("get_messages");
|
|
148
|
+
const messages = (history.messages ?? []).slice(-200).map((m, index) => serializeMessage(m, index)).filter((m) => !!m);
|
|
149
|
+
if (this.revision !== revision)
|
|
150
|
+
return;
|
|
151
|
+
this.seq = messages.length;
|
|
152
|
+
this.set({ sessionId: state.sessionId, sessionFile: state.sessionFile, model: state.model ? { id: String(state.model.id), provider: String(state.model.provider), name: String(state.model.name ?? state.model.id) } : undefined, thinkingLevel: state.thinkingLevel, running: state.isStreaming || state.isCompacting, messages: this.retainMessages(messages) });
|
|
153
|
+
}
|
|
154
|
+
async prompt(text, queue) {
|
|
155
|
+
if (this.state.phase !== "ready")
|
|
156
|
+
throw new Error("Remote Pi is not ready");
|
|
157
|
+
if (!text.trim() || text.length > 200000)
|
|
158
|
+
throw new Error("Invalid remote prompt");
|
|
159
|
+
this.set({ error: undefined });
|
|
160
|
+
await this.request("prompt", { message: text, ...(queue ? { streamingBehavior: queue } : {}) });
|
|
161
|
+
}
|
|
162
|
+
async abort() { await this.request("abort"); }
|
|
163
|
+
async newSession() {
|
|
164
|
+
if (this.state.running || this.state.dialogs.length)
|
|
165
|
+
throw new Error("Stop the remote task first");
|
|
166
|
+
const result = await this.request("new_session");
|
|
167
|
+
if (result.cancelled)
|
|
168
|
+
return;
|
|
169
|
+
this.set({ tools: [], streamingMessage: undefined });
|
|
170
|
+
await this.refresh();
|
|
171
|
+
}
|
|
172
|
+
async model(provider, modelId) {
|
|
173
|
+
if (this.state.running)
|
|
174
|
+
throw new Error("Stop the remote task first");
|
|
175
|
+
if (!this.state.models.some(m => m.id === modelId && m.provider === provider))
|
|
176
|
+
throw new Error("Unknown remote model");
|
|
177
|
+
await this.request("set_model", { provider, modelId });
|
|
178
|
+
await this.refresh();
|
|
179
|
+
}
|
|
180
|
+
dialog(id, response) {
|
|
181
|
+
const dialog = this.state.dialogs.find(d => d.id === id);
|
|
182
|
+
if (!dialog)
|
|
183
|
+
throw new Error("Remote dialog expired");
|
|
184
|
+
if (!response.cancelled && dialog.method === "select" && !dialog.options?.includes(response.value ?? ""))
|
|
185
|
+
throw new Error("Invalid remote selection");
|
|
186
|
+
if (response.value && response.value.length > 200000)
|
|
187
|
+
throw new Error("Remote response too large");
|
|
188
|
+
clearTimeout(this.dialogTimers.get(id));
|
|
189
|
+
this.dialogTimers.delete(id);
|
|
190
|
+
this.stream.write(JSON.stringify({ type: "extension_ui_response", id, ...response }) + "\n");
|
|
191
|
+
this.set({ dialogs: this.state.dialogs.filter(d => d.id !== id) });
|
|
192
|
+
}
|
|
193
|
+
event(event) {
|
|
194
|
+
if (!event || typeof event.type !== "string")
|
|
195
|
+
throw new Error("Missing RPC record type");
|
|
196
|
+
if (event.type === "response") {
|
|
197
|
+
const task = this.pending.get(event.id);
|
|
198
|
+
if (!task)
|
|
199
|
+
return;
|
|
200
|
+
clearTimeout(task.timer);
|
|
201
|
+
this.pending.delete(event.id);
|
|
202
|
+
if (event.success)
|
|
203
|
+
task.resolve(event.data ?? {});
|
|
204
|
+
else
|
|
205
|
+
task.reject(new Error(String(event.error ?? "Remote Pi command failed")));
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
if (["agent_start", "compaction_start", "auto_retry_start"].includes(event.type)) {
|
|
209
|
+
this.revision++;
|
|
210
|
+
this.set({ running: true, error: undefined, tools: [] });
|
|
211
|
+
}
|
|
212
|
+
if (event.type === "agent_settled") {
|
|
213
|
+
this.set({ running: false });
|
|
214
|
+
void this.refresh().catch(error => this.set({ error: error.message }));
|
|
215
|
+
}
|
|
216
|
+
if (event.type === "message_start" && event.message?.role === "assistant")
|
|
217
|
+
this.set({ streamingMessage: serializeMessage(event.message, this.seq) ?? undefined });
|
|
218
|
+
if (event.type === "message_update") {
|
|
219
|
+
const delta = event.assistantMessageEvent, current = this.state.streamingMessage;
|
|
220
|
+
if (!current || !delta || !Number.isInteger(delta.contentIndex) || delta.contentIndex < 0 || delta.contentIndex > 4096)
|
|
221
|
+
return;
|
|
222
|
+
const content = [...current.content], index = delta.contentIndex, previous = content[index];
|
|
223
|
+
if (delta.type === "text_delta")
|
|
224
|
+
content[index] = { type: "text", text: (String(previous && "text" in previous ? previous.text ?? "" : "") + String(delta.delta ?? "")).slice(-200000) };
|
|
225
|
+
else if (delta.type === "thinking_delta")
|
|
226
|
+
content[index] = { type: "thinking", thinking: (String(previous && "thinking" in previous ? previous.thinking ?? "" : "") + String(delta.delta ?? "")).slice(-200000) };
|
|
227
|
+
else if (delta.type === "toolcall_start")
|
|
228
|
+
content[index] = { type: "toolCall", id: String(delta.id), name: String(delta.toolName), argumentsText: "" };
|
|
229
|
+
else if (delta.type === "toolcall_delta" && previous?.type === "toolCall")
|
|
230
|
+
content[index] = { ...previous, argumentsText: (String(previous.argumentsText ?? "") + String(delta.delta ?? "")).slice(0, 20000) };
|
|
231
|
+
else
|
|
232
|
+
return;
|
|
233
|
+
this.set({ streamingMessage: { ...current, content: content.filter(Boolean) } });
|
|
234
|
+
}
|
|
235
|
+
if (event.type === "message_end") {
|
|
236
|
+
this.revision++;
|
|
237
|
+
const message = serializeMessage(event.message, this.seq++);
|
|
238
|
+
if (message)
|
|
239
|
+
this.set({ messages: this.retainMessages([...this.state.messages, message]), ...(event.message.role === "assistant" ? { streamingMessage: undefined } : {}) });
|
|
240
|
+
}
|
|
241
|
+
if (event.type === "tool_execution_start" || event.type === "tool_execution_end") {
|
|
242
|
+
const tools = this.state.tools.filter(t => t.id !== event.toolCallId);
|
|
243
|
+
tools.push({ id: String(event.toolCallId), name: String(event.toolName), running: event.type === "tool_execution_start", isError: !!event.isError });
|
|
244
|
+
this.set({ tools: tools.slice(-256) });
|
|
245
|
+
}
|
|
246
|
+
if (event.type === "extension_ui_request") {
|
|
247
|
+
if (["select", "confirm", "input", "editor"].includes(event.method)) {
|
|
248
|
+
if (this.state.dialogs.length >= 32) {
|
|
249
|
+
this.stream.write(JSON.stringify({ type: "extension_ui_response", id: event.id, cancelled: true }) + "\n");
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
if (typeof event.timeout === "number" && event.timeout >= 0 && event.timeout <= 2147483647)
|
|
253
|
+
this.dialogTimers.set(String(event.id), setTimeout(() => { this.dialogTimers.delete(String(event.id)); this.set({ dialogs: this.state.dialogs.filter(d => d.id !== event.id) }); }, event.timeout));
|
|
254
|
+
this.set({ dialogs: [...this.state.dialogs, { id: String(event.id), method: event.method, title: String(event.title ?? ""), message: String(event.message ?? ""), options: Array.isArray(event.options) ? event.options.map(String) : undefined, prefill: typeof event.prefill === "string" ? event.prefill : undefined }] }, true);
|
|
255
|
+
}
|
|
256
|
+
if (event.method === "notify")
|
|
257
|
+
this.set({ notice: String(event.message ?? "").slice(0, 8000) });
|
|
258
|
+
if (event.method === "set_editor_text")
|
|
259
|
+
this.set({ editorText: { id: String(event.id), text: String(event.text ?? "").slice(0, 200000) } });
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/** Read-only source adapters. Never read saved passwords or execute SSH config commands. */
|
|
2
|
+
import { readdir, readFile, stat } from "node:fs/promises";
|
|
3
|
+
import { basename, dirname, join, relative, resolve, isAbsolute } from "node:path";
|
|
4
|
+
import { homedir, userInfo } from "node:os";
|
|
5
|
+
import { decodeText } from "./text-sniff.js";
|
|
6
|
+
export function parseXshell(text, file) {
|
|
7
|
+
const sections = {};
|
|
8
|
+
let section = "";
|
|
9
|
+
for (const line of text.replace(/^\uFEFF/, "").split(/\r?\n/)) {
|
|
10
|
+
const header = line.trim().match(/^\[([^\]]+)\]$/);
|
|
11
|
+
if (header) {
|
|
12
|
+
section = header[1].toUpperCase();
|
|
13
|
+
sections[section] ??= {};
|
|
14
|
+
continue;
|
|
15
|
+
}
|
|
16
|
+
const pair = line.match(/^\s*([^=;#]+?)\s*=(.*)$/);
|
|
17
|
+
if (pair)
|
|
18
|
+
(sections[section] ??= {})[pair[1].toUpperCase()] = pair[2].trim();
|
|
19
|
+
}
|
|
20
|
+
const c = sections.CONNECTION ?? {}, a = sections["CONNECTION:AUTHENTICATION"] ?? {};
|
|
21
|
+
const host = c.HOST ?? "", port = Number(c.PORT || 22);
|
|
22
|
+
if (!host || /[\r\n\0]/.test(host) || !Number.isInteger(port) || port < 1 || port > 65535)
|
|
23
|
+
throw new Error(`${file}: 地址或端口无效`);
|
|
24
|
+
const unsupported = [];
|
|
25
|
+
if (c.PROTOCOL?.toUpperCase() !== "SSH")
|
|
26
|
+
unsupported.push("protocol");
|
|
27
|
+
for (const [name, values] of Object.entries(sections)) {
|
|
28
|
+
for (const [key, value] of Object.entries(values)) {
|
|
29
|
+
if ((/PROXY|JUMPHOST/.test(key) || /PROXY|JUMPHOST/.test(name)) && value && !/^(0|-1|none|false)$/i.test(value))
|
|
30
|
+
unsupported.push("proxy");
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
if (Object.entries(a).some(([key, value]) => /AUTH.*PROFILE/.test(key) && value && !/^(0|false|none)$/i.test(value)))
|
|
34
|
+
unsupported.push("authentication-profile");
|
|
35
|
+
const keyPath = a.USERKEY || a.PUBLICKEYFILE || a.PRIVATEKEY || undefined;
|
|
36
|
+
const auth = a.METHOD === "1" || keyPath ? "key" : "password";
|
|
37
|
+
if (a.METHOD && !["0", "1"].includes(a.METHOD))
|
|
38
|
+
unsupported.push("authentication");
|
|
39
|
+
if (!a.USERNAME)
|
|
40
|
+
unsupported.push("username");
|
|
41
|
+
if (auth === "key" && !keyPath)
|
|
42
|
+
unsupported.push("key");
|
|
43
|
+
return { name: basename(file).replace(/\.xsh$/i, ""), group: dirname(file) === "." ? "Xshell" : dirname(file), host, port, username: a.USERNAME || "", auth, keyPath, defaultDir: "/", sourceKey: file, unsupported: [...new Set(unsupported)] };
|
|
44
|
+
}
|
|
45
|
+
/** First obtained value wins, matching OpenSSH's Host ordering. Complex directives are blocked. */
|
|
46
|
+
export function parseSshConfig(text) {
|
|
47
|
+
const blocks = [{ patterns: ["*"], values: {} }];
|
|
48
|
+
let current = blocks[0];
|
|
49
|
+
const unsupported = new Set();
|
|
50
|
+
for (const line of text.split(/\r?\n/)) {
|
|
51
|
+
const m = line.trim().match(/^([^\s=#]+)(?:\s*=\s*|\s+)(.*)$/);
|
|
52
|
+
if (!m || m[1].startsWith("#"))
|
|
53
|
+
continue;
|
|
54
|
+
const key = m[1].toLowerCase();
|
|
55
|
+
const value = m[2].replace(/\s+#.*$/, "").trim().replace(/^"(.*)"$/, "$1");
|
|
56
|
+
if (key === "host") {
|
|
57
|
+
current = { patterns: value.split(/\s+/), values: {} };
|
|
58
|
+
blocks.push(current);
|
|
59
|
+
}
|
|
60
|
+
else if (key === "match" || key === "include")
|
|
61
|
+
unsupported.add(key);
|
|
62
|
+
else
|
|
63
|
+
current.values[key] ??= value;
|
|
64
|
+
}
|
|
65
|
+
const match = (pattern, alias) => new RegExp(`^${pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replaceAll("*", ".*").replaceAll("?", ".")}$`, "i").test(alias);
|
|
66
|
+
const aliases = [...new Set(blocks.flatMap((b) => b.patterns).filter((p) => !/[*!?]/.test(p)))];
|
|
67
|
+
return aliases.map((alias) => {
|
|
68
|
+
const v = {};
|
|
69
|
+
for (const b of blocks)
|
|
70
|
+
if (b.patterns.some((p) => !p.startsWith("!") && match(p, alias)) && !b.patterns.some((p) => p.startsWith("!") && match(p.slice(1), alias)))
|
|
71
|
+
for (const [k, val] of Object.entries(b.values))
|
|
72
|
+
v[k] ??= val;
|
|
73
|
+
const blocked = [...unsupported];
|
|
74
|
+
for (const key of ["proxyjump", "proxycommand", "localcommand", "remotecommand", "identityagent", "certificatefile", "localforward", "remoteforward", "dynamicforward"])
|
|
75
|
+
if (v[key] && v[key] !== "none")
|
|
76
|
+
blocked.push(key);
|
|
77
|
+
const port = Number(v.port || 22), host = v.hostname || alias;
|
|
78
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535)
|
|
79
|
+
throw new Error(`${alias}: 端口无效`);
|
|
80
|
+
if (host.includes("%") || v.identityfile?.includes("%"))
|
|
81
|
+
blocked.push("tokens");
|
|
82
|
+
return { name: alias, group: "SSH config", host, port, username: v.user || userInfo().username, auth: v.identityfile ? "key" : "agent", keyPath: v.identityfile, defaultDir: "/", sourceKey: alias, unsupported: blocked };
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
async function readBounded(path) {
|
|
86
|
+
if ((await stat(path)).size > 1024 * 1024)
|
|
87
|
+
throw new Error("配置文件超过 1 MiB");
|
|
88
|
+
const bytes = await readFile(path);
|
|
89
|
+
if (bytes[0] === 0xff && bytes[1] === 0xfe)
|
|
90
|
+
return bytes.subarray(2).toString("utf16le");
|
|
91
|
+
return decodeText(bytes);
|
|
92
|
+
}
|
|
93
|
+
export async function scanSource(source) {
|
|
94
|
+
if (source.kind === "ssh") {
|
|
95
|
+
const nodes = parseSshConfig(await readBounded(source.path));
|
|
96
|
+
if (nodes.length > 200)
|
|
97
|
+
throw new Error("每个来源最多 200 个节点");
|
|
98
|
+
return nodes;
|
|
99
|
+
}
|
|
100
|
+
const nodes = [];
|
|
101
|
+
let visited = 0;
|
|
102
|
+
async function walk(dir, depth) {
|
|
103
|
+
if (depth > 12)
|
|
104
|
+
throw new Error("会话目录层级过深");
|
|
105
|
+
for (const entry of await readdir(dir, { withFileTypes: true })) {
|
|
106
|
+
if (++visited > 4000)
|
|
107
|
+
throw new Error("会话目录文件过多");
|
|
108
|
+
const path = join(dir, entry.name);
|
|
109
|
+
if (entry.isDirectory())
|
|
110
|
+
await walk(path, depth + 1);
|
|
111
|
+
else if (entry.isFile() && /\.xsh$/i.test(entry.name)) {
|
|
112
|
+
const node = parseXshell(await readBounded(path), relative(source.path, path));
|
|
113
|
+
if (node.keyPath) {
|
|
114
|
+
node.keyPath = node.keyPath.replace(/%([^%]+)%/g, (original, name) => process.env[name] ?? original);
|
|
115
|
+
if (!isAbsolute(node.keyPath) && !node.keyPath.startsWith("~/"))
|
|
116
|
+
node.unsupported = [...node.unsupported ?? [], "key"];
|
|
117
|
+
}
|
|
118
|
+
nodes.push(node);
|
|
119
|
+
if (nodes.length > 200)
|
|
120
|
+
throw new Error("每个来源最多 200 个节点");
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
await walk(source.path, 0);
|
|
125
|
+
return nodes;
|
|
126
|
+
}
|
|
127
|
+
export async function detectSources() {
|
|
128
|
+
const home = homedir();
|
|
129
|
+
const candidates = [{ kind: "ssh", path: join(home, ".ssh", "config") }];
|
|
130
|
+
if (process.platform === "win32")
|
|
131
|
+
for (const base of [join(home, "Documents"), ...(process.env.OneDrive ? [join(process.env.OneDrive, "Documents")] : [])])
|
|
132
|
+
candidates.unshift({ kind: "xshell", path: join(base, "NetSarang Computer", "8", "Xshell", "Sessions") });
|
|
133
|
+
const found = await Promise.all(candidates.map(async (source) => { try {
|
|
134
|
+
const nodes = await scanSource(source);
|
|
135
|
+
return { ...source, count: nodes.length, groups: new Set(nodes.map((n) => n.group)).size };
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
return null;
|
|
139
|
+
} }));
|
|
140
|
+
return found.filter((s) => s !== null);
|
|
141
|
+
}
|
|
142
|
+
export function sourcePath(value) { return resolve(value.startsWith("~/") ? join(homedir(), value.slice(2)) : value); }
|