cozyclay 2.1.0 → 3.0.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 (583) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/README.md +12 -30
  3. package/bin/agent/agent-routes.mjs +40 -113
  4. package/bin/agent/agent-runner.mjs +9 -10
  5. package/bin/agent/cliproxy-provider.mjs +135 -0
  6. package/bin/agent/credential-store.mjs +30 -7
  7. package/bin/agent/pi-tools.mjs +4 -1
  8. package/bin/agent/provider-keys.mjs +32 -0
  9. package/bin/agent/provider-login.mjs +110 -0
  10. package/bin/agent/provider-routes.mjs +99 -0
  11. package/bin/agent/providers.mjs +19 -97
  12. package/bin/agent/script-tool.mjs +17 -4
  13. package/bin/agent/studio-motion-reference.mjs +23 -0
  14. package/bin/agent/studio-prompt.mjs +1 -1
  15. package/bin/agent/studio-rebase-text.mjs +32 -0
  16. package/bin/agent/studio-rebase.mjs +26 -39
  17. package/bin/agent/studio-tools.mjs +9 -28
  18. package/bin/agent/tool-arguments.mjs +49 -0
  19. package/bin/agent/turn-frames.mjs +66 -0
  20. package/bin/codex-auth.mjs +9 -2
  21. package/bin/cozyclay.mjs +10 -116
  22. package/bin/live/cli.mjs +22 -6
  23. package/bin/mcp-runtime.mjs +2 -12
  24. package/bin/package-signature.mjs +0 -4
  25. package/dist/app/index.html +7 -6
  26. package/dist/assets/EXRLoader-CvVIvGVE.js +1 -0
  27. package/dist/assets/analytics-C9sUnndd.js +1 -0
  28. package/dist/assets/app-CCh--1Hc.js +5087 -0
  29. package/dist/assets/app-f-fkV5uP.css +1 -0
  30. package/dist/assets/compile-worker-CkxiD6lH.js +5 -0
  31. package/dist/assets/fflate.module-KbGj5-H6.js +5 -0
  32. package/dist/assets/{first-shot-handoff-BlT33CGf.js → first-shot-handoff-CC4xe4HM.js} +1 -1
  33. package/dist/assets/{landing-BSesrCpL.js → landing-DjHD8u7k.js} +1 -1
  34. package/dist/assets/scene-asset-sync-Hx_FzdOH.js +20 -0
  35. package/dist/assets/scene-asset-sync-_S8NQl5B.css +1 -0
  36. package/dist/assets/workflow-C4yKH1p4.js +179 -0
  37. package/dist/cozyclay-package.json +1 -1
  38. package/dist/index.html +3 -3
  39. package/dist/llms.txt +14 -0
  40. package/dist/sw.js +3 -6
  41. package/dist/workflow/index.html +5 -4
  42. package/mcp/LIVE-PROTOCOL.md +7 -47
  43. package/mcp/README.md +16 -26
  44. package/mcp/live-hub.mjs +6 -10
  45. package/mcp/package.json +1 -3
  46. package/mcp/tool-handlers.mjs +4 -168
  47. package/package.json +3 -22
  48. package/src/App.jsx +174 -2639
  49. package/src/agent-progress.js +79 -0
  50. package/src/agent-turn-mark.js +30 -0
  51. package/src/analytics.js +1 -19
  52. package/src/app-context.js +5 -0
  53. package/src/app-stage.jsx +2 -34
  54. package/src/asset-pane.jsx +6 -5
  55. package/src/camera-block.js +21 -0
  56. package/src/camera-direction.js +120 -0
  57. package/src/character-kind.js +3 -9
  58. package/src/command-bus.js +8 -6
  59. package/src/commands/cast.js +5 -34
  60. package/src/commands/elements/character.js +1 -1
  61. package/src/commands/elements/motion.js +1 -6
  62. package/src/commands/elements/object.js +6 -2
  63. package/src/commands/elements/shot.js +4 -1
  64. package/src/commands/elements.js +38 -14
  65. package/src/commands/index.js +3 -1
  66. package/src/commands/kits.js +37 -0
  67. package/src/commands/motion-timing.js +57 -0
  68. package/src/commands/motion.js +9 -152
  69. package/src/commands/motion3-compile.js +65 -0
  70. package/src/commands/motion3.js +207 -0
  71. package/src/commands/objects.js +6 -4
  72. package/src/commands/shot.js +49 -11
  73. package/src/commands/stage.js +14 -1
  74. package/src/commands/view.js +4 -1
  75. package/src/document-mirrors.js +42 -0
  76. package/src/document-store.js +4 -0
  77. package/src/domains/cast.js +19 -194
  78. package/src/domains/motion/motion-range-pin-rules.js +1 -1
  79. package/src/domains/motion/take-source.js +51 -0
  80. package/src/domains/motion.js +60 -1838
  81. package/src/domains/scenes.js +12 -55
  82. package/src/domains/shots.js +2 -2
  83. package/src/domains/stage.js +4 -2
  84. package/src/draft-ghost.js +46 -0
  85. package/src/draft-ghost.jsx +129 -0
  86. package/src/dualview.jsx +4 -3
  87. package/src/effects/EffectsLayer.jsx +112 -0
  88. package/src/effects/effect-meshes.js +232 -0
  89. package/src/effects/effect-record.js +103 -0
  90. package/src/effects/emitter.js +59 -0
  91. package/src/effects/simulate.js +261 -0
  92. package/src/effects/state.js +59 -0
  93. package/src/execution-telemetry.js +2 -3
  94. package/src/first-success-guide.jsx +5 -5
  95. package/src/ghost-set.js +257 -0
  96. package/src/held-object-edit.jsx +113 -0
  97. package/src/held-object-pose.js +175 -0
  98. package/src/hierarchy-model.js +1 -1
  99. package/src/hierarchy-panel.jsx +1 -1
  100. package/src/imported-mesh.jsx +103 -0
  101. package/src/kits/held.js +84 -0
  102. package/src/kits/index.js +70 -0
  103. package/src/kits/smoking.js +57 -0
  104. package/src/light-blockers.js +93 -0
  105. package/src/motion/beat-board-model.js +84 -0
  106. package/src/motion/beat-board.css +124 -0
  107. package/src/motion/beat-board.jsx +59 -0
  108. package/src/motion/center-of-mass.js +66 -0
  109. package/src/motion/fix-collisions.js +850 -0
  110. package/src/motion/physics-review.js +118 -0
  111. package/src/{ardy → motion}/physics-support.js +1 -76
  112. package/src/motion/rig-snapshot.js +22 -0
  113. package/src/motion/take-segments.jsx +140 -0
  114. package/src/{ardy → motion}/timeline-coordinates.js +0 -21
  115. package/src/{ardy → motion}/timeline.css +1 -2
  116. package/src/{ardy → motion}/timeline.jsx +48 -419
  117. package/src/motion/waypoints.js +76 -0
  118. package/src/motion3/beats.js +83 -0
  119. package/src/motion3/body.js +158 -0
  120. package/src/motion3/bones.js +47 -0
  121. package/src/motion3/carried.js +47 -0
  122. package/src/motion3/compile-job.js +75 -0
  123. package/src/motion3/compile-worker.js +10 -0
  124. package/src/motion3/compiler/anchors.js +105 -0
  125. package/src/motion3/compiler/arm-orient.js +128 -0
  126. package/src/motion3/compiler/channels.js +125 -0
  127. package/src/motion3/compiler/compile.js +205 -0
  128. package/src/motion3/compiler/gait-rhythm.js +97 -0
  129. package/src/motion3/compiler/grip.js +171 -0
  130. package/src/motion3/compiler/head-steady.js +48 -0
  131. package/src/motion3/compiler/hold.js +134 -0
  132. package/src/motion3/compiler/idle.js +113 -0
  133. package/src/motion3/compiler/limbs.js +170 -0
  134. package/src/motion3/compiler/math.js +72 -0
  135. package/src/motion3/compiler/pose.js +162 -0
  136. package/src/motion3/compiler/quat.js +49 -0
  137. package/src/motion3/compiler/skeleton.js +74 -0
  138. package/src/motion3/compiler/validate-common.js +32 -0
  139. package/src/motion3/compiler/validate-goals.js +118 -0
  140. package/src/motion3/compiler/validate-objects.js +143 -0
  141. package/src/motion3/compiler/validate-parts.js +116 -0
  142. package/src/motion3/compiler/validate.js +122 -0
  143. package/src/motion3/compiler/vocab.js +73 -0
  144. package/src/motion3/compiler/walk-pelvis.js +275 -0
  145. package/src/motion3/compiler/walk.js +207 -0
  146. package/src/motion3/compiler/wrist-clamp.js +32 -0
  147. package/src/motion3/delta.js +47 -0
  148. package/src/motion3/describe.js +174 -0
  149. package/src/motion3/docs.js +27 -0
  150. package/src/motion3/edit-carry.js +21 -0
  151. package/src/motion3/fix/ease.js +16 -0
  152. package/src/motion3/fix/ground.js +37 -0
  153. package/src/motion3/fix/index.js +52 -0
  154. package/src/motion3/fix/push.js +71 -0
  155. package/src/motion3/fix/reach.js +46 -0
  156. package/src/motion3/fix/retime.js +79 -0
  157. package/src/motion3/fix/self.js +211 -0
  158. package/src/motion3/fix/slide.js +70 -0
  159. package/src/motion3/fix/turn.js +51 -0
  160. package/src/motion3/follow.js +39 -0
  161. package/src/motion3/free-motion.js +173 -0
  162. package/src/motion3/grip-choice.js +130 -0
  163. package/src/motion3/hold-plan.js +157 -0
  164. package/src/motion3/json-patch.js +77 -0
  165. package/src/motion3/key-aim.js +105 -0
  166. package/src/motion3/key-edit.js +82 -0
  167. package/src/motion3/lenient.js +127 -0
  168. package/src/motion3/object-keys.js +236 -0
  169. package/src/motion3/object-pose.js +182 -0
  170. package/src/motion3/object-tracks.js +12 -0
  171. package/src/motion3/pipeline.js +102 -0
  172. package/src/motion3/playback.js +51 -0
  173. package/src/motion3/probes.js +91 -0
  174. package/src/motion3/qa/body-checks.js +34 -0
  175. package/src/motion3/qa/check-names.js +14 -0
  176. package/src/motion3/qa/checks.js +121 -0
  177. package/src/motion3/qa/contact-checks.js +51 -0
  178. package/src/motion3/qa/explain.js +80 -0
  179. package/src/motion3/qa/fmt.js +5 -0
  180. package/src/motion3/qa/foot-checks.js +72 -0
  181. package/src/motion3/qa/geometry.js +77 -0
  182. package/src/motion3/qa/goals.js +191 -0
  183. package/src/motion3/qa/ground.js +60 -0
  184. package/src/motion3/qa/hints-contact.js +179 -0
  185. package/src/motion3/qa/hints-goal.js +160 -0
  186. package/src/motion3/qa/hints-knee.js +24 -0
  187. package/src/motion3/qa/hints-motion.js +209 -0
  188. package/src/motion3/qa/hints-object.js +91 -0
  189. package/src/motion3/qa/index.js +42 -0
  190. package/src/motion3/qa/key-checks.js +82 -0
  191. package/src/motion3/qa/near.js +96 -0
  192. package/src/motion3/qa/object-checks.js +135 -0
  193. package/src/motion3/qa/patch.js +161 -0
  194. package/src/motion3/qa/severity.js +38 -0
  195. package/src/motion3/qa/spec-checks.js +65 -0
  196. package/src/motion3/qa/style.js +218 -0
  197. package/src/motion3/qa/wrist-checks.js +69 -0
  198. package/src/motion3/qa/wrist-obs.js +60 -0
  199. package/src/motion3/refs.js +120 -0
  200. package/src/motion3/repair-tune.js +121 -0
  201. package/src/motion3/repair.js +83 -0
  202. package/src/motion3/report.js +67 -0
  203. package/src/motion3/rigs/index.js +14 -0
  204. package/src/motion3/rigs/x-bot-tpose-body.json +1 -0
  205. package/src/motion3/rigs/x-bot-tpose.json +1 -0
  206. package/src/motion3/rigs/y-bot-tpose-body.json +1 -0
  207. package/src/motion3/rigs/y-bot-tpose.json +1 -0
  208. package/src/motion3/scene.js +60 -0
  209. package/src/motion3/schema/brief.js +89 -0
  210. package/src/motion3/schema/fields.js +44 -0
  211. package/src/motion3/schema/spec-examples.js +46 -0
  212. package/src/motion3/schema/topics.js +111 -0
  213. package/src/motion3/schema/types.js +147 -0
  214. package/src/motion3/search.js +117 -0
  215. package/src/motion3/second-hand.js +112 -0
  216. package/src/motion3/skills/basic.js +82 -0
  217. package/src/motion3/skills/book.js +76 -0
  218. package/src/motion3/skills/car.js +145 -0
  219. package/src/motion3/skills/frames-auto.js +29 -0
  220. package/src/motion3/skills/index.js +83 -0
  221. package/src/motion3/skills/info.js +65 -0
  222. package/src/motion3/skills/smoke-kit.js +109 -0
  223. package/src/motion3/skills/smoke.js +160 -0
  224. package/src/motion3/spec-store.js +59 -0
  225. package/src/motion3/take-edit.js +155 -0
  226. package/src/motion3/take.js +63 -0
  227. package/src/motion3/timeline.js +127 -0
  228. package/src/motion3/views.js +121 -0
  229. package/src/motion3/walk-plan.js +119 -0
  230. package/src/object-catalog.jsx +7 -1
  231. package/src/object-colors.js +98 -0
  232. package/src/object-gizmo-parts.js +115 -0
  233. package/src/object-gizmo.jsx +37 -122
  234. package/src/object-glow.js +139 -0
  235. package/src/object-hidden.js +33 -0
  236. package/src/object-look.js +196 -0
  237. package/src/object-size.js +10 -0
  238. package/src/object-travel.js +71 -6
  239. package/src/panels/CameraPanel.jsx +7 -7
  240. package/src/panels/HdrEnvironmentControls.jsx +59 -0
  241. package/src/panels/LightPanel.jsx +25 -4
  242. package/src/panels/ObjectLookPanel.jsx +88 -0
  243. package/src/panels/PoseImagePanel.jsx +1 -1
  244. package/src/panels/PosePanel.jsx +2 -17
  245. package/src/panels/RigControlPanel.jsx +5 -53
  246. package/src/panels/SubjectBox.jsx +4 -1
  247. package/src/panels/details.css +14 -0
  248. package/src/posestudio.jsx +4 -27
  249. package/src/project-browser.jsx +3 -4
  250. package/src/project-resources.js +18 -31
  251. package/src/project.js +13 -104
  252. package/src/props.jsx +25 -280
  253. package/src/rail-dot-drag.js +23 -0
  254. package/src/render-passes.js +10 -0
  255. package/src/resource-status.jsx +1 -0
  256. package/src/result-modal.jsx +4 -19
  257. package/src/room.jsx +20 -12
  258. package/src/scene-assets.js +8 -6
  259. package/src/scene-light-place.js +72 -0
  260. package/src/scene-lights.jsx +148 -0
  261. package/src/scene-objects.js +31 -139
  262. package/src/scenes.js +15 -105
  263. package/src/semantic-edit.js +2 -5
  264. package/src/set-pieces.jsx +183 -0
  265. package/src/settings-menu.jsx +3 -11
  266. package/src/shell/BottomDock.jsx +11 -53
  267. package/src/shell/CompositionGuideButton.jsx +20 -0
  268. package/src/shell/DetailsSlot.jsx +16 -200
  269. package/src/shell/GhostsMenuGroup.jsx +74 -0
  270. package/src/shell/LibrarySlot.jsx +1 -3
  271. package/src/shell/MenuBar.jsx +3 -3
  272. package/src/shell/PreferencesDialog.jsx +15 -264
  273. package/src/shell/PreferencesSlot.jsx +2 -8
  274. package/src/shell/ShotPreviewHeader.jsx +17 -0
  275. package/src/shell/TopBar.jsx +5 -83
  276. package/src/shell/ViewportToolbar.jsx +23 -93
  277. package/src/shell/agent-glass.css +19 -6
  278. package/src/shell/details-selection.js +42 -0
  279. package/src/shell/ghosts-menu.css +22 -0
  280. package/src/shell/glass-regions.css +29 -3
  281. package/src/shell/preferences.css +6 -1
  282. package/src/shell/viewport.css +12 -0
  283. package/src/shot-authoring.js +4 -7
  284. package/src/soft-shadows.js +37 -0
  285. package/src/stage-attach-frames.js +12 -12
  286. package/src/stage-canvas-rigs.jsx +3 -3
  287. package/src/stage-capture.jsx +42 -8
  288. package/src/stage-characters.jsx +22 -216
  289. package/src/stage-editor-gizmos.jsx +6 -6
  290. package/src/stage-environment.js +93 -0
  291. package/src/stage-hdr-builtins.js +107 -0
  292. package/src/stage-hdr-image.js +116 -0
  293. package/src/stage-hdr-library.js +70 -0
  294. package/src/stage-hdr-map.js +94 -0
  295. package/src/stage-hdr.js +130 -0
  296. package/src/stage-lighting.js +149 -0
  297. package/src/stage-occlusion.js +210 -0
  298. package/src/stage-sky.jsx +159 -0
  299. package/src/stage-startup-defaults.js +5 -9
  300. package/src/studio-actions.js +12 -14
  301. package/src/studio-agent-commands.js +46 -16
  302. package/src/studio-agent-motion.js +9 -9
  303. package/src/studio-agent-protocol.js +46 -73
  304. package/src/studio-app-binding.js +29 -45
  305. package/src/studio-asset-list.js +17 -0
  306. package/src/studio-contact-sheet.js +37 -5
  307. package/src/studio-elements.js +27 -7
  308. package/src/studio-entity-bounds.js +18 -0
  309. package/src/studio-error-codes.js +8 -0
  310. package/src/studio-export-framing.js +152 -0
  311. package/src/studio-lighting-check.js +103 -0
  312. package/src/studio-motion-visual.js +150 -0
  313. package/src/studio-schema-errors.js +80 -0
  314. package/src/studio-support-gap.js +74 -0
  315. package/src/styles.css +1 -19
  316. package/src/timeline-defaults.js +5 -0
  317. package/src/timeline-extent.js +8 -20
  318. package/src/tutorial-walk.js +17 -0
  319. package/src/ui-scrub.js +1 -1
  320. package/src/use-shot-camera-tracks.js +12 -0
  321. package/src/view-ghosts.js +97 -0
  322. package/src/view-ghosts.jsx +49 -0
  323. package/src/workflow/AgentPanel.jsx +47 -166
  324. package/src/workflow/AgentProgressCard.jsx +107 -0
  325. package/src/workflow/AgentReplyText.jsx +37 -0
  326. package/src/workflow/AgentTurnSteps.jsx +84 -0
  327. package/src/workflow/LoginPanel.jsx +140 -0
  328. package/src/workflow/WorkflowBuilder.jsx +7 -23
  329. package/src/workflow/agent-client.js +54 -51
  330. package/src/workflow/agent-panel.css +71 -1
  331. package/src/workflow/agent-progress-card.css +167 -0
  332. package/src/workflow/agent-progress-view.js +47 -0
  333. package/src/workflow/agent-turn-steps.css +113 -0
  334. package/src/workflow/agent-turn.js +191 -0
  335. package/src/workflow/login-client.js +34 -0
  336. package/src/workflow/motion-input.js +18 -31
  337. package/src/workflow/node-schema.js +1 -1
  338. package/src/workflow/reply-markdown.js +69 -0
  339. package/src/workflow/scene-asset-sync.js +8 -8
  340. package/tools/agent-session-stats.mjs +61 -0
  341. package/tools/dev-full.mjs +5 -56
  342. package/tools/eslint-baseline.json +12 -13
  343. package/tools/headless-studio.mjs +85 -0
  344. package/tools/knip-baseline.json +0 -179
  345. package/tools/loc-baseline.json +33 -83
  346. package/tools/motion3/extract-body.mjs +103 -0
  347. package/tools/motion3/extract-rig.mjs +85 -0
  348. package/tools/process-supervisor.mjs +0 -88
  349. package/tools/run-tests.mjs +9 -124
  350. package/bin/agent/motion-runtime.mjs +0 -76
  351. package/dist/ardy/cskel27-rest.json +0 -557
  352. package/dist/assets/analytics-B41FmPYI.js +0 -1
  353. package/dist/assets/app-Do-Og5Pu.css +0 -1
  354. package/dist/assets/app-DwP-tl2x.js +0 -4920
  355. package/dist/assets/config-Bx7pYsD9.js +0 -1
  356. package/dist/assets/config-DeoFTYzb.css +0 -1
  357. package/dist/assets/demo-Dt0qZfS7.js +0 -1
  358. package/dist/assets/scene-asset-sync-BCWDJB1d.js +0 -19
  359. package/dist/assets/scene-asset-sync-DjJ_cJ2R.css +0 -1
  360. package/dist/assets/ticket-jqMhqL88.js +0 -1
  361. package/dist/assets/workflow-Br-K7Srs.js +0 -179
  362. package/dist/demo/walk-then-stop.npz +0 -0
  363. package/mcp/ardy-prompts.mjs +0 -218
  364. package/src/ardy/auto-fix-panel.css +0 -108
  365. package/src/ardy/auto-physics.js +0 -1194
  366. package/src/ardy/client.js +0 -153
  367. package/src/ardy/convert.js +0 -313
  368. package/src/ardy/cskel27-neutral.js +0 -39
  369. package/src/ardy/cskel27.js +0 -77
  370. package/src/ardy/export.js +0 -165
  371. package/src/ardy/fix-collisions.js +0 -2257
  372. package/src/ardy/foot-lock.js +0 -230
  373. package/src/ardy/motion-calibration.js +0 -118
  374. package/src/ardy/motion-edit.js +0 -213
  375. package/src/ardy/motion-url.js +0 -55
  376. package/src/ardy/mp4-duration.js +0 -102
  377. package/src/ardy/npz.js +0 -595
  378. package/src/ardy/physics-panel.css +0 -21
  379. package/src/ardy/physics-panel.jsx +0 -89
  380. package/src/ardy/physics-refine.js +0 -111
  381. package/src/ardy/physics-review.js +0 -567
  382. package/src/ardy/physics-temporal.js +0 -54
  383. package/src/ardy/platform-fit-panel.css +0 -64
  384. package/src/ardy/platform-fit-panel.jsx +0 -41
  385. package/src/ardy/platform-fit.js +0 -326
  386. package/src/ardy/playback.js +0 -711
  387. package/src/ardy/pose-pin.js +0 -97
  388. package/src/ardy/prompt-clips.js +0 -30
  389. package/src/ardy/retime.js +0 -211
  390. package/src/ardy/root-drop.js +0 -369
  391. package/src/ardy/timeline-resize.js +0 -13
  392. package/src/ardy/to-cskel27.js +0 -225
  393. package/src/ardy/trim.js +0 -33
  394. package/src/ardy/waypoints.js +0 -369
  395. package/src/ardy-bridge-contract.js +0 -281
  396. package/src/domains/motion/motion-video-capture.js +0 -664
  397. package/src/i2v-motion-client.js +0 -77
  398. package/src/i2v-motion-studio.jsx +0 -180
  399. package/src/line-edit.js +0 -1348
  400. package/src/motion/generation.js +0 -105
  401. package/src/motion-readiness-ui.jsx +0 -67
  402. package/src/motion-readiness.js +0 -40
  403. package/src/motion-resources.js +0 -177
  404. package/src/motion-store.js +0 -65
  405. package/src/motion-trail.js +0 -601
  406. package/src/multimodel-ingest.js +0 -394
  407. package/src/panels/PromptBlocksPanel.jsx +0 -444
  408. package/src/panels/VideoCapturePanel.jsx +0 -181
  409. package/src/pose-extract/browser.js +0 -42
  410. package/src/pose-extract/detector.js +0 -100
  411. package/src/pose-extract/fit.js +0 -227
  412. package/src/pose-extract/image-frame.js +0 -51
  413. package/src/pose-extract/index.js +0 -58
  414. package/src/pose-extract/keyframes.js +0 -74
  415. package/src/pose-extract/landmarks.js +0 -162
  416. package/src/pose-extract/mirror.js +0 -153
  417. package/src/pose-extract/take.js +0 -131
  418. package/src/pose-extract/video-frames.js +0 -91
  419. package/src/take-recipe.js +0 -165
  420. package/src/trail-key-conflicts.js +0 -39
  421. package/src/trail-pick.js +0 -59
  422. package/src/use-i2v-capture-modal.js +0 -15
  423. package/tools/ardy/BRIDGE.md +0 -37
  424. package/tools/ardy/GVHMR-NPZ.md +0 -68
  425. package/tools/ardy/README.md +0 -124
  426. package/tools/ardy/__pycache__/cclay_gvhmr_worker.cpython-313.pyc +0 -0
  427. package/tools/ardy/artifacts.mjs +0 -34
  428. package/tools/ardy/bridge.mjs +0 -1732
  429. package/tools/ardy/bvh-cskel27.mjs +0 -1265
  430. package/tools/ardy/cclay_constrained_generate.py +0 -561
  431. package/tools/ardy/cclay_gvhmr_worker.py +0 -264
  432. package/tools/ardy/cclay_motion_edit.py +0 -510
  433. package/tools/ardy/cclay_sequence_generate.py +0 -705
  434. package/tools/ardy/cclay_worker.py +0 -304
  435. package/tools/ardy/dump-npz.py +0 -205
  436. package/tools/ardy/extract-rest.mjs +0 -299
  437. package/tools/ardy/extract.mjs +0 -507
  438. package/tools/ardy/footage.mjs +0 -467
  439. package/tools/ardy/gvhmr-floor.mjs +0 -102
  440. package/tools/ardy/gvhmr-to-cskel27.mjs +0 -21
  441. package/tools/ardy/gvhmr_fastpath.py +0 -168
  442. package/tools/ardy/gvhmr_trajectory.py +0 -240
  443. package/tools/ardy/merge-text-encoder.py +0 -160
  444. package/tools/ardy/mocap-metrics.mjs +0 -200
  445. package/tools/ardy/mocap-quality-gate.mjs +0 -110
  446. package/tools/ardy/motion-stabilize.mjs +0 -348
  447. package/tools/ardy/npz.mjs +0 -414
  448. package/tools/ardy/pose-to-npz.mjs +0 -106
  449. package/tools/ardy/prompt-limits.mjs +0 -5
  450. package/tools/ardy/run-edit-on-box.sh +0 -73
  451. package/tools/ardy/run-local.mjs +0 -233
  452. package/tools/ardy/run-on-box.sh +0 -593
  453. package/tools/ardy/run-sequence-on-box.sh +0 -177
  454. package/tools/ardy/runners/gvhmr-worker.mjs +0 -206
  455. package/tools/ardy/runners/index.mjs +0 -18
  456. package/tools/ardy/runners/local.mjs +0 -455
  457. package/tools/ardy/runners/proc.mjs +0 -125
  458. package/tools/ardy/runners/remote.mjs +0 -252
  459. package/tools/ardy/setup-local.mjs +0 -189
  460. package/tools/ardy/setup-text-encoder-on-box.sh +0 -93
  461. package/tools/ardy/setup-text-encoder.py +0 -306
  462. package/tools/ardy/smpl-cskel27.mjs +0 -178
  463. package/tools/ardy/visual-qa.mjs +0 -196
  464. package/tools/ardy/vq-car.mjs +0 -52
  465. package/tools/bench/EXP3.md +0 -61
  466. package/tools/bench/cclay_bench_extract_incam.py +0 -125
  467. package/tools/bench/cclay_bench_extract_obs.py +0 -204
  468. package/tools/bench/cclay_bench_runner.py +0 -42
  469. package/tools/bench/cube-contact.mjs +0 -162
  470. package/tools/bench/exp3.mjs +0 -152
  471. package/tools/bench/extract-bench-lib.mjs +0 -216
  472. package/tools/bench/extract-bench.mjs +0 -302
  473. package/tools/bench/fal-generate.mjs +0 -88
  474. package/tools/bench/fit/README.md +0 -189
  475. package/tools/bench/fit/camera.mjs +0 -59
  476. package/tools/bench/fit/contact.mjs +0 -419
  477. package/tools/bench/fit/footlock.mjs +0 -234
  478. package/tools/bench/fit/motion.mjs +0 -85
  479. package/tools/bench/fit/pin.mjs +0 -27
  480. package/tools/bench/fit/remote.mjs +0 -105
  481. package/tools/bench/fit-bench.mjs +0 -126
  482. package/tools/bench/fit-sanity.mjs +0 -45
  483. package/tools/bench/metrics.mjs +0 -313
  484. package/tools/bench/obs/depth.mjs +0 -258
  485. package/tools/bench/obs/extrinsics.mjs +0 -175
  486. package/tools/bench/obs/fit_mannequin_betas.py +0 -340
  487. package/tools/bench/obs/ground.mjs +0 -294
  488. package/tools/bench/obs/heading.mjs +0 -151
  489. package/tools/bench/obs/ladder.mjs +0 -555
  490. package/tools/bench/obs/mannequin-betas.json +0 -124
  491. package/tools/bench/obs/remote.mjs +0 -83
  492. package/tools/bench/obs/rest_joints.py +0 -52
  493. package/tools/bench/obs/ybot-targets.mjs +0 -49
  494. package/tools/bench/obs-bench.mjs +0 -520
  495. package/tools/bench/score.mjs +0 -343
  496. package/tools/bench/summarize.mjs +0 -86
  497. package/tools/demo-worker/README.md +0 -137
  498. package/tools/demo-worker/api-client.mjs +0 -233
  499. package/tools/demo-worker/generate.mjs +0 -301
  500. package/tools/demo-worker/index.mjs +0 -466
  501. package/tools/demo-worker/sign-core.mjs +0 -112
  502. package/tools/demo-worker/sign.mjs +0 -161
  503. package/tools/gt-render/browser.mjs +0 -159
  504. package/tools/gt-render/camera-math.mjs +0 -276
  505. package/tools/gt-render/page.mjs +0 -267
  506. package/tools/gt-render/render.mjs +0 -514
  507. package/tools/gt-render/scene-box.mjs +0 -31
  508. package/tools/gt-render/take-transform.mjs +0 -87
  509. package/tools/kimodo/compare-seam.mjs +0 -108
  510. package/tools/kimodo/constraints.mjs +0 -244
  511. package/tools/kimodo/edit.mjs +0 -259
  512. package/tools/kimodo/effector-constraints.mjs +0 -200
  513. package/tools/kimodo/generate.mjs +0 -646
  514. package/tools/kimodo/local-output.mjs +0 -265
  515. package/tools/kimodo/measure-pose.mjs +0 -65
  516. package/tools/kimodo/measure-preserve.mjs +0 -899
  517. package/tools/kimodo/measure-waypoints.mjs +0 -55
  518. package/tools/kimodo/mlx-generate.py +0 -111
  519. package/tools/kimodo/pose-constraints.mjs +0 -248
  520. package/tools/kimodo/preserve-mask.mjs +0 -543
  521. package/tools/kimodo/read-npz.mjs +0 -189
  522. package/tools/kimodo/run-edit-on-box.mjs +0 -155
  523. package/tools/kimodo/run-sequence-on-box.mjs +0 -178
  524. package/tools/kimodo/runner.mjs +0 -284
  525. package/tools/kimodo/seam-batch.mjs +0 -77
  526. package/tools/kimodo/setup-local.mjs +0 -30
  527. package/tools/kimodo/setup-on-box.sh +0 -339
  528. package/tools/kimodo/soma77-to-cskel27.mjs +0 -305
  529. package/tools/morphgs/assets/gen_truth.py +0 -39
  530. package/tools/morphgs/assets/mesh_ori_rig.txt +0 -27
  531. package/tools/morphgs/assets/playback-check.mjs +0 -30
  532. package/tools/morphgs/demo-gate.sh +0 -26
  533. package/tools/morphgs/fbx2morphgs.mjs +0 -68
  534. package/tools/morphgs/morphgs-to-cskel27.mjs +0 -105
  535. package/tools/morphgs/patches/preprocess_src-none-mode.patch +0 -76
  536. package/tools/morphgs/setup-on-cluster.sh +0 -126
  537. package/tools/projflow/driver.py +0 -1069
  538. package/tools/projflow/generate.mjs +0 -805
  539. package/tools/projflow/hml22-to-cskel27.mjs +0 -735
  540. package/tools/projflow/line-edit-job.mjs +0 -547
  541. package/tools/projflow/replay.mjs +0 -452
  542. package/tools/projflow/runner.mjs +0 -269
  543. package/tools/projflow/service.mjs +0 -742
  544. package/tools/track/DESIGN.md +0 -513
  545. package/tools/track/backfill-provenance.mjs +0 -116
  546. package/tools/track/budget.mjs +0 -8
  547. package/tools/track/check-rig.mjs +0 -209
  548. package/tools/track/diagnostics.schema.json +0 -60
  549. package/tools/track/export-rig.mjs +0 -256
  550. package/tools/track/fallback.mjs +0 -7
  551. package/tools/track/fk-parity-fixture.mjs +0 -161
  552. package/tools/track/gate.mjs +0 -349
  553. package/tools/track/masks.mjs +0 -318
  554. package/tools/track/metrics.mjs +0 -201
  555. package/tools/track/publish-obs.mjs +0 -108
  556. package/tools/track/py/check_env.py +0 -61
  557. package/tools/track/py/eval_lr.py +0 -229
  558. package/tools/track/py/lr_viterbi.py +0 -294
  559. package/tools/track/py/masks.py +0 -405
  560. package/tools/track/py/objective.py +0 -382
  561. package/tools/track/py/rig.py +0 -508
  562. package/tools/track/py/scene.py +0 -326
  563. package/tools/track/py/test_joint_indices.py +0 -123
  564. package/tools/track/py/test_lr_viterbi.py +0 -195
  565. package/tools/track/py/test_masks.py +0 -229
  566. package/tools/track/py/test_rig.py +0 -288
  567. package/tools/track/py/test_scene.py +0 -354
  568. package/tools/track/py/test_track.py +0 -328
  569. package/tools/track/py/track.py +0 -411
  570. package/tools/track/remote.mjs +0 -140
  571. package/tools/track/rig-dump.mjs +0 -227
  572. package/tools/track/run-box-tests.mjs +0 -14
  573. package/tools/track/run-box.mjs +0 -168
  574. package/tools/track/setup-box.sh +0 -59
  575. package/tools/track/study-2d.mjs +0 -728
  576. /package/src/{ardy → motion}/collision-blockers.js +0 -0
  577. /package/src/{ardy → motion}/ground.js +0 -0
  578. /package/src/{ardy → motion}/ik-drag.js +0 -0
  579. /package/src/{ardy → motion}/ik-key-json.js +0 -0
  580. /package/src/{ardy → motion}/ik.js +0 -0
  581. /package/src/{ardy → motion}/physics-surface.js +0 -0
  582. /package/src/{ardy → motion}/playback-clock.js +0 -0
  583. /package/src/{ardy → motion}/range-pin.js +0 -0
@@ -0,0 +1,135 @@
1
+ // CLIProxyAPI, the one provider CozyClay adds to pi's built-ins (moved out of providers.mjs for #pi-login).
2
+ // Its login is pi's own api-key login shape: the flow asks for the server address and then the key, and
3
+ // returns one api-key credential that carries the address as provider env (CLIPROXY_BASE_URL), so an app
4
+ // opened from the Dock needs no shell variable. The address is read when the registry is built: the
5
+ // environment wins, then the stored credential's env, then the default local port.
6
+
7
+ const CLIPROXY_DEFAULT_BASE = "http://127.0.0.1:8317";
8
+ const BASE_ENV = "CLIPROXY_BASE_URL";
9
+ const KEY_ENV = "CLIPROXY_API_KEY";
10
+
11
+ /** A server address as typed, cleaned: http(s), no trailing slash, no trailing /v1 (the models add it). */
12
+ function cleanBase(value) {
13
+ const url = new URL(String(value ?? "").trim());
14
+ if (!["http:", "https:"].includes(url.protocol)) throw new Error("The server address must start with http:// or https://.");
15
+ return url.href.replace(/\/+$/, "").replace(/\/v1$/, "");
16
+ }
17
+
18
+ /** The address the registry builds CLIProxyAPI's models on: environment, then the stored credential, then the default. */
19
+ export function cliproxyBase({ env = process.env, credential } = {}) {
20
+ const fromEnv = typeof env?.[BASE_ENV] === "string" ? env[BASE_ENV].trim() : "";
21
+ return (fromEnv || credential?.env?.[BASE_ENV] || CLIPROXY_DEFAULT_BASE).replace(/\/$/, "");
22
+ }
23
+
24
+ /** pi ApiKeyAuth: login asks for the address, then the key; resolve prefers the stored key, then CLIPROXY_API_KEY. */
25
+ function cliproxyAuth() {
26
+ return {
27
+ name: "CLIProxyAPI server and key",
28
+ login: async (interaction) => {
29
+ interaction.signal.throwIfAborted();
30
+ const address = await interaction.prompt({ type: "text", message: "CLIProxyAPI server address", placeholder: CLIPROXY_DEFAULT_BASE });
31
+ const base = cleanBase(address.trim() || CLIPROXY_DEFAULT_BASE);
32
+ interaction.signal.throwIfAborted();
33
+ const key = await interaction.prompt({ type: "secret", message: "CLIProxyAPI key" });
34
+ interaction.signal.throwIfAborted();
35
+ return { type: "api_key", key, env: { [BASE_ENV]: base } };
36
+ },
37
+ resolve: async ({ ctx, credential, signal }) => {
38
+ signal.throwIfAborted();
39
+ if (credential?.key) return { auth: { apiKey: credential.key }, env: credential.env, source: "stored credential" };
40
+ const key = await ctx.env(KEY_ENV);
41
+ return key ? { auth: { apiKey: key }, source: KEY_ENV } : undefined;
42
+ },
43
+ };
44
+ }
45
+
46
+ export async function cliproxyProvider({ baseUrl, env = process.env } = {}) {
47
+ const base = (baseUrl ?? cliproxyBase({ env })).replace(/\/$/, "");
48
+ const [{ createProvider }, { openaiProvider }, { anthropicProvider }, openaiResponsesApi, anthropicMessagesApi] = await Promise.all([
49
+ import("@earendil-works/pi-ai"),
50
+ import("@earendil-works/pi-ai/providers/openai"),
51
+ import("@earendil-works/pi-ai/providers/anthropic"),
52
+ import("@earendil-works/pi-ai/api/openai-responses"),
53
+ import("@earendil-works/pi-ai/api/anthropic-messages"),
54
+ ]);
55
+ const ids = new Set();
56
+ const models = [
57
+ ...openaiProvider().getModels().map((model) => ({ ...model, provider: "cliproxy", baseUrl: `${base}/v1` })),
58
+ ...anthropicProvider().getModels().map((model) => ({
59
+ ...model,
60
+ provider: "cliproxy",
61
+ baseUrl: base,
62
+ // CLIProxyAPI's /v1/messages rejects messages[].output_config and thinking.block_binding, so keep
63
+ // adaptive thinking (forceAdaptiveThinking) but drop the mid-conversation output_config shape.
64
+ compat: { ...model.compat, supportsMidConvoEffort: false },
65
+ })),
66
+ ].filter((model) => !ids.has(model.id) && ids.add(model.id));
67
+ return createProvider({
68
+ id: "cliproxy",
69
+ name: "CLIProxyAPI",
70
+ baseUrl: base,
71
+ auth: { apiKey: cliproxyAuth() },
72
+ models,
73
+ api: { "openai-responses": openaiResponsesApi, "anthropic-messages": anthropicMessagesApi },
74
+ });
75
+ }
76
+
77
+ /** The proxy's own /v1/models (id -> owner), fetched once per registry; a failed fetch is asked again next time. */
78
+ export async function registryLiveCliproxy(models, apiKey, state) {
79
+ if (!state.cliproxyFetch) {
80
+ state.cliproxyFetch = Promise.resolve().then(async () => {
81
+ const provider = models?.getProvider?.("cliproxy");
82
+ const model = provider?.getModels?.().find((entry) => entry.api === "openai-responses");
83
+ if (!model?.baseUrl || !apiKey) throw new Error("CLIProxyAPI is not configured");
84
+ const response = await fetch(`${model.baseUrl}/models`, {
85
+ headers: { Authorization: `Bearer ${apiKey}` },
86
+ signal: AbortSignal.timeout(5000),
87
+ });
88
+ if (!response.ok) throw new Error(`CLIProxyAPI models request failed: ${response.status}`);
89
+ const body = await response.json();
90
+ // id -> owned_by: the owner is how CLIProxyAPI routes a model (openai, anthropic, or an
91
+ // openai-compatibility upstream name), so it picks the wire format for models pi does not know.
92
+ return new Map((Array.isArray(body?.data) ? body.data : [])
93
+ .map((entry) => typeof entry === "string" ? [entry, undefined] : [entry?.id, typeof entry?.owned_by === "string" ? entry.owned_by : undefined])
94
+ .filter(([id]) => id));
95
+ }).catch(() => { state.cliproxyFetch = null; return null; });
96
+ }
97
+ return state.cliproxyFetch;
98
+ }
99
+
100
+ const CLIPROXY_NON_CHAT = /^gpt-image-|-auto-review$/;
101
+
102
+ /** A live CLIProxyAPI model pi's catalogue does not know yet (#400 follow-up), built on the newest
103
+ * catalogue model of the same family and wire format: `owned_by: openai` rides openai-responses,
104
+ * `owned_by: anthropic` rides anthropic-messages. Other owners (openai-compatibility upstreams such as
105
+ * opencode-go) are left out: they need upstream-specific headers the proxy does not pass through. */
106
+ export function cliproxyLiveModel(id, owner, knownModels) {
107
+ if (CLIPROXY_NON_CHAT.test(id)) return null;
108
+ const api = owner === "openai" ? "openai-responses" : owner === "anthropic" ? "anthropic-messages" : null;
109
+ if (!api) return null;
110
+ const candidates = knownModels.filter((model) => model.api === api);
111
+ const family = owner === "anthropic" ? id.match(/^claude-[a-z]+/)?.[0] : id.match(/^gpt-\d+/)?.[0];
112
+ const sameFamily = family ? candidates.filter((model) => model.id.startsWith(family)) : [];
113
+ const template = [...(sameFamily.length ? sameFamily : candidates)].sort((a, b) => b.id.localeCompare(a.id, undefined, { numeric: true }))[0];
114
+ if (!template) return null;
115
+ // The 5-5 claude generation rejects thinking {type:"disabled"}; a null "off" makes pi-ai send no thinking block (#723).
116
+ const generation = owner === "anthropic" ? id.match(/-(\d+)-(\d+)$/) : null;
117
+ if (generation && Number(generation[1]) >= 5) return { ...template, id, name: id, thinkingLevelMap: { ...template.thinkingLevelMap, off: null } };
118
+ return { ...template, id, name: id };
119
+ }
120
+
121
+ export function extendCliproxyProvider(provider, live, state) {
122
+ if (state.extendedProviders.has(provider)) return false;
123
+ const getModels = provider.getModels.bind(provider);
124
+ const knownModels = getModels();
125
+ const known = new Set(knownModels.map((model) => model.id));
126
+ const additions = [...live]
127
+ .filter(([id]) => !known.has(id))
128
+ .map(([id, owner]) => cliproxyLiveModel(id, owner, knownModels))
129
+ .filter(Boolean)
130
+ .sort((a, b) => b.id.localeCompare(a.id, undefined, { numeric: true }));
131
+ if (!additions.length) return false;
132
+ provider.getModels = () => [...getModels(), ...additions];
133
+ state.extendedProviders.add(provider);
134
+ return true;
135
+ }
@@ -1,4 +1,5 @@
1
1
  import { PROVIDERS } from "./providers.mjs";
2
+ import { readCredentials, removeCredential, setCredential } from "./provider-keys.mjs";
2
3
 
3
4
  const codexId = "openai-codex";
4
5
  const apiProvider = (id) => PROVIDERS.find((provider) => provider.id === id);
@@ -44,7 +45,12 @@ function rawOAuth(credential) {
44
45
  };
45
46
  }
46
47
 
47
- export function createCredentialStore({ auth, keys, env = process.env }) {
48
+ // pi's CredentialStore for every provider pi can log in to (#pi-login): ChatGPT stays in codex-auth's file (its
49
+ // id_token carries the plan and account the image routes need), a bare api key in providers.json, and what pi's
50
+ // login flows return beyond that (OAuth tokens, api keys with provider env) in auth.json. A credential that
51
+ // lands in one file is removed from the other, so one provider never has two.
52
+
53
+ export function createCredentialStore({ auth, keys, env = process.env, credentialFile = { read: readCredentials, set: setCredential, remove: removeCredential } }) {
48
54
  const configured = (id) => apiProvider(id);
49
55
  return {
50
56
  async read(providerId, { signal } = {}) {
@@ -54,8 +60,11 @@ export function createCredentialStore({ auth, keys, env = process.env }) {
54
60
  return mapOAuth(stored);
55
61
  }
56
62
  const provider = configured(providerId);
57
- if (!provider) return undefined;
58
- const key = envValue(provider, env) ?? keys.readKeys()[providerId];
63
+ const fromEnv = provider && envValue(provider, env);
64
+ if (fromEnv) return { type: "api_key", key: fromEnv };
65
+ const credential = credentialFile.read()[providerId];
66
+ if (credential) return credential;
67
+ const key = keys.readKeys()[providerId];
59
68
  return key ? { type: "api_key", key } : undefined;
60
69
  },
61
70
  async list({ signal } = {}) {
@@ -68,8 +77,13 @@ export function createCredentialStore({ auth, keys, env = process.env }) {
68
77
  if (stored) result.push({ providerId: provider.id, type: "oauth", source: "chatgpt" });
69
78
  continue;
70
79
  }
71
- const source = envValue(provider, env) ? "env" : saved[provider.id] ? "file" : null;
72
- if (source) result.push({ providerId: provider.id, type: "api_key", source });
80
+ const source = envValue(provider, env) ? "env" : saved[provider.id] || credentialFile.read()[provider.id] ? "file" : null;
81
+ if (source) result.push({ providerId: provider.id, type: credentialFile.read()[provider.id]?.type ?? "api_key", source });
82
+ }
83
+ // providers outside the curated list that a pi login stored
84
+ const listed = new Set(result.map((entry) => entry.providerId));
85
+ for (const [providerId, credential] of Object.entries({ ...Object.fromEntries(Object.keys(saved).map((id) => [id, { type: "api_key" }])), ...credentialFile.read() })) {
86
+ if (!listed.has(providerId)) result.push({ providerId, type: credential.type, source: "file" });
73
87
  }
74
88
  return result;
75
89
  },
@@ -79,14 +93,23 @@ export function createCredentialStore({ auth, keys, env = process.env }) {
79
93
  const next = await fn(current);
80
94
  if (next === undefined) return current;
81
95
  if (providerId === codexId) await auth.writeStored(rawOAuth(next));
82
- else keys.setKey(providerId, next.key);
96
+ else if (next.type === "api_key" && !next.env) {
97
+ keys.setKey(providerId, next.key);
98
+ credentialFile.remove(providerId);
99
+ } else {
100
+ credentialFile.set(providerId, next);
101
+ if (keys.readKeys()[providerId]) keys.removeKey(providerId);
102
+ }
83
103
  return next;
84
104
  }, signal);
85
105
  },
86
106
  async delete(providerId, { signal } = {}) {
87
107
  await serialized(providerId, async () => {
88
108
  if (providerId === codexId) await auth.logout();
89
- else if (configured(providerId)) keys.removeKey(providerId);
109
+ else {
110
+ if (keys.readKeys()[providerId]) keys.removeKey(providerId);
111
+ credentialFile.remove(providerId);
112
+ }
90
113
  }, signal);
91
114
  },
92
115
  };
@@ -1,6 +1,7 @@
1
1
  import { Value } from "typebox/value";
2
2
  import { STUDIO_TOOL_LABELS } from "../../src/studio-agent-protocol.js";
3
3
  import { toTypeBox } from "./schema-to-typebox.mjs";
4
+ import { toolArgumentPreparer } from "./tool-arguments.mjs";
4
5
 
5
6
 
6
7
  function dataUrlImage(dataUrl) {
@@ -28,12 +29,14 @@ function formatToolError(error) {
28
29
  /** Convert CozyClay's handler catalogue into pi AgentTools. */
29
30
  export function toAgentTools(tools, { emit, signal, labels = STUDIO_TOOL_LABELS } = {}) {
30
31
  return (Array.isArray(tools) ? tools : []).map((tool) => {
31
- const parameters = toTypeBox(tool.parameters || { type: "object", properties: {}, additionalProperties: false });
32
+ const schema = tool.parameters || { type: "object", properties: {}, additionalProperties: false }, parameters = toTypeBox(schema);
32
33
  return {
33
34
  name: tool.name,
34
35
  label: labels?.[tool.name] ?? tool.label ?? tool.name.replaceAll("_", " "),
35
36
  description: tool.description || tool.name,
36
37
  parameters,
38
+ // Before pi's own check: array texts parsed, unknown fields named (tool-arguments.mjs).
39
+ prepareArguments: toolArgumentPreparer(tool.name, schema),
37
40
  // pi's low-level `Agent` calls `execute(id, params, signal, onUpdate)`;
38
41
  // pi's `AgentHarness` calls `execute(id, params, onUpdate, toolContext,
39
42
  // invocation, context)` where `context.abortSignal` is the REAL abort
@@ -54,3 +54,35 @@ export function removeKey(id) {
54
54
  return next;
55
55
  }
56
56
 
57
+ // Credentials pi's login flows return that are more than a bare key (#pi-login): OAuth tokens (Claude Pro/Max,
58
+ // GitHub Copilot, xAI, Kimi, OpenRouter) and api-key credentials that carry provider env (CLIProxyAPI's server
59
+ // address). They are kept as pi's canonical Credential, one per provider id, in auth.json beside providers.json;
60
+ // a bare api key stays in providers.json as before. credential-store.mjs decides which file a write goes to.
61
+ function authPath() {
62
+ return join(configDir(), "auth.json");
63
+ }
64
+
65
+ /** { [providerId]: Credential } from auth.json; a missing or unreadable file is {}. */
66
+ export function readCredentials() {
67
+ try {
68
+ const value = JSON.parse(readFileSync(authPath(), "utf8"));
69
+ if (!value || typeof value !== "object" || Array.isArray(value)) return {};
70
+ return Object.fromEntries(Object.entries(value).filter(([id, entry]) => id && entry && typeof entry === "object" && ["api_key", "oauth"].includes(entry.type)));
71
+ } catch {
72
+ return {};
73
+ }
74
+ }
75
+
76
+ export function setCredential(id, credential) {
77
+ const next = { ...readCredentials(), [id]: credential };
78
+ writeSecureJson(authPath(), next);
79
+ return next;
80
+ }
81
+
82
+ export function removeCredential(id) {
83
+ const current = readCredentials();
84
+ if (!(id in current)) return current;
85
+ delete current[id];
86
+ writeSecureJson(authPath(), current);
87
+ return current;
88
+ }
@@ -0,0 +1,110 @@
1
+ // pi's provider login (the coding agent's /login) over HTTP for the agent panel (#pi-login). The sidecar
2
+ // runs `models.login(providerId, type, interaction)` itself; the interaction is a queue the panel reads:
3
+ // { type: "prompt", promptId, prompt: { type: text|secret|select|manual_code, message, placeholder?, options? } }
4
+ // { type: "event", event: pi AuthEvent (info, auth_url, device_code, progress) }
5
+ // { type: "prompt_closed", promptId } pi withdrew a prompt (e.g. a callback beat the pasted code)
6
+ // { type: "done", ok, message? }
7
+ // and answers with the prompt's value. Nothing the user types is ever put in the queue. pi persists the
8
+ // credential through the registry's CredentialStore (credential-store.mjs); onSignedIn rebuilds the registry.
9
+ import { randomUUID } from "node:crypto";
10
+
11
+ const WAIT_MS = 25_000;
12
+ const KEEP_MS = 60_000;
13
+
14
+ /** The login methods pi offers for each provider, as the panel lists them (subscriptions first). */
15
+ export async function loginOptions(models, { chatgpt } = {}) {
16
+ const options = [];
17
+ for (const provider of models.getProviders()) {
18
+ const auth = provider.auth ?? {};
19
+ // ChatGPT keeps CozyClay's own sign-in: its id_token carries the plan and account the image routes read.
20
+ const methods = provider.id === "openai-codex"
21
+ ? [{ type: "chatgpt", label: "Sign in with ChatGPT (Plus/Pro)", subscription: true }]
22
+ : [
23
+ ...(auth.oauth?.login ? [{ type: "oauth", label: auth.oauth.loginLabel || auth.oauth.name, subscription: !!auth.oauth.isSubscription }] : []),
24
+ ...(auth.apiKey?.login ? [{ type: "api_key", label: auth.apiKey.name }] : []),
25
+ ];
26
+ if (!methods.length) continue;
27
+ const check = provider.id === "openai-codex"
28
+ ? (chatgpt?.() ? { type: "oauth", source: "ChatGPT" } : undefined)
29
+ : await models.checkAuth(provider.id).catch(() => undefined);
30
+ options.push({ id: provider.id, name: provider.name ?? provider.id, methods, connected: !!check, via: check?.type ?? null, source: check?.source ?? null });
31
+ }
32
+ const rank = (entry) => (entry.connected ? 0 : entry.methods.some((method) => method.subscription) ? 1 : 2);
33
+ return options.sort((a, b) => rank(a) - rank(b) || a.name.localeCompare(b.name));
34
+ }
35
+
36
+ export function createLoginFlows({ getModels, onSignedIn }) {
37
+ const flows = new Map();
38
+ const wake = (flow) => { for (const waiter of [...flow.waiters]) waiter(); };
39
+ const push = (flow, entry) => { flow.events.push(entry); wake(flow); };
40
+ const finish = (flow, entry) => {
41
+ flow.done = true;
42
+ for (const pending of flow.pending.values()) pending.reject(new Error("The login ended."));
43
+ flow.pending.clear();
44
+ push(flow, entry);
45
+ setTimeout(() => flows.delete(flow.id), KEEP_MS).unref?.();
46
+ };
47
+
48
+ function start(providerId, type) {
49
+ const flow = { id: randomUUID(), providerId, type, events: [], waiters: new Set(), pending: new Map(), controller: new AbortController(), done: false };
50
+ const interaction = {
51
+ signal: flow.controller.signal,
52
+ prompt(prompt) {
53
+ return new Promise((resolve, reject) => {
54
+ const promptId = randomUUID();
55
+ flow.pending.set(promptId, { resolve, reject });
56
+ const { signal, ...shown } = prompt;
57
+ push(flow, { type: "prompt", promptId, prompt: shown });
58
+ signal?.addEventListener("abort", () => {
59
+ if (!flow.pending.delete(promptId)) return;
60
+ push(flow, { type: "prompt_closed", promptId });
61
+ reject(signal.reason ?? new Error("The prompt was withdrawn."));
62
+ }, { once: true });
63
+ });
64
+ },
65
+ notify(event) { push(flow, { type: "event", event }); },
66
+ };
67
+ flows.set(flow.id, flow);
68
+ void (async () => {
69
+ try {
70
+ await (await getModels()).login(providerId, type, interaction);
71
+ await onSignedIn?.(providerId);
72
+ finish(flow, { type: "done", ok: true });
73
+ } catch (error) {
74
+ finish(flow, { type: "done", ok: false, message: flow.controller.signal.aborted ? "The login was cancelled." : String(error?.message || error || "The login failed.") });
75
+ }
76
+ })();
77
+ return flow.id;
78
+ }
79
+
80
+ /** Entries after `after`, waiting up to WAIT_MS for the first new one; null for an unknown flow. */
81
+ async function read(id, after = 0) {
82
+ const flow = flows.get(id);
83
+ if (!flow) return null;
84
+ if (flow.events.length <= after && !flow.done) {
85
+ await new Promise((resolve) => {
86
+ const done = () => { clearTimeout(timer); flow.waiters.delete(done); resolve(); };
87
+ const timer = setTimeout(done, WAIT_MS);
88
+ flow.waiters.add(done);
89
+ });
90
+ }
91
+ return { events: flow.events.slice(after), next: flow.events.length, done: flow.done };
92
+ }
93
+
94
+ function answer(id, promptId, value) {
95
+ const pending = flows.get(id)?.pending.get(promptId);
96
+ if (!pending) return false;
97
+ flows.get(id).pending.delete(promptId);
98
+ pending.resolve(String(value ?? ""));
99
+ return true;
100
+ }
101
+
102
+ function cancel(id) {
103
+ const flow = flows.get(id);
104
+ if (!flow || flow.done) return false;
105
+ flow.controller.abort();
106
+ return true;
107
+ }
108
+
109
+ return { start, read, answer, cancel };
110
+ }
@@ -0,0 +1,99 @@
1
+ // Provider routes (moved out of agent-routes.mjs):
2
+ // GET /agent/providers the curated providers' sign-in state (MCP and older panels)
3
+ // PUT /agent/providers/:id { key }: store a bare api key for a curated provider
4
+ // DELETE /agent/providers/:id forget a curated provider's key
5
+ // and pi's own login (#pi-login), which the panel uses for every provider pi ships:
6
+ // GET /agent/logins each provider's login methods and whether it is connected
7
+ // POST /agent/logins { providerId, type: "oauth" | "api_key" } -> { flowId }
8
+ // GET /agent/logins/:flowId?after=n the flow's prompts and events after n (long poll, provider-login.mjs)
9
+ // POST /agent/logins/:flowId/answer { promptId, value }
10
+ // DELETE /agent/logins/:flowId cancel the flow
11
+ // POST /agent/logout/:providerId pi's logout for that provider
12
+ // Every credential change calls onChange, so the model registry is rebuilt with what was just stored.
13
+ import { createLoginFlows, loginOptions } from "./provider-login.mjs";
14
+
15
+ const KEY_PATH = /^\/agent\/providers\/([^/]+)$/;
16
+ const FLOW_PATH = /^\/agent\/logins\/([^/]+)(\/answer)?$/;
17
+ const LOGOUT_PATH = /^\/agent\/logout\/([^/]+)$/;
18
+
19
+ /** Returns the route handler for one sidecar; `getModels` is its registry, `onChange` rebuilds it. */
20
+ export function createProviderRoutes({ auth, getModels, onChange, json, readBody }) {
21
+ const flows = createLoginFlows({ getModels, onSignedIn: () => onChange?.() });
22
+ return async function handleProviderRoute(req, res, path) {
23
+ if (path === "/agent/providers" && req.method === "GET") {
24
+ const { providerStatus } = await import("./providers.mjs");
25
+ try { json(res, 200, { providers: providerStatus({ auth }) }); }
26
+ catch { json(res, 500, { error: "provider credentials unavailable" }); }
27
+ return true;
28
+ }
29
+ const keyMatch = KEY_PATH.exec(path);
30
+ if (keyMatch && ["PUT", "DELETE"].includes(req.method)) return keyRoute(req, res, keyMatch[1]);
31
+ if (path === "/agent/logins" && req.method === "GET") {
32
+ try { json(res, 200, { providers: await loginOptions(await getModels(), { chatgpt: () => !!auth.status?.().signedIn }) }); }
33
+ catch (error) { json(res, 502, { error: String(error?.message || error) }); }
34
+ return true;
35
+ }
36
+ if (path === "/agent/logins" && req.method === "POST") {
37
+ let value;
38
+ try { value = await readBody(req); } catch { json(res, 400, { error: "invalid request" }); return true; }
39
+ const models = await getModels();
40
+ const provider = models.getProvider(String(value?.providerId ?? ""));
41
+ const method = value?.type === "oauth" ? provider?.auth?.oauth : value?.type === "api_key" ? provider?.auth?.apiKey : null;
42
+ if (!provider || !method?.login || provider.id === "openai-codex") { json(res, 400, { error: "That provider has no such login." }); return true; }
43
+ json(res, 200, { flowId: flows.start(provider.id, value.type) });
44
+ return true;
45
+ }
46
+ const flowMatch = FLOW_PATH.exec(path);
47
+ if (flowMatch && !flowMatch[2] && req.method === "GET") {
48
+ const after = Number(new URL(req.url, "http://127.0.0.1").searchParams.get("after")) || 0;
49
+ const page = await flows.read(flowMatch[1], after);
50
+ if (!page) json(res, 404, { error: "login not found" }); else json(res, 200, page);
51
+ return true;
52
+ }
53
+ if (flowMatch && flowMatch[2] && req.method === "POST") {
54
+ let value;
55
+ try { value = await readBody(req); } catch { json(res, 400, { error: "invalid request" }); return true; }
56
+ if (!flows.answer(flowMatch[1], String(value?.promptId ?? ""), value?.value)) { json(res, 409, { error: "That prompt is no longer open." }); return true; }
57
+ json(res, 200, { ok: true });
58
+ return true;
59
+ }
60
+ if (flowMatch && !flowMatch[2] && req.method === "DELETE") {
61
+ json(res, 200, { ok: flows.cancel(flowMatch[1]) });
62
+ return true;
63
+ }
64
+ const logoutMatch = LOGOUT_PATH.exec(path);
65
+ if (logoutMatch && req.method === "POST") {
66
+ if (logoutMatch[1] === "openai-codex") { json(res, 400, { error: "Use Sign out for ChatGPT." }); return true; }
67
+ try { await (await getModels()).logout(logoutMatch[1]); }
68
+ catch (error) { json(res, 500, { error: String(error?.message || error) }); return true; }
69
+ await onChange?.();
70
+ json(res, 200, { ok: true });
71
+ return true;
72
+ }
73
+ return false;
74
+ };
75
+
76
+ async function keyRoute(req, res, id) {
77
+ const { PROVIDERS } = await import("./providers.mjs");
78
+ const keys = await import("./provider-keys.mjs");
79
+ const { createCredentialStore } = await import("./credential-store.mjs");
80
+ if (!PROVIDERS.some((provider) => provider.id === id)) { json(res, 404, { error: "provider not found" }); return true; }
81
+ const credentials = createCredentialStore({ auth, keys });
82
+ let key;
83
+ if (req.method === "PUT") {
84
+ if (id === "openai-codex") { json(res, 400, { error: "Use ChatGPT sign-in for OpenAI Codex." }); return true; }
85
+ try {
86
+ const value = await readBody(req);
87
+ if (typeof value?.key !== "string" || !value.key.trim()) throw new Error("Invalid key.");
88
+ key = value.key.trim();
89
+ } catch { json(res, 400, { error: "a non-empty API key is required" }); return true; }
90
+ }
91
+ try {
92
+ if (req.method === "PUT") await credentials.modify(id, async () => ({ type: "api_key", key }));
93
+ else await credentials.delete(id);
94
+ } catch { json(res, 500, { error: "provider credentials could not be saved" }); return true; }
95
+ await onChange?.();
96
+ json(res, 200, { ok: true });
97
+ return true;
98
+ }
99
+ }
@@ -2,6 +2,9 @@ import { AsyncLocalStorage } from "node:async_hooks";
2
2
  import * as defaultAuth from "../codex-auth.mjs";
3
3
  import * as defaultKeys from "./provider-keys.mjs";
4
4
  import { createCredentialStore } from "./credential-store.mjs";
5
+ import { readCredentials } from "./provider-keys.mjs";
6
+ import { cliproxyBase, cliproxyProvider, extendCliproxyProvider, registryLiveCliproxy } from "./cliproxy-provider.mjs";
7
+ export { cliproxyLiveModel } from "./cliproxy-provider.mjs";
5
8
 
6
9
  // #379 (F2 pass 5 finding 4): pi never forwards a per-request `fetch` to the
7
10
  // codex API module — `pi-agent-core`'s `createRequestOptions` (the options
@@ -69,37 +72,6 @@ const factories = {
69
72
  openrouter: "openrouterProvider",
70
73
  };
71
74
 
72
- async function cliproxyProvider({ baseUrl, env = process.env } = {}) {
73
- const base = (baseUrl ?? env.CLIPROXY_BASE_URL ?? "http://127.0.0.1:8317").replace(/\/$/, "");
74
- const [{ createProvider, envApiKeyAuth }, { openaiProvider }, { anthropicProvider }, openaiResponsesApi, anthropicMessagesApi] = await Promise.all([
75
- import("@earendil-works/pi-ai"),
76
- import("@earendil-works/pi-ai/providers/openai"),
77
- import("@earendil-works/pi-ai/providers/anthropic"),
78
- import("@earendil-works/pi-ai/api/openai-responses"),
79
- import("@earendil-works/pi-ai/api/anthropic-messages"),
80
- ]);
81
- const ids = new Set();
82
- const models = [
83
- ...openaiProvider().getModels().map((model) => ({ ...model, provider: "cliproxy", baseUrl: `${base}/v1` })),
84
- ...anthropicProvider().getModels().map((model) => ({
85
- ...model,
86
- provider: "cliproxy",
87
- baseUrl: base,
88
- // CLIProxyAPI's /v1/messages rejects messages[].output_config and thinking.block_binding, so keep
89
- // adaptive thinking (forceAdaptiveThinking) but drop the mid-conversation output_config shape.
90
- compat: { ...model.compat, supportsMidConvoEffort: false },
91
- })),
92
- ].filter((model) => !ids.has(model.id) && ids.add(model.id));
93
- return createProvider({
94
- id: "cliproxy",
95
- name: "CLIProxyAPI",
96
- baseUrl: base,
97
- auth: { apiKey: envApiKeyAuth("CLIProxyAPI key", ["CLIPROXY_API_KEY"]) },
98
- models,
99
- api: { "openai-responses": openaiResponsesApi, "anthropic-messages": anthropicMessagesApi },
100
- });
101
- }
102
-
103
75
  const providerConfig = (id) => PROVIDERS.find((provider) => provider.id === id);
104
76
 
105
77
  export async function loadProvider(id, options = {}) {
@@ -159,7 +131,7 @@ async function registryLiveCatalogue(models, codex) {
159
131
  state.fetch = Promise.resolve().then(() => codex.listModels()).then((result) => {
160
132
  state.models = liveModelsCodex(result);
161
133
  return state.models;
162
- }).catch(() => []);
134
+ }).catch(() => { state.fetch = null; return []; });
163
135
  }
164
136
  return state.fetch;
165
137
  }
@@ -176,75 +148,17 @@ async function registerLiveCodexModels(models, codex) {
176
148
  return live;
177
149
  }
178
150
 
179
- async function registryLiveCliproxy(models, apiKey) {
180
- const state = catalogueState(models);
181
- if (!state.cliproxyFetch) {
182
- state.cliproxyFetch = Promise.resolve().then(async () => {
183
- const provider = models?.getProvider?.("cliproxy");
184
- const model = provider?.getModels?.().find((entry) => entry.api === "openai-responses");
185
- if (!model?.baseUrl || !apiKey) throw new Error("CLIProxyAPI is not configured");
186
- const response = await fetch(`${model.baseUrl}/models`, {
187
- headers: { Authorization: `Bearer ${apiKey}` },
188
- signal: AbortSignal.timeout(5000),
189
- });
190
- if (!response.ok) throw new Error(`CLIProxyAPI models request failed: ${response.status}`);
191
- const body = await response.json();
192
- // id -> owned_by: the owner is how CLIProxyAPI routes a model (openai, anthropic, or an
193
- // openai-compatibility upstream name), so it picks the wire format for models pi does not know.
194
- return new Map((Array.isArray(body?.data) ? body.data : [])
195
- .map((entry) => typeof entry === "string" ? [entry, undefined] : [entry?.id, typeof entry?.owned_by === "string" ? entry.owned_by : undefined])
196
- .filter(([id]) => id));
197
- }).catch(() => null);
198
- }
199
- return state.cliproxyFetch;
200
- }
201
-
202
- const CLIPROXY_NON_CHAT = /^gpt-image-|-auto-review$/;
203
-
204
- /** A live CLIProxyAPI model pi's catalogue does not know yet (#400 follow-up), built on the newest
205
- * catalogue model of the same family and wire format: `owned_by: openai` rides openai-responses,
206
- * `owned_by: anthropic` rides anthropic-messages. Other owners (openai-compatibility upstreams such as
207
- * opencode-go) are left out: they need upstream-specific headers the proxy does not pass through. */
208
- export function cliproxyLiveModel(id, owner, knownModels) {
209
- if (CLIPROXY_NON_CHAT.test(id)) return null;
210
- const api = owner === "openai" ? "openai-responses" : owner === "anthropic" ? "anthropic-messages" : null;
211
- if (!api) return null;
212
- const candidates = knownModels.filter((model) => model.api === api);
213
- const family = owner === "anthropic" ? id.match(/^claude-[a-z]+/)?.[0] : id.match(/^gpt-\d+/)?.[0];
214
- const sameFamily = family ? candidates.filter((model) => model.id.startsWith(family)) : [];
215
- const template = [...(sameFamily.length ? sameFamily : candidates)].sort((a, b) => b.id.localeCompare(a.id, undefined, { numeric: true }))[0];
216
- if (!template) return null;
217
- // The 5-5 claude generation rejects thinking {type:"disabled"}; a null "off" makes pi-ai send no thinking block (#723).
218
- const generation = owner === "anthropic" ? id.match(/-(\d+)-(\d+)$/) : null;
219
- if (generation && Number(generation[1]) >= 5) return { ...template, id, name: id, thinkingLevelMap: { ...template.thinkingLevelMap, off: null } };
220
- return { ...template, id, name: id };
221
- }
222
-
223
- function extendCliproxyProvider(provider, live, state) {
224
- if (state.extendedProviders.has(provider)) return false;
225
- const getModels = provider.getModels.bind(provider);
226
- const knownModels = getModels();
227
- const known = new Set(knownModels.map((model) => model.id));
228
- const additions = [...live]
229
- .filter(([id]) => !known.has(id))
230
- .map(([id, owner]) => cliproxyLiveModel(id, owner, knownModels))
231
- .filter(Boolean)
232
- .sort((a, b) => b.id.localeCompare(a.id, undefined, { numeric: true }));
233
- if (!additions.length) return false;
234
- provider.getModels = () => [...getModels(), ...additions];
235
- state.extendedProviders.add(provider);
236
- return true;
237
- }
238
-
239
151
  export async function createModels({ credentials, auth = defaultAuth, keys = defaultKeys, env = process.env, codexBaseUrl, cliproxyBaseUrl } = {}) {
240
152
  const { createModels: createPiModels } = await import("@earendil-works/pi-ai");
241
153
  const store = credentials ?? createCredentialStore({ auth, keys, env });
242
154
  const models = createPiModels({ credentials: store });
243
155
  for (const provider of PROVIDERS) {
244
- const baseUrl = provider.id === "openai-codex" ? codexBaseUrl : provider.id === "cliproxy" ? cliproxyBaseUrl : undefined;
245
- const loaded = await loadProvider(provider.id, { baseUrl, env });
246
- models.setProvider(loaded);
156
+ const baseUrl = provider.id === "openai-codex" ? codexBaseUrl : provider.id === "cliproxy" ? cliproxyBaseUrl ?? cliproxyBase({ env, credential: await store.read("cliproxy").catch(() => undefined) }) : undefined;
157
+ models.setProvider(await loadProvider(provider.id, { baseUrl, env }));
247
158
  }
159
+ // every other provider pi ships, so each of pi's logins (subscriptions and api keys) has a provider to land on (#pi-login)
160
+ const { builtinProviders } = await import("@earendil-works/pi-ai/providers/all");
161
+ for (const provider of builtinProviders()) if (!models.getProvider(provider.id)) models.setProvider(provider);
248
162
  return models;
249
163
  }
250
164
 
@@ -254,8 +168,9 @@ export async function resolveModel(requested, options = {}) {
254
168
  const slash = value.indexOf("/");
255
169
  const provider = slash === -1 ? "openai-codex" : value.slice(0, slash);
256
170
  const modelId = slash === -1 ? value : value.slice(slash + 1);
257
- if (!providerConfig(provider) || !modelId) throw unknownModel(requested);
171
+ if (!modelId) throw unknownModel(requested);
258
172
  const models = options.models ?? await createModels(options);
173
+ if (!providerConfig(provider) && !models.getProvider?.(provider)) throw unknownModel(requested);
259
174
  const model = models.getModel(provider, modelId);
260
175
  if (!model) throw unknownModel(requested);
261
176
  return { provider, modelId, model };
@@ -483,7 +398,7 @@ export async function listAgentModels({ models, codex, auth = defaultAuth, keys
483
398
  list = [...list].sort(astraFirst);
484
399
  } else if (provider.id === "cliproxy") {
485
400
  if (signedIn) {
486
- const live = await registryLiveCliproxy(resolvedModels, authResult?.auth?.apiKey);
401
+ const live = await registryLiveCliproxy(resolvedModels, authResult?.auth?.apiKey, catalogueState(resolvedModels));
487
402
  if (live) {
488
403
  // The proxy's own /v1/models is the lineup: register what pi's catalogue has not caught up
489
404
  // with on the shared registry, so the turn route resolves the same ids the dropdown shows.
@@ -496,6 +411,13 @@ export async function listAgentModels({ models, codex, auth = defaultAuth, keys
496
411
  }
497
412
  providers.push({ id: provider.id, label: provider.label, signedIn, authSource, models: list });
498
413
  }
414
+ // a provider outside the curated list joins the picker once a pi login stored its credential here (#pi-login); ambient
415
+ // credentials (an AWS profile, cloud ADC files) alone do not list it, so the picker shows what the author connected
416
+ const stored = new Set([...Object.keys(readCredentials()), ...Object.keys(saved)]);
417
+ for (const provider of resolvedModels.getProviders?.() ?? []) {
418
+ const check = providerConfig(provider.id) || !stored.has(provider.id) ? undefined : await resolvedModels.checkAuth?.(provider.id).catch(() => undefined);
419
+ if (check) providers.push({ id: provider.id, label: provider.name ?? provider.id, signedIn: true, authSource: check.type === "oauth" || /stored/i.test(check.source ?? "") ? "file" : "env", models: catalogModels(resolvedModels, provider.id, getSupportedThinkingLevels) });
420
+ }
499
421
  // The flat union is what the panel's dropdown reads today (`models[].id`);
500
422
  // with six providers in play the id has to be the provider/id key so two
501
423
  // providers' same-named model never collide there, while each provider's