@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.
Files changed (456) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +74 -2
  3. package/bin/pi-harness.mjs +1819 -0
  4. package/deploy/com.youweichen.pi-harness.plist +48 -0
  5. package/deploy/nginx-subpath.conf +88 -0
  6. package/deploy/pi-harness-task.xml +71 -0
  7. package/deploy/pi-harness.service +31 -0
  8. package/dist/server/agent-service.js +3131 -0
  9. package/dist/server/app-version.js +19 -0
  10. package/dist/server/attachments.js +526 -0
  11. package/dist/server/auth-file.js +61 -0
  12. package/dist/server/bg-servers.js +154 -0
  13. package/dist/server/client-eviction.js +19 -0
  14. package/dist/server/client-id.js +9 -0
  15. package/dist/server/client-state.js +202 -0
  16. package/dist/server/codemode-image-routes.js +36 -0
  17. package/dist/server/codemode-presentation.js +19 -0
  18. package/dist/server/component-updates.js +135 -0
  19. package/dist/server/context-breakdown.js +56 -0
  20. package/dist/server/control-socket.js +178 -0
  21. package/dist/server/conversation.js +65 -0
  22. package/dist/server/dir-browser.js +77 -0
  23. package/dist/server/editor-snapshot.js +38 -0
  24. package/dist/server/ensure-bash.js +80 -0
  25. package/dist/server/extension-display.js +23 -0
  26. package/dist/server/extensions-catalog.js +64 -0
  27. package/dist/server/extensions-model.js +46 -0
  28. package/dist/server/extensions-routes.js +255 -0
  29. package/dist/server/extensions-worker.js +188 -0
  30. package/dist/server/files-service.js +787 -0
  31. package/dist/server/index.js +1033 -0
  32. package/dist/server/markdown-images.js +42 -0
  33. package/dist/server/model-admin.js +801 -0
  34. package/dist/server/model-config-merge.js +32 -0
  35. package/dist/server/native-mcp-config.js +103 -0
  36. package/dist/server/native-mcp-presentation.js +12 -0
  37. package/dist/server/native-prompt-template.js +12 -0
  38. package/dist/server/native-tools.js +32 -0
  39. package/dist/server/node-agent.js +262 -0
  40. package/dist/server/node-sources.js +142 -0
  41. package/dist/server/node-workbench.js +729 -0
  42. package/dist/server/origin-policy.js +50 -0
  43. package/dist/server/patch-node-pty.js +96 -0
  44. package/dist/server/pi-environment.js +203 -0
  45. package/dist/server/plan/extension.js +74 -0
  46. package/dist/server/plan/progress.js +90 -0
  47. package/dist/server/plan/settings.js +41 -0
  48. package/dist/server/plan/state.js +123 -0
  49. package/dist/server/plugin-facilities.js +304 -0
  50. package/dist/server/plugin-updater.js +245 -0
  51. package/dist/server/plugins.js +712 -0
  52. package/dist/server/private-file.js +28 -0
  53. package/dist/server/process-utils.js +112 -0
  54. package/dist/server/prompt-delivery.js +80 -0
  55. package/dist/server/protocol-version.js +11 -0
  56. package/dist/server/protocol.js +6 -0
  57. package/dist/server/provider-auth.js +99 -0
  58. package/dist/server/query-cache.js +45 -0
  59. package/dist/server/recovery-state.js +37 -0
  60. package/dist/server/scm.js +459 -0
  61. package/dist/server/security-headers.js +24 -0
  62. package/dist/server/serialize.js +254 -0
  63. package/dist/server/session-file-read.js +225 -0
  64. package/dist/server/session-record-worker.js +21 -0
  65. package/dist/server/session-reload.js +13 -0
  66. package/dist/server/session-tree-controller.js +250 -0
  67. package/dist/server/session-tree.js +191 -0
  68. package/dist/server/settings-service.js +53 -0
  69. package/dist/server/skills-service.js +87 -0
  70. package/dist/server/slash-commands.js +286 -0
  71. package/dist/server/sqlite-preview.js +58 -0
  72. package/dist/server/sqlite-query.js +67 -0
  73. package/dist/server/sqlite-worker.js +12 -0
  74. package/dist/server/system-prompt-files.js +158 -0
  75. package/dist/server/system-prompt-routes.js +40 -0
  76. package/dist/server/system-prompt-view.js +49 -0
  77. package/dist/server/task-progress.js +233 -0
  78. package/dist/server/terminals.js +833 -0
  79. package/dist/server/text-sniff.js +269 -0
  80. package/dist/server/thinking-timing.js +91 -0
  81. package/dist/server/token-auth.js +37 -0
  82. package/dist/server/tool-output.js +97 -0
  83. package/dist/server/tool-text-incidents.js +82 -0
  84. package/dist/server/tool-text.js +67 -0
  85. package/dist/server/uploads.js +112 -0
  86. package/dist/server/user-attachments.js +96 -0
  87. package/dist/server/webui-context.js +257 -0
  88. package/dist/server/wiki-links.js +136 -0
  89. package/dist/server/wiki-pdf-worker.js +23 -0
  90. package/dist/server/wiki-pdf.js +36 -0
  91. package/dist/server/wiki-routes.js +184 -0
  92. package/dist/server/wiki-service.js +524 -0
  93. package/electron/main.mjs +607 -0
  94. package/extensions/webui.ts +192 -0
  95. package/package.json +122 -4
  96. package/web/dist/apple-touch-icon.png +0 -0
  97. package/web/dist/assets/NodeWorkbench-BiP2ktQP.js +3 -0
  98. package/web/dist/assets/TerminalPanel-CKaSUsN-.js +2 -0
  99. package/web/dist/assets/abnfDiagram-VCTEODGH-CnwRJDRL.js +1 -0
  100. package/web/dist/assets/arc-CG4Q7-W_.js +1 -0
  101. package/web/dist/assets/architectureDiagram-5GKGNRK7-BrGqX_Rg.js +36 -0
  102. package/web/dist/assets/blockDiagram-I7D4REHJ-C2t0PjZT.js +129 -0
  103. package/web/dist/assets/c4Diagram-7LVT6UL2-CadrcgGL.js +38 -0
  104. package/web/dist/assets/channel-C27Jizb6.js +1 -0
  105. package/web/dist/assets/chunk-2Q5K7J3B-CC3V2ITZ.js +1 -0
  106. package/web/dist/assets/chunk-5VM5RSS4-DqKSbGmE.js +15 -0
  107. package/web/dist/assets/chunk-F27PBJKO-_CDr3JbY.js +1 -0
  108. package/web/dist/assets/chunk-IMKFNOWR-BIR4H4fX.js +231 -0
  109. package/web/dist/assets/chunk-JWPE2WC7-CrsdBGXT.js +1 -0
  110. package/web/dist/assets/chunk-POPQ4Y6H-C6q9kQmf.js +1 -0
  111. package/web/dist/assets/chunk-SVP7TREG-l2XXTr2v.js +88 -0
  112. package/web/dist/assets/chunk-TICWLB2K-29I_mZgB.js +206 -0
  113. package/web/dist/assets/chunk-XXDRQBXY-htV9FBqi.js +1 -0
  114. package/web/dist/assets/classDiagram-ZZMXUADV-DYTBrAEP.js +1 -0
  115. package/web/dist/assets/classDiagram-v2-VYDZK3BY-DYTBrAEP.js +1 -0
  116. package/web/dist/assets/cose-bilkent-JH36ORCC-Cop-ihyx.js +1 -0
  117. package/web/dist/assets/cynefin-OW5HDTMX-IEk8qBRq.js +178 -0
  118. package/web/dist/assets/cynefinDiagram-5FMLGOSQ-Bmzt6Qb5.js +62 -0
  119. package/web/dist/assets/cytoscape.esm-Bch-eiPH.js +331 -0
  120. package/web/dist/assets/dagre-GXQ25YYZ-DiXTJYYU.js +4 -0
  121. package/web/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
  122. package/web/dist/assets/diagram-S7CK7UJ4-BT7CLuNk.js +30 -0
  123. package/web/dist/assets/diagram-UQ7AKVKN-BZZ3nTCO.js +41 -0
  124. package/web/dist/assets/diagram-VSXAHHWV-Dal8LeZh.js +3 -0
  125. package/web/dist/assets/diagram-VX7I27RA-D6V9FfNA.js +24 -0
  126. package/web/dist/assets/diagram-Z3DM3KII-KUAIcFiD.js +24 -0
  127. package/web/dist/assets/ebnfDiagram-PWID7BFC-s0FEPZrV.js +1 -0
  128. package/web/dist/assets/erDiagram-RLTQ6QDP-CiIQPFCD.js +99 -0
  129. package/web/dist/assets/flowDiagram-HODETNUW-CvG4MYWn.js +168 -0
  130. package/web/dist/assets/ganttDiagram-EL5Y4UJY-CUf115ii.js +292 -0
  131. package/web/dist/assets/gitGraphDiagram-WWUBYQGX-o3K1r6fL.js +106 -0
  132. package/web/dist/assets/index-C3vDFSEV.css +10 -0
  133. package/web/dist/assets/index-C5or_GAq.js +276 -0
  134. package/web/dist/assets/infoDiagram-27XIBGKW-BxiBeQ8E.js +2 -0
  135. package/web/dist/assets/init-Gi6I4Gst.js +1 -0
  136. package/web/dist/assets/ishikawaDiagram-5VMMS53U-B1ONuCkH.js +70 -0
  137. package/web/dist/assets/journeyDiagram-3NMN7TZE-DwmuoXcX.js +139 -0
  138. package/web/dist/assets/kanban-definition-UXKFOSKX-pMKHubxH.js +89 -0
  139. package/web/dist/assets/katex-C5jXJg4s.js +257 -0
  140. package/web/dist/assets/layout-CEwfjQR0.js +1 -0
  141. package/web/dist/assets/linear-D22Gdbt1.js +1 -0
  142. package/web/dist/assets/markdown-BhJ_LInD.js +51 -0
  143. package/web/dist/assets/mermaid.core-CO2nrD04.js +312 -0
  144. package/web/dist/assets/mindmap-definition-YA3MSWOX-9MQn9ZAp.js +96 -0
  145. package/web/dist/assets/ordinal-Cboi1Yqb.js +1 -0
  146. package/web/dist/assets/pegDiagram-XKGWAZYB-Dy6fLjM-.js +1 -0
  147. package/web/dist/assets/pieDiagram-E7YTZNPT-EDLez6nA.js +39 -0
  148. package/web/dist/assets/quadrantDiagram-AXDQQJYC-Cgcd9mvy.js +7 -0
  149. package/web/dist/assets/railroadDiagram-O6MQD6OU-B4Td0tjj.js +1 -0
  150. package/web/dist/assets/react-BddUxV-9.js +24 -0
  151. package/web/dist/assets/requirementDiagram-BXWQKSXE-aF2QNZ78.js +84 -0
  152. package/web/dist/assets/sankeyDiagram-P5KCCOFB-DgTYwYMa.js +40 -0
  153. package/web/dist/assets/sequenceDiagram-WJ2MYXX4-EUcddWUc.js +162 -0
  154. package/web/dist/assets/sizeCapture-INFHLROL-CXHOv9Sx.js +1 -0
  155. package/web/dist/assets/stateDiagram-D77RDMKH-B-xlgfaK.js +1 -0
  156. package/web/dist/assets/stateDiagram-v2-MP3YSRHH-kwAwAlWX.js +1 -0
  157. package/web/dist/assets/swimlanes-42K2YHIH-BtFBOCDh.js +1 -0
  158. package/web/dist/assets/swimlanesDiagram-VR7AAH4N-D5j5Cj3S.js +8 -0
  159. package/web/dist/assets/timeline-definition-24CTP7MA-CZlQY6pb.js +120 -0
  160. package/web/dist/assets/vennDiagram-4TSXK5OY-Cn61dJvL.js +34 -0
  161. package/web/dist/assets/wardleyDiagram-VM6X3IG4-fiMNrWpr.js +78 -0
  162. package/web/dist/assets/xterm-6GBZ9nXN.css +32 -0
  163. package/web/dist/assets/xterm-D1D2FVe3.js +38 -0
  164. package/web/dist/assets/xychartDiagram-S5SC5T6Z-CuzM_sof.js +7 -0
  165. package/web/dist/brand-mark.svg +5 -0
  166. package/web/dist/favicon-32.png +0 -0
  167. package/web/dist/favicon.svg +5 -0
  168. package/web/dist/fonts/WIKI-FONTS.md +5 -0
  169. package/web/dist/fonts/ibm-plex-mono/ibm-plex-mono-latin-400-normal.woff2 +0 -0
  170. package/web/dist/fonts/ibm-plex-mono/ibm-plex-mono-latin-500-normal.woff2 +0 -0
  171. package/web/dist/fonts/ibm-plex-mono/ibm-plex-mono-latin-600-normal.woff2 +0 -0
  172. package/web/dist/fonts/ibm-plex-mono/ibm-plex-mono-latin-ext-400-normal.woff2 +0 -0
  173. package/web/dist/fonts/ibm-plex-mono/ibm-plex-mono-latin-ext-500-normal.woff2 +0 -0
  174. package/web/dist/fonts/ibm-plex-mono/ibm-plex-mono-latin-ext-600-normal.woff2 +0 -0
  175. package/web/dist/fonts/ibm-plex-sans/LICENSE +93 -0
  176. package/web/dist/fonts/ibm-plex-sans/ibm-plex-sans-latin-400-normal.woff2 +0 -0
  177. package/web/dist/fonts/ibm-plex-sans/ibm-plex-sans-latin-500-normal.woff2 +0 -0
  178. package/web/dist/fonts/ibm-plex-sans/ibm-plex-sans-latin-600-normal.woff2 +0 -0
  179. package/web/dist/fonts/ibm-plex-sans/ibm-plex-sans-latin-ext-400-normal.woff2 +0 -0
  180. package/web/dist/fonts/ibm-plex-sans/ibm-plex-sans-latin-ext-500-normal.woff2 +0 -0
  181. package/web/dist/fonts/ibm-plex-sans/ibm-plex-sans-latin-ext-600-normal.woff2 +0 -0
  182. package/web/dist/fonts/inter/inter-latin-400-normal.woff2 +0 -0
  183. package/web/dist/fonts/inter/inter-latin-500-normal.woff2 +0 -0
  184. package/web/dist/fonts/inter/inter-latin-600-normal.woff2 +0 -0
  185. package/web/dist/fonts/inter/inter-latin-700-normal.woff2 +0 -0
  186. package/web/dist/fonts/inter/inter-latin-800-normal.woff2 +0 -0
  187. package/web/dist/fonts/inter/inter-latin-ext-400-normal.woff2 +0 -0
  188. package/web/dist/fonts/inter/inter-latin-ext-500-normal.woff2 +0 -0
  189. package/web/dist/fonts/inter/inter-latin-ext-600-normal.woff2 +0 -0
  190. package/web/dist/fonts/inter/inter-latin-ext-700-normal.woff2 +0 -0
  191. package/web/dist/fonts/inter/inter-latin-ext-800-normal.woff2 +0 -0
  192. package/web/dist/fonts/jetbrains-mono/LICENSE +93 -0
  193. package/web/dist/fonts/jetbrains-mono/jetbrains-mono-latin-400-normal.woff2 +0 -0
  194. package/web/dist/fonts/jetbrains-mono/jetbrains-mono-latin-500-normal.woff2 +0 -0
  195. package/web/dist/fonts/jetbrains-mono/jetbrains-mono-latin-600-normal.woff2 +0 -0
  196. package/web/dist/fonts/jetbrains-mono/jetbrains-mono-latin-ext-400-normal.woff2 +0 -0
  197. package/web/dist/fonts/jetbrains-mono/jetbrains-mono-latin-ext-500-normal.woff2 +0 -0
  198. package/web/dist/fonts/jetbrains-mono/jetbrains-mono-latin-ext-600-normal.woff2 +0 -0
  199. package/web/dist/fonts/noto-serif-sc/LICENSE +94 -0
  200. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-0.woff2 +0 -0
  201. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-1.woff2 +0 -0
  202. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-10.woff2 +0 -0
  203. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-100.woff2 +0 -0
  204. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-11.woff2 +0 -0
  205. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-12.woff2 +0 -0
  206. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-13.woff2 +0 -0
  207. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-14.woff2 +0 -0
  208. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-15.woff2 +0 -0
  209. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-16.woff2 +0 -0
  210. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-17.woff2 +0 -0
  211. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-18.woff2 +0 -0
  212. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-19.woff2 +0 -0
  213. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-2.woff2 +0 -0
  214. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-20.woff2 +0 -0
  215. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-21.woff2 +0 -0
  216. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-22.woff2 +0 -0
  217. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-23.woff2 +0 -0
  218. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-24.woff2 +0 -0
  219. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-25.woff2 +0 -0
  220. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-26.woff2 +0 -0
  221. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-27.woff2 +0 -0
  222. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-28.woff2 +0 -0
  223. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-29.woff2 +0 -0
  224. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-3.woff2 +0 -0
  225. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-30.woff2 +0 -0
  226. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-31.woff2 +0 -0
  227. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-32.woff2 +0 -0
  228. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-33.woff2 +0 -0
  229. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-34.woff2 +0 -0
  230. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-35.woff2 +0 -0
  231. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-36.woff2 +0 -0
  232. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-37.woff2 +0 -0
  233. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-38.woff2 +0 -0
  234. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-39.woff2 +0 -0
  235. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-4.woff2 +0 -0
  236. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-40.woff2 +0 -0
  237. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-41.woff2 +0 -0
  238. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-42.woff2 +0 -0
  239. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-43.woff2 +0 -0
  240. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-44.woff2 +0 -0
  241. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-45.woff2 +0 -0
  242. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-46.woff2 +0 -0
  243. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-47.woff2 +0 -0
  244. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-48.woff2 +0 -0
  245. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-49.woff2 +0 -0
  246. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-5.woff2 +0 -0
  247. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-50.woff2 +0 -0
  248. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-51.woff2 +0 -0
  249. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-52.woff2 +0 -0
  250. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-53.woff2 +0 -0
  251. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-54.woff2 +0 -0
  252. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-55.woff2 +0 -0
  253. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-56.woff2 +0 -0
  254. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-57.woff2 +0 -0
  255. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-58.woff2 +0 -0
  256. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-59.woff2 +0 -0
  257. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-6.woff2 +0 -0
  258. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-60.woff2 +0 -0
  259. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-61.woff2 +0 -0
  260. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-62.woff2 +0 -0
  261. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-63.woff2 +0 -0
  262. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-64.woff2 +0 -0
  263. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-65.woff2 +0 -0
  264. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-66.woff2 +0 -0
  265. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-67.woff2 +0 -0
  266. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-68.woff2 +0 -0
  267. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-69.woff2 +0 -0
  268. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-7.woff2 +0 -0
  269. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-70.woff2 +0 -0
  270. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-71.woff2 +0 -0
  271. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-72.woff2 +0 -0
  272. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-73.woff2 +0 -0
  273. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-74.woff2 +0 -0
  274. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-75.woff2 +0 -0
  275. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-76.woff2 +0 -0
  276. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-77.woff2 +0 -0
  277. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-78.woff2 +0 -0
  278. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-79.woff2 +0 -0
  279. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-8.woff2 +0 -0
  280. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-80.woff2 +0 -0
  281. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-81.woff2 +0 -0
  282. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-82.woff2 +0 -0
  283. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-83.woff2 +0 -0
  284. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-84.woff2 +0 -0
  285. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-85.woff2 +0 -0
  286. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-86.woff2 +0 -0
  287. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-87.woff2 +0 -0
  288. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-88.woff2 +0 -0
  289. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-89.woff2 +0 -0
  290. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-9.woff2 +0 -0
  291. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-90.woff2 +0 -0
  292. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-91.woff2 +0 -0
  293. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-92.woff2 +0 -0
  294. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-93.woff2 +0 -0
  295. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-94.woff2 +0 -0
  296. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-95.woff2 +0 -0
  297. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-96.woff2 +0 -0
  298. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-97.woff2 +0 -0
  299. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-98.woff2 +0 -0
  300. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-99.woff2 +0 -0
  301. package/web/dist/fonts/source-serif-4/LICENSE +93 -0
  302. package/web/dist/fonts/source-serif-4/source-serif-4-500-0.woff2 +0 -0
  303. package/web/dist/fonts/source-serif-4/source-serif-4-500-1.woff2 +0 -0
  304. package/web/dist/fonts/source-serif-4/source-serif-4-500-2.woff2 +0 -0
  305. package/web/dist/fonts/source-serif-4/source-serif-4-500-3.woff2 +0 -0
  306. package/web/dist/fonts/source-serif-4/source-serif-4-500-4.woff2 +0 -0
  307. package/web/dist/fonts/source-serif-4/source-serif-4-500-5.woff2 +0 -0
  308. package/web/dist/icon/1a-mark.svg +1 -0
  309. package/web/dist/icon.ico +0 -0
  310. package/web/dist/index.html +18 -0
  311. package/web/public/apple-touch-icon.png +0 -0
  312. package/web/public/brand-mark.svg +5 -0
  313. package/web/public/favicon-32.png +0 -0
  314. package/web/public/favicon.svg +5 -0
  315. package/web/public/fonts/WIKI-FONTS.md +5 -0
  316. package/web/public/fonts/ibm-plex-mono/ibm-plex-mono-latin-400-normal.woff2 +0 -0
  317. package/web/public/fonts/ibm-plex-mono/ibm-plex-mono-latin-500-normal.woff2 +0 -0
  318. package/web/public/fonts/ibm-plex-mono/ibm-plex-mono-latin-600-normal.woff2 +0 -0
  319. package/web/public/fonts/ibm-plex-mono/ibm-plex-mono-latin-ext-400-normal.woff2 +0 -0
  320. package/web/public/fonts/ibm-plex-mono/ibm-plex-mono-latin-ext-500-normal.woff2 +0 -0
  321. package/web/public/fonts/ibm-plex-mono/ibm-plex-mono-latin-ext-600-normal.woff2 +0 -0
  322. package/web/public/fonts/ibm-plex-sans/LICENSE +93 -0
  323. package/web/public/fonts/ibm-plex-sans/ibm-plex-sans-latin-400-normal.woff2 +0 -0
  324. package/web/public/fonts/ibm-plex-sans/ibm-plex-sans-latin-500-normal.woff2 +0 -0
  325. package/web/public/fonts/ibm-plex-sans/ibm-plex-sans-latin-600-normal.woff2 +0 -0
  326. package/web/public/fonts/ibm-plex-sans/ibm-plex-sans-latin-ext-400-normal.woff2 +0 -0
  327. package/web/public/fonts/ibm-plex-sans/ibm-plex-sans-latin-ext-500-normal.woff2 +0 -0
  328. package/web/public/fonts/ibm-plex-sans/ibm-plex-sans-latin-ext-600-normal.woff2 +0 -0
  329. package/web/public/fonts/inter/inter-latin-400-normal.woff2 +0 -0
  330. package/web/public/fonts/inter/inter-latin-500-normal.woff2 +0 -0
  331. package/web/public/fonts/inter/inter-latin-600-normal.woff2 +0 -0
  332. package/web/public/fonts/inter/inter-latin-700-normal.woff2 +0 -0
  333. package/web/public/fonts/inter/inter-latin-800-normal.woff2 +0 -0
  334. package/web/public/fonts/inter/inter-latin-ext-400-normal.woff2 +0 -0
  335. package/web/public/fonts/inter/inter-latin-ext-500-normal.woff2 +0 -0
  336. package/web/public/fonts/inter/inter-latin-ext-600-normal.woff2 +0 -0
  337. package/web/public/fonts/inter/inter-latin-ext-700-normal.woff2 +0 -0
  338. package/web/public/fonts/inter/inter-latin-ext-800-normal.woff2 +0 -0
  339. package/web/public/fonts/jetbrains-mono/LICENSE +93 -0
  340. package/web/public/fonts/jetbrains-mono/jetbrains-mono-latin-400-normal.woff2 +0 -0
  341. package/web/public/fonts/jetbrains-mono/jetbrains-mono-latin-500-normal.woff2 +0 -0
  342. package/web/public/fonts/jetbrains-mono/jetbrains-mono-latin-600-normal.woff2 +0 -0
  343. package/web/public/fonts/jetbrains-mono/jetbrains-mono-latin-ext-400-normal.woff2 +0 -0
  344. package/web/public/fonts/jetbrains-mono/jetbrains-mono-latin-ext-500-normal.woff2 +0 -0
  345. package/web/public/fonts/jetbrains-mono/jetbrains-mono-latin-ext-600-normal.woff2 +0 -0
  346. package/web/public/fonts/noto-serif-sc/LICENSE +94 -0
  347. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-0.woff2 +0 -0
  348. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-1.woff2 +0 -0
  349. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-10.woff2 +0 -0
  350. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-100.woff2 +0 -0
  351. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-11.woff2 +0 -0
  352. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-12.woff2 +0 -0
  353. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-13.woff2 +0 -0
  354. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-14.woff2 +0 -0
  355. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-15.woff2 +0 -0
  356. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-16.woff2 +0 -0
  357. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-17.woff2 +0 -0
  358. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-18.woff2 +0 -0
  359. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-19.woff2 +0 -0
  360. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-2.woff2 +0 -0
  361. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-20.woff2 +0 -0
  362. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-21.woff2 +0 -0
  363. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-22.woff2 +0 -0
  364. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-23.woff2 +0 -0
  365. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-24.woff2 +0 -0
  366. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-25.woff2 +0 -0
  367. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-26.woff2 +0 -0
  368. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-27.woff2 +0 -0
  369. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-28.woff2 +0 -0
  370. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-29.woff2 +0 -0
  371. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-3.woff2 +0 -0
  372. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-30.woff2 +0 -0
  373. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-31.woff2 +0 -0
  374. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-32.woff2 +0 -0
  375. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-33.woff2 +0 -0
  376. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-34.woff2 +0 -0
  377. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-35.woff2 +0 -0
  378. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-36.woff2 +0 -0
  379. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-37.woff2 +0 -0
  380. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-38.woff2 +0 -0
  381. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-39.woff2 +0 -0
  382. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-4.woff2 +0 -0
  383. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-40.woff2 +0 -0
  384. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-41.woff2 +0 -0
  385. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-42.woff2 +0 -0
  386. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-43.woff2 +0 -0
  387. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-44.woff2 +0 -0
  388. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-45.woff2 +0 -0
  389. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-46.woff2 +0 -0
  390. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-47.woff2 +0 -0
  391. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-48.woff2 +0 -0
  392. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-49.woff2 +0 -0
  393. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-5.woff2 +0 -0
  394. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-50.woff2 +0 -0
  395. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-51.woff2 +0 -0
  396. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-52.woff2 +0 -0
  397. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-53.woff2 +0 -0
  398. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-54.woff2 +0 -0
  399. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-55.woff2 +0 -0
  400. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-56.woff2 +0 -0
  401. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-57.woff2 +0 -0
  402. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-58.woff2 +0 -0
  403. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-59.woff2 +0 -0
  404. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-6.woff2 +0 -0
  405. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-60.woff2 +0 -0
  406. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-61.woff2 +0 -0
  407. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-62.woff2 +0 -0
  408. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-63.woff2 +0 -0
  409. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-64.woff2 +0 -0
  410. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-65.woff2 +0 -0
  411. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-66.woff2 +0 -0
  412. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-67.woff2 +0 -0
  413. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-68.woff2 +0 -0
  414. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-69.woff2 +0 -0
  415. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-7.woff2 +0 -0
  416. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-70.woff2 +0 -0
  417. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-71.woff2 +0 -0
  418. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-72.woff2 +0 -0
  419. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-73.woff2 +0 -0
  420. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-74.woff2 +0 -0
  421. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-75.woff2 +0 -0
  422. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-76.woff2 +0 -0
  423. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-77.woff2 +0 -0
  424. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-78.woff2 +0 -0
  425. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-79.woff2 +0 -0
  426. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-8.woff2 +0 -0
  427. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-80.woff2 +0 -0
  428. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-81.woff2 +0 -0
  429. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-82.woff2 +0 -0
  430. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-83.woff2 +0 -0
  431. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-84.woff2 +0 -0
  432. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-85.woff2 +0 -0
  433. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-86.woff2 +0 -0
  434. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-87.woff2 +0 -0
  435. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-88.woff2 +0 -0
  436. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-89.woff2 +0 -0
  437. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-9.woff2 +0 -0
  438. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-90.woff2 +0 -0
  439. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-91.woff2 +0 -0
  440. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-92.woff2 +0 -0
  441. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-93.woff2 +0 -0
  442. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-94.woff2 +0 -0
  443. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-95.woff2 +0 -0
  444. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-96.woff2 +0 -0
  445. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-97.woff2 +0 -0
  446. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-98.woff2 +0 -0
  447. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-99.woff2 +0 -0
  448. package/web/public/fonts/source-serif-4/LICENSE +93 -0
  449. package/web/public/fonts/source-serif-4/source-serif-4-500-0.woff2 +0 -0
  450. package/web/public/fonts/source-serif-4/source-serif-4-500-1.woff2 +0 -0
  451. package/web/public/fonts/source-serif-4/source-serif-4-500-2.woff2 +0 -0
  452. package/web/public/fonts/source-serif-4/source-serif-4-500-3.woff2 +0 -0
  453. package/web/public/fonts/source-serif-4/source-serif-4-500-4.woff2 +0 -0
  454. package/web/public/fonts/source-serif-4/source-serif-4-500-5.woff2 +0 -0
  455. package/web/public/icon/1a-mark.svg +1 -0
  456. 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); }