@rexvane/piweb 0.3.1

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 (535) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +128 -0
  3. package/assets/showcase.png +0 -0
  4. package/bin/pi.js +50 -0
  5. package/bin/piweb.js +12 -0
  6. package/next.config.ts +21 -0
  7. package/package.json +88 -0
  8. package/postcss.config.mjs +7 -0
  9. package/public/file-icons/3d.svg +1 -0
  10. package/public/file-icons/LICENSE +8 -0
  11. package/public/file-icons/adobe-illustrator.svg +1 -0
  12. package/public/file-icons/adobe-photoshop.svg +1 -0
  13. package/public/file-icons/agent.svg +1 -0
  14. package/public/file-icons/asciidoc.svg +1 -0
  15. package/public/file-icons/assembly.svg +1 -0
  16. package/public/file-icons/astro.svg +1 -0
  17. package/public/file-icons/audio.svg +1 -0
  18. package/public/file-icons/authors.svg +1 -0
  19. package/public/file-icons/babel.svg +1 -0
  20. package/public/file-icons/bibliography.svg +1 -0
  21. package/public/file-icons/bicep.svg +1 -0
  22. package/public/file-icons/blender.svg +1 -0
  23. package/public/file-icons/bun.svg +1 -0
  24. package/public/file-icons/c.svg +1 -0
  25. package/public/file-icons/certificate.svg +1 -0
  26. package/public/file-icons/changelog.svg +1 -0
  27. package/public/file-icons/clojure.svg +1 -0
  28. package/public/file-icons/cmake.svg +1 -0
  29. package/public/file-icons/codeowners.svg +1 -0
  30. package/public/file-icons/commitlint.svg +1 -0
  31. package/public/file-icons/conduct.svg +1 -0
  32. package/public/file-icons/console.svg +1 -0
  33. package/public/file-icons/contributing.svg +1 -0
  34. package/public/file-icons/cpp.svg +1 -0
  35. package/public/file-icons/csharp.svg +1 -0
  36. package/public/file-icons/css.svg +1 -0
  37. package/public/file-icons/cursor.svg +1 -0
  38. package/public/file-icons/dart.svg +1 -0
  39. package/public/file-icons/database.svg +1 -0
  40. package/public/file-icons/deno.svg +1 -0
  41. package/public/file-icons/diff.svg +1 -0
  42. package/public/file-icons/docker.svg +1 -0
  43. package/public/file-icons/document.svg +1 -0
  44. package/public/file-icons/editorconfig.svg +1 -0
  45. package/public/file-icons/ejs.svg +1 -0
  46. package/public/file-icons/elixir.svg +1 -0
  47. package/public/file-icons/elm.svg +1 -0
  48. package/public/file-icons/email.svg +1 -0
  49. package/public/file-icons/erlang.svg +1 -0
  50. package/public/file-icons/eslint.svg +1 -0
  51. package/public/file-icons/favicon.svg +1 -0
  52. package/public/file-icons/file.svg +1 -0
  53. package/public/file-icons/folder-admin-open.svg +1 -0
  54. package/public/file-icons/folder-admin.svg +1 -0
  55. package/public/file-icons/folder-android-open.svg +1 -0
  56. package/public/file-icons/folder-android.svg +1 -0
  57. package/public/file-icons/folder-api-open.svg +1 -0
  58. package/public/file-icons/folder-api.svg +1 -0
  59. package/public/file-icons/folder-app-open.svg +1 -0
  60. package/public/file-icons/folder-app.svg +1 -0
  61. package/public/file-icons/folder-archive-open.svg +1 -0
  62. package/public/file-icons/folder-archive.svg +1 -0
  63. package/public/file-icons/folder-atom-open.svg +1 -0
  64. package/public/file-icons/folder-atom.svg +1 -0
  65. package/public/file-icons/folder-audio-open.svg +1 -0
  66. package/public/file-icons/folder-audio.svg +1 -0
  67. package/public/file-icons/folder-aws-open.svg +1 -0
  68. package/public/file-icons/folder-aws.svg +1 -0
  69. package/public/file-icons/folder-backup-open.svg +1 -0
  70. package/public/file-icons/folder-backup.svg +1 -0
  71. package/public/file-icons/folder-benchmark-open.svg +1 -0
  72. package/public/file-icons/folder-benchmark.svg +1 -0
  73. package/public/file-icons/folder-cart-open.svg +1 -0
  74. package/public/file-icons/folder-cart.svg +1 -0
  75. package/public/file-icons/folder-ci-open.svg +1 -0
  76. package/public/file-icons/folder-ci.svg +1 -0
  77. package/public/file-icons/folder-circleci-open.svg +1 -0
  78. package/public/file-icons/folder-circleci.svg +1 -0
  79. package/public/file-icons/folder-class-open.svg +1 -0
  80. package/public/file-icons/folder-class.svg +1 -0
  81. package/public/file-icons/folder-client-open.svg +1 -0
  82. package/public/file-icons/folder-client.svg +1 -0
  83. package/public/file-icons/folder-command-open.svg +1 -0
  84. package/public/file-icons/folder-command.svg +1 -0
  85. package/public/file-icons/folder-components-open.svg +1 -0
  86. package/public/file-icons/folder-components.svg +1 -0
  87. package/public/file-icons/folder-config-open.svg +1 -0
  88. package/public/file-icons/folder-config.svg +1 -0
  89. package/public/file-icons/folder-console-open.svg +1 -0
  90. package/public/file-icons/folder-console.svg +1 -0
  91. package/public/file-icons/folder-content-open.svg +1 -0
  92. package/public/file-icons/folder-content.svg +1 -0
  93. package/public/file-icons/folder-context-open.svg +1 -0
  94. package/public/file-icons/folder-context.svg +1 -0
  95. package/public/file-icons/folder-contract-open.svg +1 -0
  96. package/public/file-icons/folder-contract.svg +1 -0
  97. package/public/file-icons/folder-controller-open.svg +1 -0
  98. package/public/file-icons/folder-controller.svg +1 -0
  99. package/public/file-icons/folder-core-open.svg +1 -0
  100. package/public/file-icons/folder-core.svg +1 -0
  101. package/public/file-icons/folder-coverage-open.svg +1 -0
  102. package/public/file-icons/folder-coverage.svg +1 -0
  103. package/public/file-icons/folder-css-open.svg +1 -0
  104. package/public/file-icons/folder-css.svg +1 -0
  105. package/public/file-icons/folder-database-open.svg +1 -0
  106. package/public/file-icons/folder-database.svg +1 -0
  107. package/public/file-icons/folder-debug-open.svg +1 -0
  108. package/public/file-icons/folder-debug.svg +1 -0
  109. package/public/file-icons/folder-deprecated-open.svg +1 -0
  110. package/public/file-icons/folder-deprecated.svg +1 -0
  111. package/public/file-icons/folder-desktop-open.svg +1 -0
  112. package/public/file-icons/folder-desktop.svg +1 -0
  113. package/public/file-icons/folder-dist-open.svg +1 -0
  114. package/public/file-icons/folder-dist.svg +1 -0
  115. package/public/file-icons/folder-docker-open.svg +1 -0
  116. package/public/file-icons/folder-docker.svg +1 -0
  117. package/public/file-icons/folder-docs-open.svg +1 -0
  118. package/public/file-icons/folder-docs.svg +1 -0
  119. package/public/file-icons/folder-element-open.svg +1 -0
  120. package/public/file-icons/folder-element.svg +1 -0
  121. package/public/file-icons/folder-environment-open.svg +1 -0
  122. package/public/file-icons/folder-environment.svg +1 -0
  123. package/public/file-icons/folder-event-open.svg +1 -0
  124. package/public/file-icons/folder-event.svg +1 -0
  125. package/public/file-icons/folder-examples-open.svg +1 -0
  126. package/public/file-icons/folder-examples.svg +1 -0
  127. package/public/file-icons/folder-features-open.svg +1 -0
  128. package/public/file-icons/folder-features.svg +1 -0
  129. package/public/file-icons/folder-font-open.svg +1 -0
  130. package/public/file-icons/folder-font.svg +1 -0
  131. package/public/file-icons/folder-functions-open.svg +1 -0
  132. package/public/file-icons/folder-functions.svg +1 -0
  133. package/public/file-icons/folder-generator-open.svg +1 -0
  134. package/public/file-icons/folder-generator.svg +1 -0
  135. package/public/file-icons/folder-git-open.svg +1 -0
  136. package/public/file-icons/folder-git.svg +1 -0
  137. package/public/file-icons/folder-github-open.svg +1 -0
  138. package/public/file-icons/folder-github.svg +1 -0
  139. package/public/file-icons/folder-gitlab-open.svg +1 -0
  140. package/public/file-icons/folder-gitlab.svg +1 -0
  141. package/public/file-icons/folder-graphql-open.svg +1 -0
  142. package/public/file-icons/folder-graphql.svg +1 -0
  143. package/public/file-icons/folder-helper-open.svg +1 -0
  144. package/public/file-icons/folder-helper.svg +1 -0
  145. package/public/file-icons/folder-hook-open.svg +1 -0
  146. package/public/file-icons/folder-hook.svg +1 -0
  147. package/public/file-icons/folder-husky-open.svg +1 -0
  148. package/public/file-icons/folder-husky.svg +1 -0
  149. package/public/file-icons/folder-i18n-open.svg +1 -0
  150. package/public/file-icons/folder-i18n.svg +1 -0
  151. package/public/file-icons/folder-images-open.svg +1 -0
  152. package/public/file-icons/folder-images.svg +1 -0
  153. package/public/file-icons/folder-include-open.svg +1 -0
  154. package/public/file-icons/folder-include.svg +1 -0
  155. package/public/file-icons/folder-intellij-open.svg +1 -0
  156. package/public/file-icons/folder-intellij.svg +1 -0
  157. package/public/file-icons/folder-interface-open.svg +1 -0
  158. package/public/file-icons/folder-interface.svg +1 -0
  159. package/public/file-icons/folder-ios-open.svg +1 -0
  160. package/public/file-icons/folder-ios.svg +1 -0
  161. package/public/file-icons/folder-java-open.svg +1 -0
  162. package/public/file-icons/folder-java.svg +1 -0
  163. package/public/file-icons/folder-job-open.svg +1 -0
  164. package/public/file-icons/folder-job.svg +1 -0
  165. package/public/file-icons/folder-jupyter-open.svg +1 -0
  166. package/public/file-icons/folder-jupyter.svg +1 -0
  167. package/public/file-icons/folder-keys-open.svg +1 -0
  168. package/public/file-icons/folder-keys.svg +1 -0
  169. package/public/file-icons/folder-kubernetes-open.svg +1 -0
  170. package/public/file-icons/folder-kubernetes.svg +1 -0
  171. package/public/file-icons/folder-layout-open.svg +1 -0
  172. package/public/file-icons/folder-layout.svg +1 -0
  173. package/public/file-icons/folder-less-open.svg +1 -0
  174. package/public/file-icons/folder-less.svg +1 -0
  175. package/public/file-icons/folder-lib-open.svg +1 -0
  176. package/public/file-icons/folder-lib.svg +1 -0
  177. package/public/file-icons/folder-log-open.svg +1 -0
  178. package/public/file-icons/folder-log.svg +1 -0
  179. package/public/file-icons/folder-messages-open.svg +1 -0
  180. package/public/file-icons/folder-messages.svg +1 -0
  181. package/public/file-icons/folder-meta-open.svg +1 -0
  182. package/public/file-icons/folder-meta.svg +1 -0
  183. package/public/file-icons/folder-middleware-open.svg +1 -0
  184. package/public/file-icons/folder-middleware.svg +1 -0
  185. package/public/file-icons/folder-migrations-open.svg +1 -0
  186. package/public/file-icons/folder-migrations.svg +1 -0
  187. package/public/file-icons/folder-mobile-open.svg +1 -0
  188. package/public/file-icons/folder-mobile.svg +1 -0
  189. package/public/file-icons/folder-mock-open.svg +1 -0
  190. package/public/file-icons/folder-mock.svg +1 -0
  191. package/public/file-icons/folder-molecule-open.svg +1 -0
  192. package/public/file-icons/folder-molecule.svg +1 -0
  193. package/public/file-icons/folder-next-open.svg +1 -0
  194. package/public/file-icons/folder-next.svg +1 -0
  195. package/public/file-icons/folder-node-open.svg +1 -0
  196. package/public/file-icons/folder-node.svg +1 -0
  197. package/public/file-icons/folder-nuxt-open.svg +1 -0
  198. package/public/file-icons/folder-nuxt.svg +1 -0
  199. package/public/file-icons/folder-open.svg +1 -0
  200. package/public/file-icons/folder-organism-open.svg +1 -0
  201. package/public/file-icons/folder-organism.svg +1 -0
  202. package/public/file-icons/folder-packages-open.svg +1 -0
  203. package/public/file-icons/folder-packages.svg +1 -0
  204. package/public/file-icons/folder-plugin-open.svg +1 -0
  205. package/public/file-icons/folder-plugin.svg +1 -0
  206. package/public/file-icons/folder-prisma-open.svg +1 -0
  207. package/public/file-icons/folder-prisma.svg +1 -0
  208. package/public/file-icons/folder-prompts-open.svg +1 -0
  209. package/public/file-icons/folder-prompts.svg +1 -0
  210. package/public/file-icons/folder-proto-open.svg +1 -0
  211. package/public/file-icons/folder-proto.svg +1 -0
  212. package/public/file-icons/folder-public-open.svg +1 -0
  213. package/public/file-icons/folder-public.svg +1 -0
  214. package/public/file-icons/folder-python-open.svg +1 -0
  215. package/public/file-icons/folder-python.svg +1 -0
  216. package/public/file-icons/folder-queue-open.svg +1 -0
  217. package/public/file-icons/folder-queue.svg +1 -0
  218. package/public/file-icons/folder-resource-open.svg +1 -0
  219. package/public/file-icons/folder-resource.svg +1 -0
  220. package/public/file-icons/folder-review-open.svg +1 -0
  221. package/public/file-icons/folder-review.svg +1 -0
  222. package/public/file-icons/folder-robot-open.svg +1 -0
  223. package/public/file-icons/folder-robot.svg +1 -0
  224. package/public/file-icons/folder-routes-open.svg +1 -0
  225. package/public/file-icons/folder-routes.svg +1 -0
  226. package/public/file-icons/folder-rust-open.svg +1 -0
  227. package/public/file-icons/folder-rust.svg +1 -0
  228. package/public/file-icons/folder-sass-open.svg +1 -0
  229. package/public/file-icons/folder-sass.svg +1 -0
  230. package/public/file-icons/folder-scripts-open.svg +1 -0
  231. package/public/file-icons/folder-scripts.svg +1 -0
  232. package/public/file-icons/folder-secure-open.svg +1 -0
  233. package/public/file-icons/folder-secure.svg +1 -0
  234. package/public/file-icons/folder-seeders-open.svg +1 -0
  235. package/public/file-icons/folder-seeders.svg +1 -0
  236. package/public/file-icons/folder-server-open.svg +1 -0
  237. package/public/file-icons/folder-server.svg +1 -0
  238. package/public/file-icons/folder-shared-open.svg +1 -0
  239. package/public/file-icons/folder-shared.svg +1 -0
  240. package/public/file-icons/folder-skills-open.svg +1 -0
  241. package/public/file-icons/folder-skills.svg +1 -0
  242. package/public/file-icons/folder-src-open.svg +1 -0
  243. package/public/file-icons/folder-src.svg +1 -0
  244. package/public/file-icons/folder-store-open.svg +1 -0
  245. package/public/file-icons/folder-store.svg +1 -0
  246. package/public/file-icons/folder-storybook-open.svg +1 -0
  247. package/public/file-icons/folder-storybook.svg +1 -0
  248. package/public/file-icons/folder-supabase-open.svg +1 -0
  249. package/public/file-icons/folder-supabase.svg +1 -0
  250. package/public/file-icons/folder-svelte-open.svg +1 -0
  251. package/public/file-icons/folder-svelte.svg +1 -0
  252. package/public/file-icons/folder-target-open.svg +1 -0
  253. package/public/file-icons/folder-target.svg +1 -0
  254. package/public/file-icons/folder-tasks-open.svg +1 -0
  255. package/public/file-icons/folder-tasks.svg +1 -0
  256. package/public/file-icons/folder-temp-open.svg +1 -0
  257. package/public/file-icons/folder-temp.svg +1 -0
  258. package/public/file-icons/folder-template-open.svg +1 -0
  259. package/public/file-icons/folder-template.svg +1 -0
  260. package/public/file-icons/folder-terraform-open.svg +1 -0
  261. package/public/file-icons/folder-terraform.svg +1 -0
  262. package/public/file-icons/folder-test-open.svg +1 -0
  263. package/public/file-icons/folder-test.svg +1 -0
  264. package/public/file-icons/folder-theme-open.svg +1 -0
  265. package/public/file-icons/folder-theme.svg +1 -0
  266. package/public/file-icons/folder-tools-open.svg +1 -0
  267. package/public/file-icons/folder-tools.svg +1 -0
  268. package/public/file-icons/folder-turborepo-open.svg +1 -0
  269. package/public/file-icons/folder-turborepo.svg +1 -0
  270. package/public/file-icons/folder-typescript-open.svg +1 -0
  271. package/public/file-icons/folder-typescript.svg +1 -0
  272. package/public/file-icons/folder-ui-open.svg +1 -0
  273. package/public/file-icons/folder-ui.svg +1 -0
  274. package/public/file-icons/folder-utils-open.svg +1 -0
  275. package/public/file-icons/folder-utils.svg +1 -0
  276. package/public/file-icons/folder-video-open.svg +1 -0
  277. package/public/file-icons/folder-video.svg +1 -0
  278. package/public/file-icons/folder-views-open.svg +1 -0
  279. package/public/file-icons/folder-views.svg +1 -0
  280. package/public/file-icons/folder-vscode-open.svg +1 -0
  281. package/public/file-icons/folder-vscode.svg +1 -0
  282. package/public/file-icons/folder.svg +1 -0
  283. package/public/file-icons/font.svg +1 -0
  284. package/public/file-icons/fsharp.svg +1 -0
  285. package/public/file-icons/gemfile.svg +1 -0
  286. package/public/file-icons/git.svg +1 -0
  287. package/public/file-icons/go-mod.svg +1 -0
  288. package/public/file-icons/go.svg +1 -0
  289. package/public/file-icons/gradle.svg +1 -0
  290. package/public/file-icons/graphql.svg +1 -0
  291. package/public/file-icons/groovy.svg +1 -0
  292. package/public/file-icons/h.svg +1 -0
  293. package/public/file-icons/handlebars.svg +1 -0
  294. package/public/file-icons/haskell.svg +1 -0
  295. package/public/file-icons/hcl.svg +1 -0
  296. package/public/file-icons/heroku.svg +1 -0
  297. package/public/file-icons/hpp.svg +1 -0
  298. package/public/file-icons/html.svg +1 -0
  299. package/public/file-icons/husky.svg +1 -0
  300. package/public/file-icons/i18n.svg +1 -0
  301. package/public/file-icons/image.svg +1 -0
  302. package/public/file-icons/java.svg +1 -0
  303. package/public/file-icons/javascript.svg +1 -0
  304. package/public/file-icons/jenkins.svg +1 -0
  305. package/public/file-icons/jest.svg +1 -0
  306. package/public/file-icons/jsconfig.svg +1 -0
  307. package/public/file-icons/json.svg +1 -0
  308. package/public/file-icons/julia.svg +1 -0
  309. package/public/file-icons/jupyter.svg +1 -0
  310. package/public/file-icons/just.svg +1 -0
  311. package/public/file-icons/key.svg +1 -0
  312. package/public/file-icons/kotlin.svg +1 -0
  313. package/public/file-icons/lerna.svg +1 -0
  314. package/public/file-icons/less.svg +1 -0
  315. package/public/file-icons/license.svg +1 -0
  316. package/public/file-icons/lintstaged.svg +1 -0
  317. package/public/file-icons/liquid.svg +1 -0
  318. package/public/file-icons/lock.svg +1 -0
  319. package/public/file-icons/log.svg +1 -0
  320. package/public/file-icons/lua.svg +1 -0
  321. package/public/file-icons/makefile.svg +1 -0
  322. package/public/file-icons/markdown.svg +1 -0
  323. package/public/file-icons/maven.svg +1 -0
  324. package/public/file-icons/mdx.svg +1 -0
  325. package/public/file-icons/netlify.svg +1 -0
  326. package/public/file-icons/next.svg +1 -0
  327. package/public/file-icons/nim.svg +1 -0
  328. package/public/file-icons/nix.svg +1 -0
  329. package/public/file-icons/nodejs.svg +1 -0
  330. package/public/file-icons/nodemon.svg +1 -0
  331. package/public/file-icons/npm.svg +1 -0
  332. package/public/file-icons/nx.svg +1 -0
  333. package/public/file-icons/objective-c.svg +1 -0
  334. package/public/file-icons/objective-cpp.svg +1 -0
  335. package/public/file-icons/ocaml.svg +1 -0
  336. package/public/file-icons/pdf.svg +1 -0
  337. package/public/file-icons/php.svg +1 -0
  338. package/public/file-icons/pm2-ecosystem.svg +1 -0
  339. package/public/file-icons/pnpm.svg +1 -0
  340. package/public/file-icons/poetry.svg +1 -0
  341. package/public/file-icons/postcss.svg +1 -0
  342. package/public/file-icons/powerpoint.svg +1 -0
  343. package/public/file-icons/powershell.svg +1 -0
  344. package/public/file-icons/prettier.svg +1 -0
  345. package/public/file-icons/prisma.svg +1 -0
  346. package/public/file-icons/prolog.svg +1 -0
  347. package/public/file-icons/proto.svg +1 -0
  348. package/public/file-icons/pug.svg +1 -0
  349. package/public/file-icons/python-misc.svg +1 -0
  350. package/public/file-icons/python.svg +1 -0
  351. package/public/file-icons/r.svg +1 -0
  352. package/public/file-icons/razor.svg +1 -0
  353. package/public/file-icons/react.svg +1 -0
  354. package/public/file-icons/react_ts.svg +1 -0
  355. package/public/file-icons/readme.svg +1 -0
  356. package/public/file-icons/renovate.svg +1 -0
  357. package/public/file-icons/robots.svg +1 -0
  358. package/public/file-icons/rollup.svg +1 -0
  359. package/public/file-icons/ruby.svg +1 -0
  360. package/public/file-icons/rust.svg +1 -0
  361. package/public/file-icons/sass.svg +1 -0
  362. package/public/file-icons/scala.svg +1 -0
  363. package/public/file-icons/semantic-release.svg +1 -0
  364. package/public/file-icons/settings.svg +1 -0
  365. package/public/file-icons/shader.svg +1 -0
  366. package/public/file-icons/sml.svg +1 -0
  367. package/public/file-icons/solidity.svg +1 -0
  368. package/public/file-icons/stylus.svg +1 -0
  369. package/public/file-icons/svelte.svg +1 -0
  370. package/public/file-icons/svg.svg +1 -0
  371. package/public/file-icons/swift.svg +1 -0
  372. package/public/file-icons/table.svg +1 -0
  373. package/public/file-icons/tailwindcss.svg +1 -0
  374. package/public/file-icons/taskfile.svg +1 -0
  375. package/public/file-icons/terraform.svg +1 -0
  376. package/public/file-icons/test-js.svg +1 -0
  377. package/public/file-icons/test-jsx.svg +1 -0
  378. package/public/file-icons/test-ts.svg +1 -0
  379. package/public/file-icons/tex.svg +1 -0
  380. package/public/file-icons/toml.svg +1 -0
  381. package/public/file-icons/travis.svg +1 -0
  382. package/public/file-icons/tsconfig.svg +1 -0
  383. package/public/file-icons/tune.svg +1 -0
  384. package/public/file-icons/turborepo.svg +1 -0
  385. package/public/file-icons/typescript-def.svg +1 -0
  386. package/public/file-icons/typescript.svg +1 -0
  387. package/public/file-icons/unity.svg +1 -0
  388. package/public/file-icons/uv.svg +1 -0
  389. package/public/file-icons/vala.svg +1 -0
  390. package/public/file-icons/vercel.svg +1 -0
  391. package/public/file-icons/video.svg +1 -0
  392. package/public/file-icons/vim.svg +1 -0
  393. package/public/file-icons/vite.svg +1 -0
  394. package/public/file-icons/vitest.svg +1 -0
  395. package/public/file-icons/vlang.svg +1 -0
  396. package/public/file-icons/vue.svg +1 -0
  397. package/public/file-icons/webassembly.svg +1 -0
  398. package/public/file-icons/webpack.svg +1 -0
  399. package/public/file-icons/word.svg +1 -0
  400. package/public/file-icons/xml.svg +1 -0
  401. package/public/file-icons/yaml.svg +1 -0
  402. package/public/file-icons/yarn.svg +1 -0
  403. package/public/file-icons/zig.svg +1 -0
  404. package/public/file-icons/zip.svg +1 -0
  405. package/public/provider-icons/antgroup-color.svg +1 -0
  406. package/public/provider-icons/anthropic.svg +1 -0
  407. package/public/provider-icons/azure-color.svg +1 -0
  408. package/public/provider-icons/baseten.svg +1 -0
  409. package/public/provider-icons/bedrock-color.svg +1 -0
  410. package/public/provider-icons/cerebras-color.svg +1 -0
  411. package/public/provider-icons/cloudflare-color.svg +1 -0
  412. package/public/provider-icons/deepseek-color.svg +1 -0
  413. package/public/provider-icons/fireworks-color.svg +1 -0
  414. package/public/provider-icons/githubcopilot.svg +1 -0
  415. package/public/provider-icons/google-color.svg +1 -0
  416. package/public/provider-icons/googlecloud-color.svg +1 -0
  417. package/public/provider-icons/groq.svg +1 -0
  418. package/public/provider-icons/huggingface-color.svg +1 -0
  419. package/public/provider-icons/kimi-color.svg +1 -0
  420. package/public/provider-icons/minimax-color.svg +1 -0
  421. package/public/provider-icons/mistral-color.svg +1 -0
  422. package/public/provider-icons/moonshot.svg +1 -0
  423. package/public/provider-icons/nvidia-color.svg +1 -0
  424. package/public/provider-icons/openai.svg +1 -0
  425. package/public/provider-icons/opencode.svg +1 -0
  426. package/public/provider-icons/openrouter-color.svg +1 -0
  427. package/public/provider-icons/qwen-color.svg +1 -0
  428. package/public/provider-icons/together-color.svg +1 -0
  429. package/public/provider-icons/vercel.svg +1 -0
  430. package/public/provider-icons/xai.svg +1 -0
  431. package/public/provider-icons/xiaomimimo.svg +1 -0
  432. package/public/provider-icons/zai.svg +1 -0
  433. package/scripts/build-output.mjs +131 -0
  434. package/scripts/dev.mjs +18 -0
  435. package/scripts/entrypoint.mjs +8 -0
  436. package/scripts/install-build.mjs +134 -0
  437. package/scripts/launcher.mjs +249 -0
  438. package/scripts/pick-folder.ps1 +110 -0
  439. package/scripts/postinstall.mjs +12 -0
  440. package/scripts/process-runner.mjs +60 -0
  441. package/scripts/release.mjs +172 -0
  442. package/scripts/sync-file-icons.mjs +118 -0
  443. package/scripts/sync-provider-icons.mjs +41 -0
  444. package/src/app/api/agent/[id]/events/route.ts +81 -0
  445. package/src/app/api/agent/[id]/route.ts +24 -0
  446. package/src/app/api/agent/[id]/tree/route.ts +49 -0
  447. package/src/app/api/agent/new/route.ts +19 -0
  448. package/src/app/api/config/route.ts +22 -0
  449. package/src/app/api/files/route.ts +63 -0
  450. package/src/app/api/git/route.ts +32 -0
  451. package/src/app/api/growth/route.ts +50 -0
  452. package/src/app/api/health/route.ts +7 -0
  453. package/src/app/api/models/route.ts +129 -0
  454. package/src/app/api/pi-settings/route.ts +22 -0
  455. package/src/app/api/plugins/route.ts +79 -0
  456. package/src/app/api/security/route.ts +41 -0
  457. package/src/app/api/sessions/[id]/export/route.ts +25 -0
  458. package/src/app/api/sessions/[id]/route.ts +37 -0
  459. package/src/app/api/sessions/route.ts +23 -0
  460. package/src/app/api/skills/route.ts +39 -0
  461. package/src/app/api/update/route.ts +33 -0
  462. package/src/app/api/version/route.ts +8 -0
  463. package/src/app/api/web-auth/route.ts +80 -0
  464. package/src/app/api/workspaces/route.ts +78 -0
  465. package/src/app/globals.css +2039 -0
  466. package/src/app/icon.svg +21 -0
  467. package/src/app/layout.tsx +29 -0
  468. package/src/app/login/page.tsx +111 -0
  469. package/src/app/page.tsx +10 -0
  470. package/src/components/AppShell.tsx +1171 -0
  471. package/src/components/ChatInput.tsx +729 -0
  472. package/src/components/ChatWindow.tsx +1320 -0
  473. package/src/components/ContextMeter.tsx +131 -0
  474. package/src/components/DiffView.tsx +139 -0
  475. package/src/components/ExtensionUI.tsx +181 -0
  476. package/src/components/FileViewer.tsx +555 -0
  477. package/src/components/FullDiff.tsx +220 -0
  478. package/src/components/GitPanel.tsx +357 -0
  479. package/src/components/GrowthTree.tsx +317 -0
  480. package/src/components/ModelSelector.tsx +286 -0
  481. package/src/components/OutlineRail.tsx +166 -0
  482. package/src/components/PiMark.tsx +79 -0
  483. package/src/components/ProjectPanel.tsx +309 -0
  484. package/src/components/ProviderBrand.tsx +28 -0
  485. package/src/components/ProviderSetupModal.module.css +560 -0
  486. package/src/components/ProviderSetupModal.tsx +559 -0
  487. package/src/components/SessionSidebar.tsx +1066 -0
  488. package/src/components/SettingsPanel.tsx +2216 -0
  489. package/src/components/TrajectoryAnalyzer.module.css +225 -0
  490. package/src/components/TrajectoryView.tsx +289 -0
  491. package/src/components/icons.tsx +443 -0
  492. package/src/hooks/useFileViewer.ts +58 -0
  493. package/src/hooks/useGrowth.ts +635 -0
  494. package/src/hooks/usePiWeb.ts +900 -0
  495. package/src/i18n.tsx +1043 -0
  496. package/src/lib/agent-manager.ts +1348 -0
  497. package/src/lib/app-root.ts +4 -0
  498. package/src/lib/auth.ts +51 -0
  499. package/src/lib/command-validation.ts +116 -0
  500. package/src/lib/enter-behavior.ts +40 -0
  501. package/src/lib/extension-ui.ts +134 -0
  502. package/src/lib/file-icons.json +557 -0
  503. package/src/lib/file-icons.ts +39 -0
  504. package/src/lib/files-service.ts +280 -0
  505. package/src/lib/git-service.ts +255 -0
  506. package/src/lib/growth-rounds.ts +84 -0
  507. package/src/lib/growth-service.ts +586 -0
  508. package/src/lib/growth-tracker.ts +328 -0
  509. package/src/lib/growth-tree.ts +245 -0
  510. package/src/lib/growth-turns.ts +23 -0
  511. package/src/lib/highlight.ts +59 -0
  512. package/src/lib/model-draft.ts +103 -0
  513. package/src/lib/models-service.ts +852 -0
  514. package/src/lib/path-security.ts +101 -0
  515. package/src/lib/pi-settings.ts +115 -0
  516. package/src/lib/pi.ts +367 -0
  517. package/src/lib/plugins-service.ts +312 -0
  518. package/src/lib/process-format.ts +261 -0
  519. package/src/lib/provider-display.ts +57 -0
  520. package/src/lib/security-service.ts +52 -0
  521. package/src/lib/semver.ts +39 -0
  522. package/src/lib/session-reader.ts +204 -0
  523. package/src/lib/settings-write-lock.ts +45 -0
  524. package/src/lib/skills-service.ts +97 -0
  525. package/src/lib/text-sanitize.ts +20 -0
  526. package/src/lib/theme.ts +137 -0
  527. package/src/lib/trajectory.ts +294 -0
  528. package/src/lib/types.ts +300 -0
  529. package/src/lib/update-service.ts +129 -0
  530. package/src/lib/version.ts +6 -0
  531. package/src/lib/web-auth-shared.ts +35 -0
  532. package/src/lib/web-auth.ts +53 -0
  533. package/src/lib/workspace-store.ts +261 -0
  534. package/src/proxy.ts +53 -0
  535. package/tsconfig.json +49 -0
@@ -0,0 +1,852 @@
1
+ /**
2
+ * models-service:pi 模型目录 / 认证状态 / API key 管理 / 自定义 provider(models.json)。
3
+ */
4
+ import fs from "node:fs/promises";
5
+ import path from "node:path";
6
+ import { createHash, randomUUID } from "node:crypto";
7
+ import dns from "node:dns/promises";
8
+ import { request as httpRequest } from "node:http";
9
+ import { request as httpsRequest } from "node:https";
10
+ import { BlockList, isIP } from "node:net";
11
+ import { ModelRuntime } from "@earendil-works/pi-coding-agent";
12
+ import { getNodeValue, parseTree, printParseErrorCode, type ParseError } from "jsonc-parser";
13
+ import { getAgentDir, getModelRuntime, resetModelRuntime } from "./pi";
14
+ import {
15
+ getSupportedThinkingLevels, InMemoryCredentialStore, InMemoryModelsStore,
16
+ type AuthInteraction, type AuthPrompt, type AuthType,
17
+ } from "@earendil-works/pi-ai";
18
+ import { withExternalSettingsLock, withSettingsWriteLock } from "./settings-write-lock";
19
+
20
+ export interface ModelView {
21
+ provider: string;
22
+ id: string;
23
+ name: string;
24
+ api: string;
25
+ baseUrl?: string;
26
+ reasoning: boolean;
27
+ thinkingLevels: string[];
28
+ contextWindow: number;
29
+ input: string[];
30
+ cost: { input: number; output: number } | null;
31
+ }
32
+
33
+ export interface ProviderView {
34
+ id: string;
35
+ name: string;
36
+ baseUrl?: string;
37
+ apiKeyLabel?: string;
38
+ authTypes: Array<"api_key" | "oauth">;
39
+ apis: string[];
40
+ builtIn: boolean;
41
+ authConfigured: boolean;
42
+ authSource?: string;
43
+ authReady: boolean;
44
+ authFastPath: boolean;
45
+ resolvedAuthSource?: string;
46
+ authError?: string;
47
+ keyManaged: boolean;
48
+ modelCount: number;
49
+ }
50
+
51
+ export interface DiscoveredModel {
52
+ id: string;
53
+ name?: string;
54
+ }
55
+
56
+ let builtInProviderIdsPromise: Promise<Set<string>> | null = null;
57
+
58
+ /** 目录短缓存:页面挂载与设置关闭都会拉一次;认证探测(每个供应商一次网络往返)不必秒级重复 */
59
+ let listCache: { at: number; value: Promise<{ providers: ProviderView[]; models: ModelView[] }> } | null = null;
60
+ const LIST_TTL_MS = 5_000;
61
+
62
+ export function invalidateModelList(): void {
63
+ listCache = null;
64
+ }
65
+
66
+ /** Read the unmodified SDK catalog through its public modelsPath:null option. */
67
+ function getBuiltInProviderIds(): Promise<Set<string>> {
68
+ if (!builtInProviderIdsPromise) {
69
+ builtInProviderIdsPromise = ModelRuntime.create({
70
+ modelsPath: null, refreshOnCreate: false, allowModelNetwork: false,
71
+ credentials: new InMemoryCredentialStore(), modelsStore: new InMemoryModelsStore(),
72
+ })
73
+ .then((runtime) => new Set(runtime.getProviders().map((provider) => provider.id)))
74
+ .catch((error) => {
75
+ builtInProviderIdsPromise = null;
76
+ throw error;
77
+ });
78
+ }
79
+ return builtInProviderIdsPromise;
80
+ }
81
+
82
+ function envKeyOf(providerId: string): string | null {
83
+ void providerId;
84
+ return null;
85
+ }
86
+
87
+ export function listModels(): Promise<{
88
+ providers: ProviderView[];
89
+ models: ModelView[];
90
+ }> {
91
+ if (listCache && Date.now() - listCache.at < LIST_TTL_MS) return listCache.value;
92
+ const value = listModelsUncached().catch((error) => {
93
+ listCache = null;
94
+ throw error;
95
+ });
96
+ listCache = { at: Date.now(), value };
97
+ return value;
98
+ }
99
+
100
+ async function listModelsUncached(): Promise<{
101
+ providers: ProviderView[];
102
+ models: ModelView[];
103
+ }> {
104
+ const rt = await getModelRuntime();
105
+ const builtInProviderIds = await getBuiltInProviderIds();
106
+ const providers = rt.getProviders() as any[];
107
+ const out: ProviderView[] = [];
108
+ const models: ModelView[] = [];
109
+ // 认证探测逐个 await 时,N 个已配置供应商就是 N 次串行网络往返;并发做,单个超时不拖累整体
110
+ const checks = await Promise.all(
111
+ providers.map(async (p) => {
112
+ const pid = String(p.id ?? p.name ?? "");
113
+ if (!pid) return null;
114
+ let auth: any = null;
115
+ try {
116
+ auth = rt.getProviderAuthStatus(pid);
117
+ } catch {
118
+ auth = null;
119
+ }
120
+ const authFastPath = rt.hasConfiguredAuth(pid);
121
+ let authReady = false;
122
+ let resolvedAuthSource: string | undefined;
123
+ let authError: string | undefined;
124
+ if (auth?.configured === true || authFastPath) {
125
+ try {
126
+ const checked = await rt.checkAuth(pid, { signal: AbortSignal.timeout(5000) });
127
+ authReady = checked !== undefined;
128
+ resolvedAuthSource = checked?.source;
129
+ } catch (error) {
130
+ authError = error instanceof Error ? error.message : String(error);
131
+ }
132
+ }
133
+ return { pid, p, auth, authFastPath, authReady, resolvedAuthSource, authError };
134
+ }),
135
+ );
136
+ for (const c of checks) {
137
+ if (!c) continue;
138
+ const { pid, p, auth, authFastPath, authReady, resolvedAuthSource, authError } = c;
139
+ const list = rt.getModels(pid) as any[];
140
+ const authTypes: Array<"api_key" | "oauth"> = [];
141
+ if (p.auth?.apiKey) authTypes.push("api_key");
142
+ if (p.auth?.oauth) authTypes.push("oauth");
143
+ out.push({
144
+ id: pid,
145
+ name: String(p.name ?? pid),
146
+ baseUrl: typeof p.baseUrl === "string" ? p.baseUrl : undefined,
147
+ apiKeyLabel: typeof p.auth?.apiKey?.name === "string" ? p.auth.apiKey.name : undefined,
148
+ authTypes,
149
+ apis: [...new Set(list.map((mm) => String(mm.api ?? "")).filter(Boolean))].sort(),
150
+ builtIn: builtInProviderIds.has(pid),
151
+ authConfigured: auth?.configured === true,
152
+ authSource: auth?.source,
153
+ authReady,
154
+ authFastPath,
155
+ resolvedAuthSource,
156
+ authError,
157
+ keyManaged: auth?.source === "stored",
158
+ modelCount: list.length,
159
+ });
160
+ for (const mm of list) {
161
+ models.push({
162
+ provider: pid,
163
+ id: String(mm.id),
164
+ name: String(mm.name ?? mm.id),
165
+ api: String(mm.api ?? ""),
166
+ baseUrl: typeof mm.baseUrl === "string" ? mm.baseUrl : undefined,
167
+ reasoning: mm.reasoning === true,
168
+ thinkingLevels: getSupportedThinkingLevels(mm),
169
+ contextWindow: Number(mm.contextWindow ?? 0),
170
+ input: Array.isArray(mm.input) ? mm.input.map(String) : ["text"],
171
+ cost: mm.cost ? { input: mm.cost.input, output: mm.cost.output } : null,
172
+ });
173
+ }
174
+ }
175
+ models.sort((a, b) => a.provider.localeCompare(b.provider) || a.id.localeCompare(b.id));
176
+ return { providers: out, models };
177
+ }
178
+
179
+ /**
180
+ * 录入 / 更新 API key(写 ~/.pi/agent/auth.json,与终端 pi 共享)。
181
+ * 部分-provider 登录不止一步(Vertex/Bedrock 先选认证方式,Cloudflare 还要
182
+ * account/gateway ID):首个文本提示用调用方给的 key 应答,其余提示经
183
+ * pendingLogin 桥接到浏览器轮询(loginState/answerLogin/cancelLogin)。
184
+ */
185
+ export async function setApiKey(providerId: string, apiKey: string): Promise<void> {
186
+ const pl = createPendingLogin(providerId);
187
+ const rt = await getLoginRuntime(pl);
188
+ await runLogin(pl, rt, "api_key", apiKey);
189
+ }
190
+
191
+ export async function removeApiKey(providerId: string): Promise<void> {
192
+ // A device-code login must not keep logout queued or restore credentials later.
193
+ cancelLogin(providerId);
194
+ const rt = await getModelRuntime();
195
+ await rt.logout(providerId);
196
+ invalidateModelList();
197
+ }
198
+
199
+ /** 已存凭证回退:编辑表单的 key 框为空(「留空保持不变」)时,从这里取。oauth=true 表示来自 OAuth 令牌(部分端点不适用)。 */
200
+ async function storedCredentialFor(providerId: string): Promise<{ key: string; oauth: boolean } | undefined> {
201
+ // 自定义 provider / 内置覆盖层:models.json providers[id].apiKey。
202
+ // 损坏/权限失败不能静默回退到别的凭证;JSONC 与编辑器使用相同解析规则。
203
+ const custom = await readCustomProvidersSnapshot(modelsJsonPath());
204
+ if (custom.content !== undefined) {
205
+ const key = parseCustomProviders(custom.content).providers[providerId]?.apiKey;
206
+ if (typeof key === "string" && key.trim()) return { key: key.trim(), oauth: false };
207
+ }
208
+ // 内置 provider:auth.json 条目 —— API key 直接用;OAuth 条目的 access 就是
209
+ // SDK 发请求时的 Bearer key(如 xai 的 toAuth 返回 { apiKey: credential.access })
210
+ try {
211
+ const parsed = JSON.parse(await fs.readFile(path.join(getAgentDir(), "auth.json"), "utf8")) as Record<string, any>;
212
+ const entry = parsed?.[providerId] ?? parsed?.providers?.[providerId];
213
+ const key = entry?.apiKey ?? entry?.api_key ?? entry?.key;
214
+ if (typeof key === "string" && key.trim()) return { key: key.trim(), oauth: false };
215
+ if (entry?.type === "oauth" && typeof entry?.access === "string" && entry.access.trim()) return { key: entry.access.trim(), oauth: true };
216
+ } catch {
217
+ /* 同上 */
218
+ }
219
+ return undefined;
220
+ }
221
+
222
+ function modelsEndpoint(value: URL): URL {
223
+ const url = new URL(value);
224
+ const path = url.pathname.replace(/\/+$/, "");
225
+ url.pathname = path.endsWith("/models") ? path : `${path}/models`;
226
+ url.hash = "";
227
+ return url;
228
+ }
229
+
230
+ export function isTrustedCredentialEndpoint(target: URL, configuredUrls: Array<string | undefined>): boolean {
231
+ const expected = modelsEndpoint(target).href;
232
+ return configuredUrls.some((value) => {
233
+ if (!value) return false;
234
+ try {
235
+ const configured = new URL(value);
236
+ return !configured.username && !configured.password && modelsEndpoint(configured).href === expected;
237
+ } catch {
238
+ return false;
239
+ }
240
+ });
241
+ }
242
+
243
+ const MAX_MODEL_CATALOG_BYTES = 4 * 1024 * 1024;
244
+
245
+ const blockedIpv4 = new BlockList();
246
+ for (const [address, prefix] of [
247
+ ["0.0.0.0", 8], ["10.0.0.0", 8], ["100.64.0.0", 10], ["127.0.0.0", 8],
248
+ ["169.254.0.0", 16], ["172.16.0.0", 12], ["192.0.0.0", 24], ["192.0.2.0", 24],
249
+ ["192.88.99.0", 24], ["192.168.0.0", 16], ["198.18.0.0", 15],
250
+ ["198.51.100.0", 24], ["203.0.113.0", 24], ["224.0.0.0", 4], ["240.0.0.0", 4],
251
+ ] as const) blockedIpv4.addSubnet(address, prefix, "ipv4");
252
+ const globalIpv6 = new BlockList();
253
+ globalIpv6.addSubnet("2000::", 3, "ipv6");
254
+ const blockedIpv6 = new BlockList();
255
+ for (const [address, prefix] of [
256
+ ["2001::", 23], ["2001:db8::", 32], ["2002::", 16],
257
+ ] as const) blockedIpv6.addSubnet(address, prefix, "ipv6");
258
+
259
+ export function isPublicModelDiscoveryAddress(address: string): boolean {
260
+ const family = isIP(address);
261
+ if (family === 4) return !blockedIpv4.check(address, "ipv4");
262
+ if (family === 6) return globalIpv6.check(address, "ipv6") && !blockedIpv6.check(address, "ipv6");
263
+ return false;
264
+ }
265
+
266
+ async function requestModelCatalog(endpoint: URL, headers: Record<string, string>, signal: AbortSignal): Promise<Response> {
267
+ const allowPrivate = process.env.PI_WEB_ALLOW_PRIVATE_MODEL_DISCOVERY === "1";
268
+ const literalFamily = isIP(endpoint.hostname.replace(/^\[|\]$/g, ""));
269
+ if (!allowPrivate && literalFamily && !isPublicModelDiscoveryAddress(endpoint.hostname.replace(/^\[|\]$/g, ""))) {
270
+ throw new Error("模型目录地址指向本机或私有网络;如需访问本地网关,请设置 PI_WEB_ALLOW_PRIVATE_MODEL_DISCOVERY=1");
271
+ }
272
+ return new Promise<Response>((resolve, reject) => {
273
+ const request = (endpoint.protocol === "https:" ? httpsRequest : httpRequest)(endpoint, {
274
+ headers,
275
+ signal,
276
+ agent: false,
277
+ lookup: allowPrivate ? undefined : (hostname, options, callback) => {
278
+ void dns.lookup(hostname, { all: true }).then((addresses) => {
279
+ const publicAddress = addresses.find(({ address }) => isPublicModelDiscoveryAddress(address));
280
+ if (!publicAddress) {
281
+ callback(new Error("模型目录地址指向本机或私有网络;如需访问本地网关,请设置 PI_WEB_ALLOW_PRIVATE_MODEL_DISCOVERY=1"), "", 0);
282
+ return;
283
+ }
284
+ if (options.all) callback(null, [publicAddress]);
285
+ else callback(null, publicAddress.address, publicAddress.family);
286
+ }, (error) => callback(error, "", 0));
287
+ },
288
+ }, (response) => {
289
+ const status = response.statusCode ?? 0;
290
+ if (status >= 300 && status < 400) {
291
+ response.destroy();
292
+ reject(new Error("模型目录不允许重定向"));
293
+ return;
294
+ }
295
+ const chunks: Buffer[] = [];
296
+ let size = 0;
297
+ const declaredSize = Number(response.headers["content-length"]);
298
+ if (declaredSize > MAX_MODEL_CATALOG_BYTES) {
299
+ response.destroy();
300
+ reject(new Error("模型目录响应过大(上限 4 MiB)"));
301
+ return;
302
+ }
303
+ response.on("data", (chunk: Buffer) => {
304
+ size += chunk.length;
305
+ if (size > MAX_MODEL_CATALOG_BYTES) {
306
+ response.destroy();
307
+ reject(new Error("模型目录响应过大(上限 4 MiB)"));
308
+ return;
309
+ }
310
+ chunks.push(chunk);
311
+ });
312
+ response.on("error", reject);
313
+ response.on("close", () => {
314
+ if (!response.complete) reject(new Error("模型目录连接提前关闭"));
315
+ });
316
+ response.on("end", () => resolve(new Response(status === 204 || status === 205 || status === 304 ? null : Buffer.concat(chunks, size), { status })));
317
+ });
318
+ request.on("error", reject);
319
+ request.end();
320
+ });
321
+ }
322
+
323
+ async function readModelCatalog(response: Response): Promise<unknown> {
324
+ const declaredSize = Number(response.headers.get("content-length"));
325
+ if (declaredSize > MAX_MODEL_CATALOG_BYTES) throw new Error("模型目录响应过大(上限 4 MiB)");
326
+ if (!response.body) throw new Error("模型目录响应为空");
327
+ const reader = response.body.getReader();
328
+ const chunks: Buffer[] = [];
329
+ let size = 0;
330
+ try {
331
+ for (;;) {
332
+ const { done, value } = await reader.read();
333
+ if (done) break;
334
+ size += value.byteLength;
335
+ if (size > MAX_MODEL_CATALOG_BYTES) throw new Error("模型目录响应过大(上限 4 MiB)");
336
+ chunks.push(Buffer.from(value));
337
+ }
338
+ } finally {
339
+ await reader.cancel().catch(() => undefined);
340
+ reader.releaseLock();
341
+ }
342
+ return JSON.parse(Buffer.concat(chunks, size).toString("utf8")) as unknown;
343
+ }
344
+
345
+ /** Fetch a provider's conventional /models catalog without persisting credentials. */
346
+ export async function discoverModels(input: { baseUrl: string; api?: string; apiKey?: string; providerId?: string }): Promise<DiscoveredModel[]> {
347
+ const rawBaseUrl = input.baseUrl.trim();
348
+ if (!rawBaseUrl) throw new Error("缺少 API 地址");
349
+ const base = new URL(rawBaseUrl);
350
+ if (base.protocol !== "http:" && base.protocol !== "https:") throw new Error("API 地址必须使用 http 或 https");
351
+ if (base.username || base.password) throw new Error("API 地址不能包含用户名或密码");
352
+ const endpoint = modelsEndpoint(base);
353
+ const headers: Record<string, string> = { Accept: "application/json" };
354
+ // 表单 key 为空时回退到该 provider 的已存凭证(编辑场景:占位符「留空保持不变」)
355
+ const credential = input.apiKey?.trim()
356
+ ? { key: input.apiKey.trim(), oauth: false }
357
+ : input.providerId
358
+ ? await storedCredentialFor(input.providerId)
359
+ : undefined;
360
+ if (credential && !input.apiKey?.trim()) {
361
+ const rt = await getModelRuntime();
362
+ const provider = rt.getProviders().find((p) => p.id === input.providerId);
363
+ const configuredUrls = [provider?.baseUrl, ...(provider ? rt.getModels(provider.id).map((model) => model.baseUrl) : [])];
364
+ if (!isTrustedCredentialEndpoint(endpoint, configuredUrls)) {
365
+ throw new Error("已存凭证只能用于该 Provider 已配置的 API 地址;新地址请显式填写密钥");
366
+ }
367
+ }
368
+ const apiKey = credential?.key;
369
+ // OpenAI Codex 的模型目录由 SDK 内置(chatgpt.com 后端无公开 /models),OAuth 令牌无法用于目录探测
370
+ if (!input.apiKey?.trim() && credential?.oauth && input.providerId === "openai-codex") {
371
+ throw new Error("OpenAI Codex 的模型目录由 SDK 内置,无需获取;如需自定义请手动添加模型 ID");
372
+ }
373
+ if (apiKey) {
374
+ if (input.api === "anthropic-messages") {
375
+ headers["x-api-key"] = apiKey;
376
+ // Anthropic 的 OAuth 令牌必须带 beta 头才被接受(与 SDK 发消息时一致)
377
+ if (credential?.oauth) headers["anthropic-beta"] = "oauth-2025-04-20";
378
+ } else {
379
+ headers.Authorization = `Bearer ${apiKey}`;
380
+ }
381
+ }
382
+ if (input.api === "anthropic-messages") headers["anthropic-version"] = "2023-06-01";
383
+ const controller = new AbortController();
384
+ const timeout = setTimeout(() => controller.abort(), 15000);
385
+ try {
386
+ const response = await requestModelCatalog(endpoint, headers, controller.signal);
387
+ if (response.status === 401) {
388
+ throw new Error(apiKey ? "模型目录请求失败(HTTP 401):密钥或令牌无效" : "模型目录请求失败(HTTP 401):请先填写 API 密钥");
389
+ }
390
+ if (response.status === 403) {
391
+ throw new Error(credential?.oauth
392
+ ? "模型目录请求失败(HTTP 403):该 OAuth 令牌无权访问模型目录(部分提供商不支持),请手动填写模型 ID"
393
+ : "模型目录请求失败(HTTP 403):密钥无权访问模型目录");
394
+ }
395
+ if (!response.ok) throw new Error(`模型目录请求失败(HTTP ${response.status})`);
396
+ const payload = await readModelCatalog(response);
397
+ const candidates = Array.isArray(payload)
398
+ ? payload
399
+ : payload && typeof payload === "object"
400
+ ? Array.isArray((payload as { data?: unknown }).data)
401
+ ? (payload as { data: unknown[] }).data
402
+ : Array.isArray((payload as { models?: unknown }).models)
403
+ ? (payload as { models: unknown[] }).models
404
+ : []
405
+ : [];
406
+ const models = candidates
407
+ .map((candidate): DiscoveredModel | null => {
408
+ if (typeof candidate === "string") return { id: candidate };
409
+ if (!candidate || typeof candidate !== "object") return null;
410
+ const value = candidate as Record<string, unknown>;
411
+ const id = typeof value.id === "string" ? value.id.trim() : "";
412
+ if (!id) return null;
413
+ const name = typeof value.name === "string" && value.name.trim() ? value.name.trim() : undefined;
414
+ return { id, ...(name ? { name } : {}) };
415
+ })
416
+ .filter((model): model is DiscoveredModel => Boolean(model))
417
+ .filter((model, index, all) => all.findIndex((item) => item.id === model.id) === index)
418
+ .slice(0, 500);
419
+ if (!models.length) throw new Error("响应中没有可识别的模型 ID");
420
+ return models;
421
+ } catch (error) {
422
+ if (error instanceof Error && error.name === "AbortError") throw new Error("模型目录请求超时");
423
+ if (error && typeof error === "object" && "code" in error
424
+ && ["ECONNREFUSED", "ECONNRESET", "ENOTFOUND", "EHOSTUNREACH", "ENETUNREACH", "ETIMEDOUT"].includes(String(error.code))) {
425
+ throw new Error("无法连接模型目录,请检查 API 地址和网络");
426
+ }
427
+ if (error instanceof TypeError && error.message.toLowerCase().includes("fetch")) throw new Error("无法连接模型目录,请检查 API 地址和网络");
428
+ throw error;
429
+ } finally {
430
+ clearTimeout(timeout);
431
+ }
432
+ }
433
+
434
+ // ---------- xAI 订阅配额(响应头 x-ratelimit-*) ----------
435
+
436
+ export interface ProviderUsage {
437
+ model: string;
438
+ requestLimit: number;
439
+ requestRemaining: number;
440
+ tokenLimit: number;
441
+ tokenRemaining: number;
442
+ probedAt: number;
443
+ }
444
+
445
+ const usageCache = new Map<string, { at: number; value: ProviderUsage | null }>();
446
+ const USAGE_TTL_MS = 5 * 60_000;
447
+
448
+ /**
449
+ * 探测提供商的实时配额。xAI 不提供周限查询端点,只能从一次最小对话请求的
450
+ * x-ratelimit-* 响应头读取 limit/remaining(窗口周期 xAI 未在头部标注)。
451
+ * 目前仅 xai(OAuth 订阅)接入;探测结果缓存 5 分钟。
452
+ */
453
+ export async function providerUsage(providerId: string, force = false): Promise<ProviderUsage | null> {
454
+ if (providerId !== "xai") return null;
455
+ const cached = usageCache.get(providerId);
456
+ if (!force && cached && Date.now() - cached.at < USAGE_TTL_MS) return cached.value;
457
+ const value = await probeXaiUsage();
458
+ usageCache.set(providerId, { at: Date.now(), value });
459
+ return value;
460
+ }
461
+
462
+ async function probeXaiUsage(): Promise<ProviderUsage | null> {
463
+ const credential = await storedCredentialFor("xai");
464
+ if (!credential) return null;
465
+ const key = credential.key;
466
+ let model = "";
467
+ try {
468
+ const rt = await getModelRuntime();
469
+ const models = rt.getModels("xai") as any[];
470
+ // 限额是按模型计算的:挑最新主力文本模型(grok-4.6 > 4.5 > 4.3),
471
+ // 排除 imagine(不走对话接口)与 multi-agent(需 beta)
472
+ const usable = models
473
+ .map((m) => String(m.id))
474
+ .filter((id) => !id.includes("imagine") && !id.includes("multi-agent"));
475
+ model = ["grok-4.6", "grok-4.5", "grok-4.3"].find((want) => usable.some((id) => id === want || id.startsWith(`${want}-`))) ?? usable[0] ?? "";
476
+ } catch {
477
+ return null;
478
+ }
479
+ if (!model) return null;
480
+ const controller = new AbortController();
481
+ const timeout = setTimeout(() => controller.abort(), 20_000);
482
+ try {
483
+ const response = await fetch("https://api.x.ai/v1/chat/completions", {
484
+ method: "POST",
485
+ headers: { Authorization: `Bearer ${key}`, "Content-Type": "application/json" },
486
+ body: JSON.stringify({ model, messages: [{ role: "user", content: "ping" }], max_tokens: 1 }),
487
+ signal: controller.signal,
488
+ });
489
+ void response.body?.cancel().catch(() => {});
490
+ const get = (name: string) => Number(response.headers.get(name) ?? NaN);
491
+ const requestLimit = get("x-ratelimit-limit-requests");
492
+ const requestRemaining = get("x-ratelimit-remaining-requests");
493
+ const tokenLimit = get("x-ratelimit-limit-tokens");
494
+ const tokenRemaining = get("x-ratelimit-remaining-tokens");
495
+ if (![requestLimit, requestRemaining, tokenLimit, tokenRemaining].every(Number.isFinite)) return null;
496
+ return { model, requestLimit, requestRemaining, tokenLimit, tokenRemaining, probedAt: Date.now() };
497
+ } catch {
498
+ return null;
499
+ } finally {
500
+ clearTimeout(timeout);
501
+ }
502
+ }
503
+
504
+ // ---------- OAuth 订阅登录(Claude/Codex/Copilot 等) ----------
505
+
506
+ interface PendingLogin {
507
+ providerId: string;
508
+ controller: AbortController;
509
+ timeout: ReturnType<typeof setTimeout> | null;
510
+ prompt: AuthPrompt | null;
511
+ resolver: ((value: string) => void) | null;
512
+ rejecter: ((err: Error) => void) | null;
513
+ notifyLog: string[];
514
+ done: boolean;
515
+ error?: string;
516
+ finishedAt?: number;
517
+ }
518
+
519
+ const pendingLogins = new Map<string, PendingLogin>();
520
+ const LOGIN_TIMEOUT_MS = 10 * 60_000;
521
+
522
+ /** 清理已完成且超过 10 分钟的登录条目,避免长期运行内存缓慢增长。 */
523
+ function purgeFinishedLogins(): void {
524
+ const now = Date.now();
525
+ for (const [id, pl] of pendingLogins) {
526
+ if (pl.done && pl.finishedAt !== undefined && now - pl.finishedAt > 10 * 60_000) pendingLogins.delete(id);
527
+ }
528
+ }
529
+
530
+ function isActiveLogin(pl: PendingLogin): boolean {
531
+ return pendingLogins.get(pl.providerId) === pl && !pl.done && !pl.controller.signal.aborted;
532
+ }
533
+
534
+ function assertActiveLogin(pl: PendingLogin): void {
535
+ pl.controller.signal.throwIfAborted();
536
+ if (!isActiveLogin(pl)) throw new Error("canceled");
537
+ }
538
+
539
+ function finishLogin(pl: PendingLogin, error?: unknown): boolean {
540
+ // Late completions/callbacks must never revive a canceled or replaced job.
541
+ if (pendingLogins.get(pl.providerId) !== pl || pl.done) return false;
542
+ pl.done = true;
543
+ if (error !== undefined) pl.error = error instanceof Error ? error.message : String(error);
544
+ pl.finishedAt = Date.now();
545
+ if (pl.timeout) clearTimeout(pl.timeout);
546
+ pl.timeout = null;
547
+ pl.rejecter?.(error instanceof Error ? error : new Error(pl.error ?? "login finished"));
548
+ pl.prompt = null;
549
+ pl.resolver = null;
550
+ pl.rejecter = null;
551
+ return true;
552
+ }
553
+
554
+ function abortLogin(pl: PendingLogin, error: Error): void {
555
+ finishLogin(pl, error);
556
+ // Public SDK contract: abort both the device-code flow and its queued credential write.
557
+ pl.controller.abort(error);
558
+ }
559
+
560
+ function createPendingLogin(providerId: string): PendingLogin {
561
+ purgeFinishedLogins();
562
+ const previous = pendingLogins.get(providerId);
563
+ if (previous && !previous.done) abortLogin(previous, new Error("canceled"));
564
+ const pl: PendingLogin = {
565
+ providerId, controller: new AbortController(), timeout: null,
566
+ prompt: null, resolver: null, rejecter: null, notifyLog: [], done: false,
567
+ };
568
+ pendingLogins.set(providerId, pl);
569
+ pl.timeout = setTimeout(() => abortLogin(pl, new Error("login timed out")), LOGIN_TIMEOUT_MS);
570
+ pl.timeout.unref?.();
571
+ return pl;
572
+ }
573
+
574
+ async function getLoginRuntime(pl: PendingLogin): Promise<ModelRuntime> {
575
+ try {
576
+ const rt = await getModelRuntime();
577
+ assertActiveLogin(pl);
578
+ return rt;
579
+ } catch (error) {
580
+ finishLogin(pl, error);
581
+ throw error;
582
+ }
583
+ }
584
+
585
+ function promptForLogin(pl: PendingLogin, prompt: AuthPrompt): Promise<string> {
586
+ assertActiveLogin(pl);
587
+ const signal = prompt.signal ? AbortSignal.any([pl.controller.signal, prompt.signal]) : pl.controller.signal;
588
+ signal.throwIfAborted();
589
+ pl.rejecter?.(new Error("prompt replaced"));
590
+ return new Promise<string>((resolve, reject) => {
591
+ let settled = false;
592
+ const cleanup = () => {
593
+ signal.removeEventListener("abort", onAbort);
594
+ if (pl.resolver === answer) {
595
+ pl.prompt = null;
596
+ pl.resolver = null;
597
+ pl.rejecter = null;
598
+ }
599
+ };
600
+ const answer = (value: string) => {
601
+ if (settled) return;
602
+ settled = true;
603
+ cleanup();
604
+ resolve(value);
605
+ };
606
+ const fail = (error: Error) => {
607
+ if (settled) return;
608
+ settled = true;
609
+ cleanup();
610
+ reject(error);
611
+ };
612
+ const onAbort = () => fail(signal.reason instanceof Error ? signal.reason : new Error("canceled"));
613
+ pl.prompt = prompt;
614
+ pl.resolver = answer;
615
+ pl.rejecter = fail;
616
+ signal.addEventListener("abort", onAbort, { once: true });
617
+ if (signal.aborted) onAbort();
618
+ });
619
+ }
620
+
621
+ async function runLogin(pl: PendingLogin, rt: ModelRuntime, type: AuthType, apiKey?: string): Promise<void> {
622
+ let firstTextAnswered = false;
623
+ const interaction: AuthInteraction = {
624
+ signal: pl.controller.signal,
625
+ prompt: async (prompt) => {
626
+ assertActiveLogin(pl);
627
+ prompt.signal?.throwIfAborted();
628
+ if (apiKey !== undefined && !firstTextAnswered && prompt.type !== "select") {
629
+ firstTextAnswered = true;
630
+ return apiKey;
631
+ }
632
+ return promptForLogin(pl, prompt);
633
+ },
634
+ notify: (event) => {
635
+ if (!isActiveLogin(pl)) return;
636
+ pl.notifyLog.push(typeof event === "string" ? event : JSON.stringify(event));
637
+ if (pl.notifyLog.length > 20) pl.notifyLog.shift();
638
+ },
639
+ };
640
+ try {
641
+ assertActiveLogin(pl);
642
+ await rt.login(pl.providerId, type, interaction);
643
+ assertActiveLogin(pl);
644
+ if (finishLogin(pl)) invalidateModelList();
645
+ } catch (error) {
646
+ finishLogin(pl, error);
647
+ throw error;
648
+ }
649
+ }
650
+
651
+ /** 启动 OAuth 登录(后台运行,交互经 pendingLogin 桥接到浏览器轮询) */
652
+ export async function startLogin(providerId: string): Promise<void> {
653
+ const pl = createPendingLogin(providerId);
654
+ const rt = await getLoginRuntime(pl);
655
+ void runLogin(pl, rt, "oauth").catch(() => undefined); // terminal errors are exposed by loginState
656
+ }
657
+
658
+ export function loginState(providerId: string): {
659
+ prompt: { type: string; message: string; placeholder?: string; options?: Array<{ label: string; value: string }> } | null;
660
+ notifyLog: string[];
661
+ done: boolean;
662
+ error?: string;
663
+ } {
664
+ purgeFinishedLogins();
665
+ const pl = pendingLogins.get(providerId);
666
+ if (!pl) return { prompt: null, notifyLog: [], done: true };
667
+ return {
668
+ prompt: pl.prompt
669
+ ? {
670
+ type: pl.prompt.type,
671
+ message: pl.prompt.message,
672
+ placeholder: "placeholder" in pl.prompt ? pl.prompt.placeholder : undefined,
673
+ // select 型提示:answerLogin 回传 SDK 的选项 id,而不是 label。
674
+ options: pl.prompt.type === "select"
675
+ ? pl.prompt.options.map((option) => ({ label: option.label, value: option.id }))
676
+ : undefined,
677
+ }
678
+ : null,
679
+ notifyLog: [...pl.notifyLog],
680
+ done: pl.done,
681
+ error: pl.error,
682
+ };
683
+ }
684
+
685
+ export function answerLogin(providerId: string, text: string): boolean {
686
+ const pl = pendingLogins.get(providerId);
687
+ if (!pl || !isActiveLogin(pl) || !pl.resolver) return false;
688
+ pl.resolver(text);
689
+ return true;
690
+ }
691
+
692
+ export function cancelLogin(providerId: string): boolean {
693
+ purgeFinishedLogins();
694
+ const pl = pendingLogins.get(providerId);
695
+ if (!pl) return false;
696
+ if (!pl.done) abortLogin(pl, new Error("canceled"));
697
+ return true;
698
+ }
699
+
700
+ // ---------- 自定义 provider(~/.pi/agent/models.json) ----------
701
+
702
+ function modelsJsonPath(): string {
703
+ return path.join(getAgentDir(), "models.json");
704
+ }
705
+
706
+ interface CustomProvidersFile extends Record<string, unknown> {
707
+ providers: Record<string, Record<string, unknown>>;
708
+ }
709
+
710
+ export class CustomProvidersValidationError extends Error {}
711
+ export class CustomProvidersConflictError extends Error {
712
+ constructor() {
713
+ super("models.json 已被修改,请重新加载后再保存");
714
+ }
715
+ }
716
+
717
+ function parseCustomProviders(content: string): CustomProvidersFile {
718
+ const errors: ParseError[] = [];
719
+ const tree = parseTree(content.replace(/^\uFEFF/, ""), errors, { allowTrailingComma: true, allowEmptyContent: false });
720
+ if (errors.length || !tree) {
721
+ const first = errors[0];
722
+ // Do not include a source excerpt: it may contain an API key.
723
+ throw new CustomProvidersValidationError(`models.json JSONC 无效${first ? ` (${printParseErrorCode(first.error)}, offset ${first.offset})` : ""}`);
724
+ }
725
+ // getNodeValue uses null-prototype objects, so keys such as __proto__ round-trip as data.
726
+ const parsed: unknown = getNodeValue(tree);
727
+ if (!isRecord(parsed) || !isRecord(parsed.providers)) {
728
+ throw new CustomProvidersValidationError('models.json 顶层必须是 { "providers": { ... } }');
729
+ }
730
+ for (const [id, provider] of Object.entries(parsed.providers)) {
731
+ if (!isRecord(provider)) throw new CustomProvidersValidationError(`provider "${id}" 必须是对象`);
732
+ }
733
+ return parsed as CustomProvidersFile;
734
+ }
735
+
736
+ function isRecord(value: unknown): value is Record<string, unknown> {
737
+ return value !== null && typeof value === "object" && !Array.isArray(value);
738
+ }
739
+
740
+ export function redactCustomProviderSecrets(content: string): { content: string; secretProviderIds: string[] } {
741
+ const parsed = parseCustomProviders(content);
742
+ const secretProviderIds: string[] = [];
743
+ for (const [id, provider] of Object.entries(parsed.providers)) {
744
+ if (!Object.prototype.hasOwnProperty.call(provider, "apiKey")) continue;
745
+ delete provider.apiKey;
746
+ secretProviderIds.push(id);
747
+ }
748
+ return { content: JSON.stringify(parsed, null, 2), secretProviderIds };
749
+ }
750
+
751
+ function mergeCustomProviderApiKeys(parsed: CustomProvidersFile, previous: CustomProvidersFile): void {
752
+ for (const [id, provider] of Object.entries(parsed.providers)) {
753
+ // 字段缺失(包括 JSON.stringify 省略的 undefined)保留旧密钥;只有显式 null 删除。
754
+ if (provider.apiKey === null) {
755
+ delete provider.apiKey;
756
+ continue;
757
+ }
758
+ const previousProvider = previous.providers[id];
759
+ if (!Object.prototype.hasOwnProperty.call(provider, "apiKey") && previousProvider
760
+ && Object.prototype.hasOwnProperty.call(previousProvider, "apiKey")) {
761
+ provider.apiKey = previousProvider.apiKey;
762
+ }
763
+ }
764
+ }
765
+
766
+ export function preserveCustomProviderApiKeys(content: string, previousContent: string): string {
767
+ const parsed = parseCustomProviders(content);
768
+ mergeCustomProviderApiKeys(parsed, parseCustomProviders(previousContent));
769
+ return JSON.stringify(parsed, null, 2);
770
+ }
771
+
772
+ async function readCustomProvidersSnapshot(file: string): Promise<{ content: string | undefined; revision: string }> {
773
+ try {
774
+ const content = await fs.readFile(file, "utf8");
775
+ return { content, revision: createHash("sha256").update(content).digest("hex") };
776
+ } catch (error) {
777
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
778
+ return { content: undefined, revision: "missing" };
779
+ }
780
+ }
781
+
782
+ export async function readCustomProviders(): Promise<{ content: string; exists: boolean; secretProviderIds: string[]; revision: string }> {
783
+ const snapshot = await readCustomProvidersSnapshot(modelsJsonPath());
784
+ return {
785
+ ...redactCustomProviderSecrets(snapshot.content ?? '{"providers":{}}'),
786
+ exists: snapshot.content !== undefined,
787
+ revision: snapshot.revision,
788
+ };
789
+ }
790
+
791
+ function validateTokenLimits(parsed: CustomProvidersFile): void {
792
+ for (const [id, provider] of Object.entries(parsed.providers)) {
793
+ if (!id.trim()) throw new CustomProvidersValidationError("provider ID 必须是非空字符串");
794
+ const models = Array.isArray(provider.models) ? provider.models : [];
795
+ const overrides = isRecord(provider.modelOverrides) ? Object.values(provider.modelOverrides) : [];
796
+ for (const model of [...models, ...overrides]) {
797
+ if (!isRecord(model)) continue; // Full structural validation is delegated to the SDK below.
798
+ for (const field of ["contextWindow", "maxTokens"] as const) {
799
+ if (model[field] === undefined) continue;
800
+ if (typeof model[field] !== "number" || !Number.isSafeInteger(model[field]) || model[field] <= 0) {
801
+ throw new CustomProvidersValidationError(`provider "${id}" 的 ${field} 必须是正整数`);
802
+ }
803
+ }
804
+ }
805
+ }
806
+ }
807
+
808
+ async function validateStagedCustomProviders(file: string): Promise<void> {
809
+ // Only the public SDK schema/composition contract. No auth files, network, commands,
810
+ // model refresh, or extension/resource loading participate in configuration validation.
811
+ const runtime = await ModelRuntime.create({
812
+ modelsPath: file,
813
+ credentials: new InMemoryCredentialStore(),
814
+ modelsStore: new InMemoryModelsStore(),
815
+ allowModelNetwork: false,
816
+ refreshOnCreate: false,
817
+ });
818
+ const error = runtime.getError();
819
+ if (error) throw new CustomProvidersValidationError(error);
820
+ }
821
+
822
+ export async function writeCustomProviders(content: string, revision?: string): Promise<void> {
823
+ const file = modelsJsonPath();
824
+ await withSettingsWriteLock(file, () => withExternalSettingsLock(file, async () => {
825
+ const previous = await readCustomProvidersSnapshot(file);
826
+ // Compare before parsing or restoring secrets; stale clients must reload, not overwrite.
827
+ if (revision !== undefined && revision !== previous.revision) throw new CustomProvidersConflictError();
828
+ const parsed = parseCustomProviders(content);
829
+ const old = parseCustomProviders(previous.content ?? '{"providers":{}}');
830
+ const temporary = `${file}.${process.pid}.${randomUUID()}.tmp`;
831
+ try {
832
+ if (previous.content !== undefined) {
833
+ await fs.writeFile(temporary, JSON.stringify(old, null, 2), { encoding: "utf8", flag: "wx", mode: 0o600 });
834
+ // A damaged existing config is never treated as an empty/missing file.
835
+ await validateStagedCustomProviders(temporary);
836
+ }
837
+ mergeCustomProviderApiKeys(parsed, old);
838
+ validateTokenLimits(parsed);
839
+ await fs.writeFile(temporary, JSON.stringify(parsed, null, 2), {
840
+ encoding: "utf8", flag: previous.content === undefined ? "wx" : "w", mode: 0o600,
841
+ });
842
+ await validateStagedCustomProviders(temporary);
843
+ // Also detect non-cooperating writers that changed the file during validation.
844
+ if ((await readCustomProvidersSnapshot(file)).revision !== previous.revision) throw new CustomProvidersConflictError();
845
+ await fs.rename(temporary, file);
846
+ } finally {
847
+ await fs.rm(temporary, { force: true }).catch(() => undefined);
848
+ }
849
+ }));
850
+ resetModelRuntime();
851
+ invalidateModelList();
852
+ }