@gajae-code/coding-agent 0.17.1 → 0.17.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (375) hide show
  1. package/CHANGELOG.md +266 -0
  2. package/dist/types/async/job-manager.d.ts +7 -6
  3. package/dist/types/cli/daemon-cli.d.ts +3 -1
  4. package/dist/types/cli/github-release.d.ts +17 -0
  5. package/dist/types/cli/public-command-entry.d.ts +56 -0
  6. package/dist/types/cli/public-command-errors.d.ts +126 -0
  7. package/dist/types/cli/public-command-evidence.d.ts +99 -0
  8. package/dist/types/cli/public-command-help.d.ts +65 -0
  9. package/dist/types/cli/public-command-registry.d.ts +69 -0
  10. package/dist/types/cli/session-picker.d.ts +1 -1
  11. package/dist/types/cli/setup-cli.d.ts +1 -0
  12. package/dist/types/cli/skills-cli.d.ts +5 -0
  13. package/dist/types/cli.d.ts +3 -0
  14. package/dist/types/commands/daemon.d.ts +4 -50
  15. package/dist/types/commands/sdk.d.ts +20 -37
  16. package/dist/types/commands/setup.d.ts +3 -0
  17. package/dist/types/commands/skills.d.ts +9 -0
  18. package/dist/types/config/autorouting-tier-map.d.ts +24 -0
  19. package/dist/types/config/file-lock.d.ts +10 -1
  20. package/dist/types/config/model-profile-activation.d.ts +63 -4
  21. package/dist/types/config/model-registry.d.ts +20 -2
  22. package/dist/types/config/settings-schema.d.ts +11 -0
  23. package/dist/types/coordinator-mcp/question-state.d.ts +6 -1
  24. package/dist/types/coordinator-mcp/server.d.ts +3 -0
  25. package/dist/types/daemon/operator-contract.d.ts +5 -1
  26. package/dist/types/eval/js/worker-protocol.d.ts +2 -0
  27. package/dist/types/extensibility/extensions/types.d.ts +12 -0
  28. package/dist/types/extensibility/gjc-plugins/registry.d.ts +4 -0
  29. package/dist/types/extensibility/runtime-skill-discovery.d.ts +18 -0
  30. package/dist/types/extensibility/shared-events.d.ts +3 -1
  31. package/dist/types/gjc-runtime/session-state-lock.d.ts +61 -0
  32. package/dist/types/internal-urls/embedded-protocol.d.ts +9 -0
  33. package/dist/types/internal-urls/index.d.ts +1 -0
  34. package/dist/types/internal-urls/parse.d.ts +9 -1
  35. package/dist/types/main.d.ts +22 -0
  36. package/dist/types/modes/acp/acp-agent.d.ts +94 -1
  37. package/dist/types/modes/acp/startup-provenance.d.ts +16 -0
  38. package/dist/types/modes/components/custom-provider-wizard.d.ts +19 -1
  39. package/dist/types/modes/components/session-selector.d.ts +5 -2
  40. package/dist/types/modes/components/thinking-selector.d.ts +2 -1
  41. package/dist/types/modes/components/user-message-selector.d.ts +5 -2
  42. package/dist/types/modes/controllers/command-controller.d.ts +1 -1
  43. package/dist/types/modes/controllers/selector-controller.d.ts +1 -1
  44. package/dist/types/modes/interactive-mode.d.ts +1 -1
  45. package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +11 -1
  46. package/dist/types/modes/shared/agent-wire/workflow-gate-types.d.ts +14 -1
  47. package/dist/types/modes/types.d.ts +2 -2
  48. package/dist/types/runtime/process-lifecycle.d.ts +2 -0
  49. package/dist/types/runtime-mcp/manager.d.ts +56 -0
  50. package/dist/types/runtime-mcp/pool.d.ts +17 -0
  51. package/dist/types/runtime-mcp/redaction.d.ts +20 -0
  52. package/dist/types/runtime-mcp/startup-policy.d.ts +6 -0
  53. package/dist/types/runtime-mcp/tool-bridge.d.ts +9 -0
  54. package/dist/types/runtime-mcp/transports/http.d.ts +11 -0
  55. package/dist/types/sdk/acp/adapter.d.ts +27 -0
  56. package/dist/types/sdk/acp/final-text.d.ts +8 -0
  57. package/dist/types/sdk/broker/broker.d.ts +13 -2
  58. package/dist/types/sdk/broker/ensure.d.ts +46 -1
  59. package/dist/types/sdk/broker/identity.d.ts +3 -1
  60. package/dist/types/sdk/broker/lifecycle-ledger.d.ts +23 -3
  61. package/dist/types/sdk/broker/lifecycle.d.ts +15 -8
  62. package/dist/types/sdk/broker/runtime.d.ts +1 -1
  63. package/dist/types/sdk/broker/session-index.d.ts +22 -3
  64. package/dist/types/sdk/broker/spawn-authority.d.ts +4 -0
  65. package/dist/types/sdk/broker/stderr-drainer.d.ts +13 -0
  66. package/dist/types/sdk/bus/index.d.ts +19 -0
  67. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +3 -1
  68. package/dist/types/sdk/bus/telegram-daemon.d.ts +5 -0
  69. package/dist/types/sdk/cli/master-cli.d.ts +6 -10
  70. package/dist/types/sdk/cli/rows.d.ts +19 -1
  71. package/dist/types/sdk/cli/session-cli.d.ts +22 -5
  72. package/dist/types/sdk/client/client.d.ts +2 -0
  73. package/dist/types/sdk/guides/cli.d.ts +3 -4
  74. package/dist/types/sdk/guides/verify.d.ts +2 -0
  75. package/dist/types/sdk/host/host.d.ts +9 -0
  76. package/dist/types/sdk/host/session-runtime.d.ts +31 -0
  77. package/dist/types/sdk/host/types.d.ts +10 -0
  78. package/dist/types/sdk/index.d.ts +1 -0
  79. package/dist/types/sdk/prompt-deadline-flush.d.ts +83 -0
  80. package/dist/types/sdk/prompt-deadline-manager.d.ts +38 -1
  81. package/dist/types/sdk/prompt-failure.d.ts +30 -1
  82. package/dist/types/sdk/prompt-status.d.ts +8 -0
  83. package/dist/types/sdk/prompt-tool-boundary.d.ts +73 -0
  84. package/dist/types/sdk/router/replay-retention-gap-cache.d.ts +13 -0
  85. package/dist/types/sdk/router/session-router.d.ts +2 -0
  86. package/dist/types/sdk/startup-capability.d.ts +2 -0
  87. package/dist/types/sdk/transport/relay.d.ts +1 -0
  88. package/dist/types/sdk/transport/serve-cli.d.ts +73 -2
  89. package/dist/types/sdk/transport/stdio.d.ts +5 -1
  90. package/dist/types/session/agent-session.d.ts +168 -21
  91. package/dist/types/session/fallback-chain-controller.d.ts +6 -6
  92. package/dist/types/session/fold-coordinator.d.ts +17 -3
  93. package/dist/types/session/internal/managed-session-storage.d.ts +7 -1
  94. package/dist/types/session/session-manager.d.ts +64 -1
  95. package/dist/types/session/session-work-lease.d.ts +14 -0
  96. package/dist/types/setup/model-onboarding-guidance.d.ts +1 -1
  97. package/dist/types/setup/provider-onboarding.d.ts +81 -1
  98. package/dist/types/thinking.d.ts +3 -1
  99. package/dist/types/tools/bash.d.ts +37 -11
  100. package/dist/types/tools/job.d.ts +1 -1
  101. package/dist/types/tools/steer-fold.d.ts +31 -0
  102. package/dist/types/utils/git.d.ts +28 -2
  103. package/dist/types/utils/mupdf-embedded.d.ts +2 -0
  104. package/dist/types/utils/mupdf-wasm-embedded.d.ts +2 -0
  105. package/dist/types/utils/mupdf.d.ts +4 -0
  106. package/dist/types/utils/session-title-generation.d.ts +6 -0
  107. package/dist/types/utils/title-generator.d.ts +11 -2
  108. package/dist/types/web/scrapers/types.d.ts +3 -0
  109. package/package.json +18 -10
  110. package/scripts/build-binary.ts +9 -3
  111. package/scripts/compile-args.ts +0 -9
  112. package/scripts/embed-mupdf.ts +52 -0
  113. package/scripts/generate-sdk-operation-inventory.ts +31 -1
  114. package/scripts/vendor-markit.ts +196 -0
  115. package/src/async/job-manager.ts +60 -9
  116. package/src/cli/daemon-cli.ts +53 -18
  117. package/src/cli/github-release.ts +87 -5
  118. package/src/cli/mcp-cli.ts +38 -4
  119. package/src/cli/public-command-entry.ts +450 -0
  120. package/src/cli/public-command-errors.ts +581 -0
  121. package/src/cli/public-command-evidence.ts +717 -0
  122. package/src/cli/public-command-help.ts +360 -0
  123. package/src/cli/public-command-registry.ts +535 -0
  124. package/src/cli/session-picker.ts +2 -1
  125. package/src/cli/setup-cli.ts +9 -1
  126. package/src/cli/skills-cli.ts +46 -2
  127. package/src/cli.ts +61 -1
  128. package/src/commands/daemon.ts +65 -69
  129. package/src/commands/sdk.ts +232 -100
  130. package/src/commands/setup.ts +2 -0
  131. package/src/commands/skills.ts +11 -0
  132. package/src/config/autorouting-tier-map.ts +16 -0
  133. package/src/config/file-lock.ts +199 -68
  134. package/src/config/model-profile-activation.ts +406 -53
  135. package/src/config/model-registry.ts +204 -25
  136. package/src/config/settings-schema.ts +26 -2
  137. package/src/coordinator-mcp/policy.ts +10 -5
  138. package/src/coordinator-mcp/question-gate-codec.ts +17 -0
  139. package/src/coordinator-mcp/question-state.ts +175 -8
  140. package/src/coordinator-mcp/server.ts +528 -87
  141. package/src/coordinator-mcp/session-reaper.ts +6 -4
  142. package/src/crash/record-loader.ts +6 -2
  143. package/src/daemon/operator-contract.ts +26 -12
  144. package/src/debug/report-bundle.ts +114 -24
  145. package/src/defaults/gjc/skills/ultragoal/SKILL.md +1 -1
  146. package/src/eval/js/context-manager.ts +8 -8
  147. package/src/eval/js/worker-core.ts +9 -4
  148. package/src/eval/js/worker-protocol.ts +2 -0
  149. package/src/extensibility/extensions/runner.ts +52 -4
  150. package/src/extensibility/extensions/types.ts +10 -0
  151. package/src/extensibility/gjc-plugins/registry.ts +196 -37
  152. package/src/extensibility/runtime-skill-discovery.ts +61 -3
  153. package/src/extensibility/shared-events.ts +3 -1
  154. package/src/gjc-runtime/managed-owner-admission.ts +67 -50
  155. package/src/gjc-runtime/session-state-lock.ts +165 -24
  156. package/src/gjc-runtime/ultragoal-succession.ts +31 -3
  157. package/src/internal-urls/docs-index.generated.ts +21 -19
  158. package/src/internal-urls/embedded-protocol.ts +83 -0
  159. package/src/internal-urls/index.ts +1 -0
  160. package/src/internal-urls/parse.ts +21 -1
  161. package/src/internal-urls/router.ts +4 -2
  162. package/src/main.ts +123 -55
  163. package/src/modes/acp/acp-agent.ts +593 -47
  164. package/src/modes/acp/acp-event-mapper.ts +9 -0
  165. package/src/modes/acp/startup-provenance.ts +59 -0
  166. package/src/modes/components/custom-provider-wizard.ts +308 -7
  167. package/src/modes/components/model-selector.ts +31 -15
  168. package/src/modes/components/oauth-selector.ts +9 -0
  169. package/src/modes/components/session-selector.ts +47 -4
  170. package/src/modes/components/status-line/segments.ts +6 -1
  171. package/src/modes/components/thinking-selector.ts +7 -3
  172. package/src/modes/components/user-message-selector.ts +57 -15
  173. package/src/modes/controllers/command-controller.ts +58 -32
  174. package/src/modes/controllers/extension-ui-controller.ts +33 -7
  175. package/src/modes/controllers/input-controller.ts +4 -0
  176. package/src/modes/controllers/selector-controller.ts +177 -18
  177. package/src/modes/interactive-mode.ts +21 -7
  178. package/src/modes/runtime-init.ts +1 -0
  179. package/src/modes/shared/agent-wire/workflow-gate-broker.ts +124 -7
  180. package/src/modes/shared/agent-wire/workflow-gate-types.ts +16 -2
  181. package/src/modes/types.ts +2 -2
  182. package/src/prompts/agent-fragments/browser-aside.md +1 -0
  183. package/src/prompts/system/title-system.md +1 -1
  184. package/src/prompts/tools/bash.md +1 -1
  185. package/src/prompts/tools/job.md +1 -1
  186. package/src/prompts/tools/read.md +2 -0
  187. package/src/runtime/network-prewarm-service.test.ts +36 -1
  188. package/src/runtime/network-prewarm-service.ts +28 -3
  189. package/src/runtime/process-lifecycle.ts +22 -11
  190. package/src/runtime-mcp/config.ts +16 -1
  191. package/src/runtime-mcp/manager.ts +689 -55
  192. package/src/runtime-mcp/pool.test.ts +31 -0
  193. package/src/runtime-mcp/pool.ts +304 -28
  194. package/src/runtime-mcp/redaction.ts +42 -0
  195. package/src/runtime-mcp/startup-policy.ts +7 -0
  196. package/src/runtime-mcp/tool-bridge.ts +71 -0
  197. package/src/runtime-mcp/transports/http.ts +108 -25
  198. package/src/sdk/acp/adapter.ts +90 -9
  199. package/src/sdk/acp/final-text.ts +19 -2
  200. package/src/sdk/broker/broker.ts +378 -52
  201. package/src/sdk/broker/ensure.ts +97 -19
  202. package/src/sdk/broker/identity.ts +9 -1
  203. package/src/sdk/broker/lifecycle-ledger.ts +95 -31
  204. package/src/sdk/broker/lifecycle.ts +690 -141
  205. package/src/sdk/broker/runtime.ts +1 -1
  206. package/src/sdk/broker/session-index.ts +97 -4
  207. package/src/sdk/broker/spawn-authority.ts +35 -7
  208. package/src/sdk/broker/stderr-drainer.ts +59 -0
  209. package/src/sdk/bus/chat-daemon-control.ts +2 -2
  210. package/src/sdk/bus/chat-daemon-runtime.ts +1 -0
  211. package/src/sdk/bus/index.ts +721 -84
  212. package/src/sdk/bus/telegram-daemon-cli.ts +31 -2
  213. package/src/sdk/bus/telegram-daemon-contract.ts +3 -1
  214. package/src/sdk/bus/telegram-daemon.ts +10 -3
  215. package/src/sdk/cli/master-cli.ts +47 -36
  216. package/src/sdk/cli/rows.ts +30 -7
  217. package/src/sdk/cli/session-cli.ts +687 -184
  218. package/src/sdk/client/client.ts +18 -0
  219. package/src/sdk/guides/catalog.ts +2 -2
  220. package/src/sdk/guides/cli.ts +20 -62
  221. package/src/sdk/guides/verify.ts +14 -0
  222. package/src/sdk/host/host.ts +49 -5
  223. package/src/sdk/host/session-runtime.test.ts +636 -9
  224. package/src/sdk/host/session-runtime.ts +499 -118
  225. package/src/sdk/host/types.ts +12 -1
  226. package/src/sdk/host/websocket-transport.lifecycle.test.ts +84 -0
  227. package/src/sdk/host/websocket-transport.ts +40 -6
  228. package/src/sdk/index.ts +11 -0
  229. package/src/sdk/prompt-deadline-flush.ts +334 -0
  230. package/src/sdk/prompt-deadline-manager.ts +136 -4
  231. package/src/sdk/prompt-failure.ts +43 -1
  232. package/src/sdk/prompt-status.ts +8 -0
  233. package/src/sdk/prompt-tool-boundary.ts +121 -0
  234. package/src/sdk/protocol/operation-inventory.generated.json +106 -0
  235. package/src/sdk/router/replay-retention-gap-cache.ts +27 -0
  236. package/src/sdk/router/session-router.ts +31 -4
  237. package/src/sdk/session.ts +194 -16
  238. package/src/sdk/startup-capability.ts +1 -1
  239. package/src/sdk/transport/relay.ts +71 -6
  240. package/src/sdk/transport/serve-cli.ts +494 -49
  241. package/src/sdk/transport/socket.ts +12 -11
  242. package/src/sdk/transport/stdio.ts +7 -3
  243. package/src/session/agent-session.ts +2437 -400
  244. package/src/session/fallback-chain-controller.ts +6 -7
  245. package/src/session/fold-coordinator.ts +24 -5
  246. package/src/session/internal/managed-session-scope.ts +58 -18
  247. package/src/session/internal/managed-session-storage.ts +304 -11
  248. package/src/session/session-manager.ts +382 -44
  249. package/src/session/session-work-lease.ts +34 -0
  250. package/src/session-import/codex.ts +12 -3
  251. package/src/setup/model-onboarding-guidance.ts +1 -1
  252. package/src/setup/provider-onboarding.ts +397 -24
  253. package/src/slash-commands/builtin-registry.ts +258 -14
  254. package/src/task/index.ts +14 -2
  255. package/src/thinking.ts +21 -2
  256. package/src/tools/bash.ts +98 -78
  257. package/src/tools/fetch.ts +119 -20
  258. package/src/tools/job.ts +151 -25
  259. package/src/tools/output-meta.ts +15 -2
  260. package/src/tools/path-utils.ts +27 -3
  261. package/src/tools/read.ts +3 -3
  262. package/src/tools/steer-fold.ts +107 -0
  263. package/src/tools/tool-catalog.generated.ts +29 -2
  264. package/src/tools/tool-errors.ts +4 -0
  265. package/src/utils/git.ts +53 -3
  266. package/src/utils/markit.ts +55 -41
  267. package/src/utils/mupdf-assets.d.ts +4 -0
  268. package/src/utils/mupdf-embedded.ts +4 -0
  269. package/src/utils/mupdf-wasm-embedded.ts +3 -0
  270. package/src/utils/mupdf-wasm.ts +24 -2
  271. package/src/utils/mupdf.ts +129 -0
  272. package/src/utils/session-title-generation.ts +29 -0
  273. package/src/utils/title-generator.ts +46 -2
  274. package/src/web/scrapers/types.ts +24 -3
  275. package/vendor/markit-ai/LICENSE +21 -0
  276. package/vendor/markit-ai/MANIFEST.json +412 -0
  277. package/vendor/markit-ai/dist/commands/config.d.ts +4 -0
  278. package/vendor/markit-ai/dist/commands/config.js +139 -0
  279. package/vendor/markit-ai/dist/commands/convert.d.ts +6 -0
  280. package/vendor/markit-ai/dist/commands/convert.js +113 -0
  281. package/vendor/markit-ai/dist/commands/formats.d.ts +2 -0
  282. package/vendor/markit-ai/dist/commands/formats.js +92 -0
  283. package/vendor/markit-ai/dist/commands/init.d.ts +2 -0
  284. package/vendor/markit-ai/dist/commands/init.js +29 -0
  285. package/vendor/markit-ai/dist/commands/onboard.d.ts +2 -0
  286. package/vendor/markit-ai/dist/commands/onboard.js +61 -0
  287. package/vendor/markit-ai/dist/commands/plugin.d.ts +4 -0
  288. package/vendor/markit-ai/dist/commands/plugin.js +58 -0
  289. package/vendor/markit-ai/dist/config.d.ts +26 -0
  290. package/vendor/markit-ai/dist/config.js +42 -0
  291. package/vendor/markit-ai/dist/converters/audio.d.ts +7 -0
  292. package/vendor/markit-ai/dist/converters/audio.js +104 -0
  293. package/vendor/markit-ai/dist/converters/csv.d.ts +7 -0
  294. package/vendor/markit-ai/dist/converters/csv.js +83 -0
  295. package/vendor/markit-ai/dist/converters/docx.d.ts +6 -0
  296. package/vendor/markit-ai/dist/converters/docx.js +57 -0
  297. package/vendor/markit-ai/dist/converters/epub.d.ts +8 -0
  298. package/vendor/markit-ai/dist/converters/epub.js +128 -0
  299. package/vendor/markit-ai/dist/converters/github.d.ts +18 -0
  300. package/vendor/markit-ai/dist/converters/github.js +148 -0
  301. package/vendor/markit-ai/dist/converters/html.d.ts +6 -0
  302. package/vendor/markit-ai/dist/converters/html.js +30 -0
  303. package/vendor/markit-ai/dist/converters/image.d.ts +6 -0
  304. package/vendor/markit-ai/dist/converters/image.js +124 -0
  305. package/vendor/markit-ai/dist/converters/ipynb.d.ts +6 -0
  306. package/vendor/markit-ai/dist/converters/ipynb.js +75 -0
  307. package/vendor/markit-ai/dist/converters/iwork.d.ts +20 -0
  308. package/vendor/markit-ai/dist/converters/iwork.js +391 -0
  309. package/vendor/markit-ai/dist/converters/json.d.ts +6 -0
  310. package/vendor/markit-ai/dist/converters/json.js +21 -0
  311. package/vendor/markit-ai/dist/converters/pdf/columns.d.ts +35 -0
  312. package/vendor/markit-ai/dist/converters/pdf/columns.js +93 -0
  313. package/vendor/markit-ai/dist/converters/pdf/extract.d.ts +19 -0
  314. package/vendor/markit-ai/dist/converters/pdf/extract.js +514 -0
  315. package/vendor/markit-ai/dist/converters/pdf/grid.d.ts +25 -0
  316. package/vendor/markit-ai/dist/converters/pdf/grid.js +795 -0
  317. package/vendor/markit-ai/dist/converters/pdf/headers.d.ts +24 -0
  318. package/vendor/markit-ai/dist/converters/pdf/headers.js +108 -0
  319. package/vendor/markit-ai/dist/converters/pdf/index.d.ts +19 -0
  320. package/vendor/markit-ai/dist/converters/pdf/index.js +136 -0
  321. package/vendor/markit-ai/dist/converters/pdf/mupdf-loader.js +9 -0
  322. package/vendor/markit-ai/dist/converters/pdf/render.d.ts +24 -0
  323. package/vendor/markit-ai/dist/converters/pdf/render.js +513 -0
  324. package/vendor/markit-ai/dist/converters/pdf/types.d.ts +75 -0
  325. package/vendor/markit-ai/dist/converters/pdf/types.js +1 -0
  326. package/vendor/markit-ai/dist/converters/plain-text.d.ts +6 -0
  327. package/vendor/markit-ai/dist/converters/plain-text.js +87 -0
  328. package/vendor/markit-ai/dist/converters/pptx.d.ts +8 -0
  329. package/vendor/markit-ai/dist/converters/pptx.js +289 -0
  330. package/vendor/markit-ai/dist/converters/rss.d.ts +11 -0
  331. package/vendor/markit-ai/dist/converters/rss.js +138 -0
  332. package/vendor/markit-ai/dist/converters/wikipedia.d.ts +6 -0
  333. package/vendor/markit-ai/dist/converters/wikipedia.js +33 -0
  334. package/vendor/markit-ai/dist/converters/xlsx.d.ts +8 -0
  335. package/vendor/markit-ai/dist/converters/xlsx.js +147 -0
  336. package/vendor/markit-ai/dist/converters/xml.d.ts +6 -0
  337. package/vendor/markit-ai/dist/converters/xml.js +18 -0
  338. package/vendor/markit-ai/dist/converters/yaml.d.ts +6 -0
  339. package/vendor/markit-ai/dist/converters/yaml.js +17 -0
  340. package/vendor/markit-ai/dist/converters/zip.d.ts +8 -0
  341. package/vendor/markit-ai/dist/converters/zip.js +57 -0
  342. package/vendor/markit-ai/dist/index.d.ts +30 -0
  343. package/vendor/markit-ai/dist/index.js +26 -0
  344. package/vendor/markit-ai/dist/main.d.ts +2 -0
  345. package/vendor/markit-ai/dist/main.js +191 -0
  346. package/vendor/markit-ai/dist/markit.d.ts +45 -0
  347. package/vendor/markit-ai/dist/markit.js +267 -0
  348. package/vendor/markit-ai/dist/plugins/api.d.ts +7 -0
  349. package/vendor/markit-ai/dist/plugins/api.js +42 -0
  350. package/vendor/markit-ai/dist/plugins/index.d.ts +4 -0
  351. package/vendor/markit-ai/dist/plugins/index.js +3 -0
  352. package/vendor/markit-ai/dist/plugins/installer.d.ts +25 -0
  353. package/vendor/markit-ai/dist/plugins/installer.js +176 -0
  354. package/vendor/markit-ai/dist/plugins/loader.d.ts +6 -0
  355. package/vendor/markit-ai/dist/plugins/loader.js +61 -0
  356. package/vendor/markit-ai/dist/plugins/types.d.ts +25 -0
  357. package/vendor/markit-ai/dist/plugins/types.js +1 -0
  358. package/vendor/markit-ai/dist/providers/anthropic.d.ts +2 -0
  359. package/vendor/markit-ai/dist/providers/anthropic.js +47 -0
  360. package/vendor/markit-ai/dist/providers/index.d.ts +21 -0
  361. package/vendor/markit-ai/dist/providers/index.js +56 -0
  362. package/vendor/markit-ai/dist/providers/openai.d.ts +2 -0
  363. package/vendor/markit-ai/dist/providers/openai.js +76 -0
  364. package/vendor/markit-ai/dist/providers/types.d.ts +26 -0
  365. package/vendor/markit-ai/dist/providers/types.js +1 -0
  366. package/vendor/markit-ai/dist/types.d.ts +36 -0
  367. package/vendor/markit-ai/dist/types.js +1 -0
  368. package/vendor/markit-ai/dist/utils/exit-codes.d.ts +4 -0
  369. package/vendor/markit-ai/dist/utils/exit-codes.js +4 -0
  370. package/vendor/markit-ai/dist/utils/output.d.ts +22 -0
  371. package/vendor/markit-ai/dist/utils/output.js +31 -0
  372. package/vendor/markit-ai/dist/utils/turndown.d.ts +8 -0
  373. package/vendor/markit-ai/dist/utils/turndown.js +70 -0
  374. package/vendor/markit-ai/package.json +74 -0
  375. package/vendor/markit-ai.patch +49 -0
@@ -1,11 +1,19 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import * as fs from "node:fs/promises";
3
3
  import * as path from "node:path";
4
- import { getAgentDbPath, getAgentDir } from "@gajae-code/utils";
4
+ import { type AuthStorage, resolveOAuthStorageProvider } from "@gajae-code/ai/core";
5
+ import {
6
+ isSafeCatalogModelId,
7
+ MODELS_LIST_REQUEST_TIMEOUT_MS,
8
+ readBoundedModelsJson,
9
+ } from "@gajae-code/ai/utils/discovery/openai-compatible";
10
+ import { getAgentDbPath, getAgentDir, logger } from "@gajae-code/utils";
11
+ import { $rotatingCredentialEnv } from "@gajae-code/utils/env";
5
12
  import { YAML } from "bun";
13
+ import { withFileLock } from "../config/file-lock";
6
14
  import { type ModelsConfig, ModelsConfigSchema, type ProviderDiscovery } from "../config/models-config-schema";
7
15
  import { compareRankedProviders, famousProviderIndex } from "../config/provider-ranking";
8
- import { AuthStorage } from "../session/auth-storage";
16
+ import { AuthStorage as AuthStorageImpl } from "../session/auth-storage";
9
17
  import providerPresets from "./provider-presets.json";
10
18
 
11
19
  export type ProviderCompatibility = "openai" | "anthropic";
@@ -21,6 +29,37 @@ export interface ProviderSetupInput {
21
29
  models?: string[];
22
30
  modelsPath?: string;
23
31
  force?: boolean;
32
+ /**
33
+ * Opt into live OpenAI `/v1/models` discovery for an OpenAI-compatible
34
+ * custom provider. Manual `--model` ids may be omitted when set; the
35
+ * runtime auto-discovers the catalog on refresh and merges it without
36
+ * duplicates.
37
+ *
38
+ * When no manual models are given, setup probes the endpoint before
39
+ * writing config so an unreachable endpoint, rejected credential, or
40
+ * empty catalog fails the setup loudly instead of writing a provider
41
+ * that can never serve a model. There is deliberately no skip flag: the
42
+ * pre-write probe is the enforcement point, and the wizard's
43
+ * interactive probe is a preview only (inputs may have changed since).
44
+ */
45
+ discover?: boolean;
46
+ /** Probe override for tests; defaults to the live endpoint probe. */
47
+ probeDiscovery?: (input: ProviderDiscoveryProbeInput) => Promise<ProviderDiscoveryProbeResult>;
48
+ /**
49
+ * Cancel discovery and persistence until the atomic config rename is
50
+ * invoked. Later cancellation does not undo a committed provider.
51
+ * Combined with the shared deadline inside the probe; callers that omit
52
+ * it get deadline-only probe behavior.
53
+ */
54
+ discoverySignal?: AbortSignal;
55
+ /**
56
+ * Live credential authority for literal-key persistence. Interactive
57
+ * callers pass the active session registry's AuthStorage so the
58
+ * subsequent targeted online refresh authenticates from the same
59
+ * authority that stored the key. When omitted, onboarding falls back
60
+ * to a short-lived store at the agent DB path (headless/CLI behavior).
61
+ */
62
+ authStorage?: ProviderSetupCredentialStore;
24
63
  }
25
64
 
26
65
  export interface ProviderSetupResult {
@@ -32,12 +71,19 @@ export interface ProviderSetupResult {
32
71
  modelsPath: string;
33
72
  redactedApiKey: string;
34
73
  credentialSource: "literal" | "env";
74
+ /** True when the provider persists a `discovery:` block (live catalog). */
75
+ discoveryEnabled: boolean;
76
+ /** The persisted discovery type, when any (e.g. `openai-models-list`). */
77
+ discoveryType?: string;
35
78
  preset?: string;
36
79
  presetName?: string;
37
80
  }
38
81
 
39
82
  type ProviderConfig = NonNullable<NonNullable<ModelsConfig["providers"]>[string]>;
40
83
  type ProviderCompatConfig = NonNullable<ProviderConfig["compat"]>;
84
+ type ProviderSetupCredentialStore = Pick<AuthStorage, "set" | "remove" | "exportSnapshot"> & {
85
+ peekApiKey?: (provider: string) => Promise<string | undefined>;
86
+ };
41
87
 
42
88
  interface ProviderPreset {
43
89
  id: string;
@@ -62,8 +108,6 @@ interface ProviderPreset {
62
108
  }
63
109
 
64
110
  const PROVIDER_ID_PATTERN = /^[a-z0-9][a-z0-9._-]*$/;
65
- const REDACT_PREFIX = 4;
66
- const REDACT_SUFFIX = 4;
67
111
  export const PROVIDER_PRESETS: readonly ProviderPreset[] = providerPresets as ProviderPreset[];
68
112
 
69
113
  export function getDefaultModelsPath(): string {
@@ -118,10 +162,35 @@ export function parseModelList(values: readonly string[]): string[] {
118
162
  return [...new Set(models)];
119
163
  }
120
164
 
121
- export function redactSecret(secret: string): string {
122
- const trimmed = secret.trim();
123
- if (trimmed.length <= REDACT_PREFIX + REDACT_SUFFIX) return "***";
124
- return `${trimmed.slice(0, REDACT_PREFIX)}…${trimmed.slice(-REDACT_SUFFIX)}`;
165
+ async function resolveStoredApiKeyForProbe(
166
+ providerId: string,
167
+ inputStore: ProviderSetupCredentialStore | undefined,
168
+ ): Promise<string | undefined> {
169
+ let store = inputStore;
170
+ let ownedStore: AuthStorageImpl | undefined;
171
+ if (!store) {
172
+ ownedStore = await AuthStorageImpl.create(getAgentDbPath());
173
+ store = ownedStore;
174
+ }
175
+ try {
176
+ const storageProvider = resolveOAuthStorageProvider(providerId);
177
+ const snapshot = store.exportSnapshot();
178
+ const hasStoredApiKey = snapshot.credentials.some(
179
+ entry => entry.provider === storageProvider && entry.credential.type === "api_key",
180
+ );
181
+ if (!hasStoredApiKey) return undefined;
182
+ if (store.peekApiKey) return (await store.peekApiKey(providerId))?.trim() || undefined;
183
+ const entry = snapshot.credentials.find(
184
+ candidate => candidate.provider === storageProvider && candidate.credential.type === "api_key",
185
+ );
186
+ return entry?.credential.type === "api_key" ? entry.credential.key.trim() || undefined : undefined;
187
+ } finally {
188
+ ownedStore?.close();
189
+ }
190
+ }
191
+
192
+ export function redactSecret(_secret: string): string {
193
+ return "***";
125
194
  }
126
195
 
127
196
  function apiForCompatibility(compatibility: ProviderCompatibility): ProviderSetupApi {
@@ -140,6 +209,7 @@ function resolvePresetInput(input: ProviderSetupInput): {
140
209
  api: ProviderSetupApi;
141
210
  compat?: ProviderCompatConfig;
142
211
  discovery?: ProviderDiscovery;
212
+ requestedDiscovery: boolean;
143
213
  } {
144
214
  const preset = input.preset ? findProviderPreset(input.preset) : undefined;
145
215
  if (input.preset && !preset) {
@@ -181,6 +251,14 @@ function resolvePresetInput(input: ProviderSetupInput): {
181
251
  if (!compatibility) {
182
252
  throw new Error("Provider compatibility is required unless --preset is used.");
183
253
  }
254
+ if (input.discover && preset) {
255
+ throw new Error(
256
+ `Provider preset '${preset.id}' manages its own model catalog; omit --discover or use --compat openai for a custom provider.`,
257
+ );
258
+ }
259
+ if (input.discover && compatibility !== "openai") {
260
+ throw new Error("Model discovery (--discover) requires an OpenAI-compatible provider; use --compat openai.");
261
+ }
184
262
  return {
185
263
  compatibility,
186
264
  preset,
@@ -193,6 +271,7 @@ function resolvePresetInput(input: ProviderSetupInput): {
193
271
  api: preset?.api ?? apiForCompatibility(compatibility),
194
272
  compat: preset?.compat,
195
273
  discovery: preset?.discovery,
274
+ requestedDiscovery: input.discover === true,
196
275
  };
197
276
  }
198
277
 
@@ -261,8 +340,13 @@ function validateSetupInput(input: ProviderSetupInput): {
261
340
  if (!apiKey) throw new Error("API key is required.");
262
341
 
263
342
  const models = parseModelList(resolved.models);
264
- if (models.length === 0 && !resolved.discovery)
265
- throw new Error("At least one model id or model discovery is required.");
343
+ // An explicit `--discover` on an OpenAI-compatible custom provider persists
344
+ // `discovery: { type: openai-models-list }`, which the runtime also
345
+ // auto-enables — so manual models become optional rather than required.
346
+ const discovery: ProviderDiscovery | undefined =
347
+ resolved.discovery ?? (resolved.requestedDiscovery ? { type: "openai-models-list" } : undefined);
348
+ if (models.length === 0 && !discovery)
349
+ throw new Error("At least one model id or model discovery is required. Use --model <id> or --discover.");
266
350
  validateModelApi(resolved.modelApi, models, resolved.preset?.id ?? resolved.providerId);
267
351
 
268
352
  return {
@@ -275,7 +359,7 @@ function validateSetupInput(input: ProviderSetupInput): {
275
359
  api: resolved.api,
276
360
  modelApi: resolved.modelApi,
277
361
  compat: resolved.compat,
278
- discovery: resolved.discovery,
362
+ discovery,
279
363
  preset: resolved.preset,
280
364
  };
281
365
  }
@@ -295,7 +379,7 @@ async function readModelsConfig(modelsPath: string): Promise<ModelsConfig> {
295
379
  return checked.data;
296
380
  }
297
381
 
298
- async function writeModelsConfig(modelsPath: string, config: ModelsConfig): Promise<void> {
382
+ async function writeModelsConfig(modelsPath: string, config: ModelsConfig, signal?: AbortSignal): Promise<void> {
299
383
  const checked = ModelsConfigSchema.safeParse(config);
300
384
  if (!checked.success) {
301
385
  const first = checked.error.issues[0];
@@ -313,6 +397,8 @@ async function writeModelsConfig(modelsPath: string, config: ModelsConfig): Prom
313
397
  } finally {
314
398
  await tempHandle.close();
315
399
  }
400
+ // Rename is the commit boundary. Cancellation after it cannot undo the saved provider.
401
+ if (signal?.aborted) throw new Error("Provider setup was cancelled; setup did not write any config.");
316
402
  await fs.rename(tempPath, modelsPath);
317
403
  try {
318
404
  const directoryHandle = await fs.open(directory, "r");
@@ -336,6 +422,45 @@ export async function addApiCompatibleProvider(input: ProviderSetupInput): Promi
336
422
  if (existing.providers?.[validated.providerId] && !input.force) {
337
423
  throw new Error(`Provider '${validated.providerId}' already exists. Use --force to replace it.`);
338
424
  }
425
+ // P2: a discovery-only add with no manual models must prove the endpoint
426
+ // serves a catalog before config is written — otherwise an unreachable
427
+ // endpoint or rejected credential would be recorded as configured while
428
+ // the provider can never serve a model. Manual-model adds skip the probe
429
+ // (the static list degrades gracefully). The check runs after the
430
+ // duplicate check above so a duplicate is rejected locally before any
431
+ // credential crosses the network.
432
+ // Only explicitly requested custom `--discover` setups are probe-gated.
433
+ // Preset-managed discovery (goat, litellm, openai-compatible-proxy, ...)
434
+ // keeps its documented contract — presets write env-var references and
435
+ // never validate live credentials — so preset adds skip the probe and
436
+ // resolve their catalog lazily at runtime as before.
437
+ if (validated.discovery?.type === "openai-models-list" && validated.models.length === 0 && !validated.preset) {
438
+ const probe = input.probeDiscovery ?? probeOpenAIModelsList;
439
+ const credentialSource = validated.credentialSource;
440
+ const storedApiKey =
441
+ credentialSource === "env" && input.force
442
+ ? await resolveStoredApiKeyForProbe(validated.providerId, input.authStorage)
443
+ : undefined;
444
+ const probed = await probe({
445
+ baseUrl: validated.baseUrl,
446
+ apiKeyEnv: credentialSource === "env" && !storedApiKey ? validated.apiKey : undefined,
447
+ apiKey: credentialSource === "literal" ? validated.apiKey : storedApiKey,
448
+ signal: input.discoverySignal,
449
+ });
450
+ // The caller (wizard revision/cancel) may have aborted while the probe
451
+ // was resolving: recheck before any credential or config write so a
452
+ // stale revision cannot be persisted after dismissal.
453
+ if (input.discoverySignal?.aborted) {
454
+ throw new Error(
455
+ `Model discovery for '${validated.providerId}' was cancelled; setup did not write any config.`,
456
+ );
457
+ }
458
+ if (probed.models.length === 0) {
459
+ throw new Error(
460
+ `Model discovery for '${validated.providerId}' returned no models; add --model <id> or fix the endpoint catalog.`,
461
+ );
462
+ }
463
+ }
339
464
  const provider: ProviderConfig = {
340
465
  baseUrl: validated.baseUrl,
341
466
  api: validated.api,
@@ -353,22 +478,97 @@ export async function addApiCompatibleProvider(input: ProviderSetupInput): Promi
353
478
  if (validated.discovery) provider.discovery = validated.discovery;
354
479
  if (validated.credentialSource === "env") {
355
480
  provider.apiKeyEnv = validated.apiKey;
356
- } else {
357
- const authStorage = await AuthStorage.create(getAgentDbPath());
481
+ }
482
+ // Serialize config merging under the file lock. Credential storage is a
483
+ // separate authority, not an atomic transaction with models.yml. Publish the
484
+ // new config first and only then update the credential authority so a live
485
+ // session can never send a replacement key to the old base URL.
486
+ type CredentialStore = ProviderSetupCredentialStore;
487
+ const cancelledError = (): Error =>
488
+ new Error(`Model discovery for '${validated.providerId}' was cancelled; setup did not write any config.`);
489
+ // A cancellable replacement must not overwrite OAuth rows that cannot be
490
+ // restored from the redacted snapshot. The check is read-only and happens
491
+ // before the config commit; credentials are never rolled back after a
492
+ // concurrent writer can have changed them.
493
+ const hasUnrestorableCredentials = (store: CredentialStore): boolean => {
494
+ const storageProvider = resolveOAuthStorageProvider(validated.providerId);
495
+ const priorEntries = store.exportSnapshot().credentials.filter(entry => entry.provider === storageProvider);
496
+ return priorEntries.some(entry => entry.credential.type !== "api_key");
497
+ };
498
+ const withCredentialStore = async (fn: (store: CredentialStore) => Promise<void>): Promise<void> => {
499
+ if (input.authStorage) {
500
+ await fn(input.authStorage);
501
+ return;
502
+ }
503
+ const authStorage = await AuthStorageImpl.create(getAgentDbPath());
358
504
  try {
359
- await authStorage.set(validated.providerId, { type: "api_key", key: validated.apiKey });
505
+ await fn(authStorage);
360
506
  } finally {
361
507
  authStorage.close();
362
508
  }
363
- }
364
- const next: ModelsConfig = {
365
- ...existing,
366
- providers: {
367
- ...(existing.providers ?? {}),
368
- [validated.providerId]: provider,
369
- },
370
509
  };
371
- await writeModelsConfig(modelsPath, next);
510
+ await withFileLock(modelsPath, async () => {
511
+ if (input.discoverySignal?.aborted) {
512
+ throw cancelledError();
513
+ }
514
+ const current = await readModelsConfig(modelsPath);
515
+ if (input.discoverySignal?.aborted) throw cancelledError();
516
+ if (current.providers?.[validated.providerId] && !input.force) {
517
+ throw new Error(`Provider '${validated.providerId}' already exists. Use --force to replace it.`);
518
+ }
519
+ if (validated.credentialSource !== "env" && input.discoverySignal) {
520
+ await withCredentialStore(async store => {
521
+ if (hasUnrestorableCredentials(store)) {
522
+ throw new Error(
523
+ `Provider '${validated.providerId}' holds non-API-key credentials that cannot be restored if setup is cancelled; remove them first or omit --force.`,
524
+ );
525
+ }
526
+ });
527
+ }
528
+ if (input.discoverySignal?.aborted) throw cancelledError();
529
+ const configExistedBeforeCommit = await Bun.file(modelsPath).exists();
530
+ let configCommitted = false;
531
+ try {
532
+ await writeModelsConfig(
533
+ modelsPath,
534
+ {
535
+ ...current,
536
+ providers: {
537
+ ...(current.providers ?? {}),
538
+ [validated.providerId]: provider,
539
+ },
540
+ },
541
+ input.discoverySignal,
542
+ );
543
+ configCommitted = true;
544
+ // Cancellation after the atomic config rename cannot undo the saved
545
+ // provider. Finish publishing the matching literal credential even if
546
+ // the caller dismissed the wizard during the rename.
547
+ if (validated.credentialSource !== "env") {
548
+ await withCredentialStore(store =>
549
+ store.set(validated.providerId, { type: "api_key", key: validated.apiKey }),
550
+ );
551
+ }
552
+ } catch (error) {
553
+ if (configCommitted) {
554
+ try {
555
+ if (configExistedBeforeCommit) await writeModelsConfig(modelsPath, current);
556
+ else await fs.rm(modelsPath, { force: true });
557
+ } catch {
558
+ // Store/config errors can contain credentials; never log or
559
+ // interpolate them. Log independently of the wizard, which may
560
+ // already be dismissed.
561
+ const message = `Provider '${validated.providerId}' setup failed while publishing credentials and could not restore the previous config; credential recovery is required.`;
562
+ logger.error(message);
563
+ throw new Error(message, { cause: error });
564
+ }
565
+ throw new Error(
566
+ `Provider '${validated.providerId}' setup could not publish credentials; the previous config was restored.`,
567
+ );
568
+ }
569
+ throw error;
570
+ }
571
+ });
372
572
  return {
373
573
  providerId: validated.providerId,
374
574
  compatibility: validated.compatibility,
@@ -378,6 +578,8 @@ export async function addApiCompatibleProvider(input: ProviderSetupInput): Promi
378
578
  modelsPath,
379
579
  redactedApiKey: redactSecret(validated.apiKey),
380
580
  credentialSource: validated.credentialSource,
581
+ discoveryEnabled: validated.discovery !== undefined,
582
+ discoveryType: validated.discovery?.type,
381
583
  preset: validated.preset?.id,
382
584
  presetName: validated.preset?.name,
383
585
  };
@@ -394,11 +596,182 @@ function isLocalHttpHost(hostname: string): boolean {
394
596
  );
395
597
  }
396
598
 
599
+ export interface ProviderDiscoveryProbeInput {
600
+ baseUrl: string;
601
+ apiKeyEnv?: string;
602
+ apiKey?: string;
603
+ timeoutMs?: number;
604
+ signal?: AbortSignal;
605
+ }
606
+
607
+ export interface ProviderDiscoveryProbeResult {
608
+ models: string[];
609
+ endpoint: string;
610
+ }
611
+
612
+ function redactProbeUrl(value: URL): string {
613
+ return `${value.origin}${value.pathname}`;
614
+ }
615
+
616
+ function normalizeProbeBaseUrl(rawBaseUrl: string): URL {
617
+ const parsed = new URL(rawBaseUrl.trim());
618
+ const trimmedPath = parsed.pathname.replace(/\/+$/g, "");
619
+ parsed.pathname = trimmedPath.endsWith("/v1") ? trimmedPath || "/v1" : `${trimmedPath}/v1`;
620
+ parsed.hash = "";
621
+ return parsed;
622
+ }
623
+
624
+ /**
625
+ * Probe an OpenAI-compatible `/v1/models` endpoint and return the sorted
626
+ * model ids. Used by the Add-custom-provider wizard so gateway catalogs
627
+ * (LiteLLM and friends) do not need manual transcription.
628
+ *
629
+ * Credential handling: an `apiKeyEnv` name resolves through the trusted
630
+ * credential env only (never `cwd/.env`); a literal `apiKey` is used as-is.
631
+ * Errors name the redacted endpoint only — the key material never appears
632
+ * in messages, and callers must not log the request headers.
633
+ */
634
+ export async function probeOpenAIModelsList(input: ProviderDiscoveryProbeInput): Promise<ProviderDiscoveryProbeResult> {
635
+ let baseUrl: URL;
636
+ try {
637
+ baseUrl = normalizeProbeBaseUrl(input.baseUrl);
638
+ } catch {
639
+ throw new Error("Model discovery needs a valid absolute base URL before probing.");
640
+ }
641
+ if (baseUrl.protocol !== "https:" && baseUrl.protocol !== "http:") {
642
+ throw new Error("Model discovery needs an http or https base URL.");
643
+ }
644
+ // P1: never transmit the bearer before the URL passes the same
645
+ // HTTPS-unless-loopback validation that setup itself enforces — a remote
646
+ // http:// URL must be rejected before any secret crosses the network.
647
+ if (baseUrl.protocol === "http:" && !isLocalHttpHost(baseUrl.hostname)) {
648
+ throw new Error("Model discovery needs https unless the endpoint targets localhost or a loopback address.");
649
+ }
650
+ const modelsUrl = new URL(baseUrl);
651
+ modelsUrl.pathname = `${modelsUrl.pathname.replace(/\/+$/g, "")}/models`;
652
+ const endpoint = redactProbeUrl(modelsUrl);
653
+ if (input.signal?.aborted) {
654
+ throw new Error(`Model discovery for ${endpoint} was cancelled before probing.`);
655
+ }
656
+ // Rotating reader (same as the model registry): an agent-.env credential
657
+ // rotated or removed after startup must be honored live, not served from
658
+ // the import-time snapshot.
659
+ const apiKey = input.apiKeyEnv ? $rotatingCredentialEnv(input.apiKeyEnv)?.trim() : input.apiKey?.trim();
660
+ if (!apiKey) {
661
+ throw new Error(
662
+ input.apiKeyEnv
663
+ ? `Discovery needs ${input.apiKeyEnv} set in the trusted environment before ${endpoint} can be probed.`
664
+ : `Discovery needs an API key before ${endpoint} can be probed.`,
665
+ );
666
+ }
667
+ const headers: Record<string, string> = {};
668
+ let response: Response;
669
+ try {
670
+ response = await fetch(modelsUrl, {
671
+ headers: { ...headers, Authorization: `Bearer ${apiKey}` },
672
+ // Combine the caller signal with the shared deadline: a caller
673
+ // signal alone must never leave the request without a timeout.
674
+ signal: AbortSignal.any([
675
+ ...(input.signal ? [input.signal] : []),
676
+ AbortSignal.timeout(input.timeoutMs ?? MODELS_LIST_REQUEST_TIMEOUT_MS),
677
+ ]),
678
+ });
679
+ } catch (error) {
680
+ // Transport layers may echo request details (including the bearer)
681
+ // in failure text: scrub resolved credential material before the
682
+ // reason reaches any error surface.
683
+ const reason = error instanceof Error ? error.message : String(error);
684
+ const scrubbed = apiKey ? reason.split(apiKey).join("[redacted]") : reason;
685
+ throw new Error(`Model discovery failed for ${endpoint}: ${scrubbed}`);
686
+ }
687
+ if (!response.ok) {
688
+ if (response.status === 401 || response.status === 403) {
689
+ throw new Error(
690
+ `Model discovery rejected by ${endpoint} (HTTP ${response.status}): the credential was rejected; check the API key.`,
691
+ );
692
+ }
693
+ throw new Error(`Model discovery failed for ${endpoint}: HTTP ${response.status}.`);
694
+ }
695
+ // Shared bounded reader (1MB cap): an unbounded response.json() would let
696
+ // a malicious endpoint exhaust process memory before ID filtering runs.
697
+ let payload: unknown;
698
+ try {
699
+ payload = await readBoundedModelsJson(response);
700
+ } catch (error) {
701
+ const reason = error instanceof Error ? error.message : String(error);
702
+ if (reason.includes("exceeds the size limit")) {
703
+ throw new Error(`Model discovery failed for ${endpoint}: the response exceeds the size limit.`);
704
+ }
705
+ throw new Error(`Model discovery failed for ${endpoint}: the response was not valid JSON.`);
706
+ }
707
+ if (typeof payload !== "object" || payload === null || !Array.isArray((payload as { data?: unknown }).data)) {
708
+ throw new Error(`Model discovery failed for ${endpoint}: the response was not an OpenAI models list.`);
709
+ }
710
+ const ids = new Set<string>();
711
+ for (const item of (payload as { data: unknown[] }).data) {
712
+ // P1: the wire catalog is attacker-controlled — drop IDs that fail the
713
+ // shared catalog safety check (control chars, OSC/ANSI, oversize)
714
+ // before they can reach the setup-screen renderer.
715
+ const rawId = typeof item === "object" && item !== null ? (item as { id?: unknown }).id : undefined;
716
+ if (!isSafeCatalogModelId(rawId)) continue;
717
+ ids.add(rawId.trim());
718
+ }
719
+ return { models: [...ids].sort((a, b) => a.localeCompare(b)), endpoint };
720
+ }
721
+
722
+ export interface DiscoveryCatalogRefresher {
723
+ refresh(mode: "offline" | "online" | "online-if-uncached", credentialSessionId?: string): Promise<void>;
724
+ refreshProvider(
725
+ providerId: string,
726
+ strategy?: "offline" | "online" | "online-if-uncached",
727
+ credentialSessionId?: string,
728
+ ): Promise<void>;
729
+ getProviderDiscoveryState(providerId: string): { status: string; error?: string } | undefined;
730
+ }
731
+
732
+ /**
733
+ * Reload static config offline, then refresh only the newly added discovery
734
+ * provider online so freshly requested live catalogs are selectable
735
+ * immediately (including alongside manual models). Preset-managed
736
+ * discovery is excluded by callers: presets resolve lazily at runtime.
737
+ * Returns a recovery hint instead of reporting unconditional success when
738
+ * the live catalog stays unavailable: the config is saved and valid, but
739
+ * the user needs to know the catalog did not populate and how to recover
740
+ * (wait for the next refresh, or re-add with explicit `--model` ids).
741
+ */
742
+ export async function reloadAndRefreshDiscoveryCatalog(
743
+ registry: DiscoveryCatalogRefresher,
744
+ providerId: string,
745
+ credentialSessionId?: string,
746
+ ): Promise<string | null> {
747
+ await registry.refresh("offline", credentialSessionId);
748
+ try {
749
+ await registry.refreshProvider(providerId, "online", credentialSessionId);
750
+ } catch {
751
+ // Fall through to the status read: discovery failures surface as
752
+ // state, not rejections.
753
+ }
754
+ const state = registry.getProviderDiscoveryState(providerId);
755
+ if (state?.status === "ok") return null;
756
+ const detail = state?.error ? ` (${state.error})` : "";
757
+ return (
758
+ `Live catalog unavailable${detail}; the provider is saved and will populate on the next online refresh. ` +
759
+ `To pin models now, re-add with --force --model <id> (the provider already exists).`
760
+ );
761
+ }
762
+
397
763
  export function formatProviderSetupResult(result: ProviderSetupResult): string {
398
764
  return [
399
765
  `Provider '${result.providerId}' configured as ${result.compatibility}-compatible.`,
400
766
  ...(result.presetName ? [`Preset: ${result.presetName}`] : []),
401
- `Models: ${result.modelIds.length > 0 ? result.modelIds.join(", ") : "discovered automatically"}`,
767
+ `Models: ${result.modelIds.length > 0 ? result.modelIds.join(", ") : result.discoveryEnabled ? "discovered automatically" : "(none)"}`,
768
+ ...(result.discoveryEnabled
769
+ ? [
770
+ result.discoveryType === "openai-models-list"
771
+ ? "Discovery: live OpenAI /v1/models catalog"
772
+ : `Discovery: live ${result.discoveryType ?? "provider"} catalog`,
773
+ ]
774
+ : []),
402
775
  `Base URL: ${result.baseUrl}`,
403
776
  `API key: ${result.credentialSource === "env" ? `${result.redactedApiKey} (environment variable)` : result.redactedApiKey}`,
404
777
  `Config: ${result.modelsPath}`,