@nghyane/arcane 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (738) hide show
  1. package/CHANGELOG.md +3 -0
  2. package/README.md +12 -0
  3. package/examples/README.md +21 -0
  4. package/examples/custom-tools/README.md +109 -0
  5. package/examples/custom-tools/hello/index.ts +20 -0
  6. package/examples/custom-tools/todo/index.ts +206 -0
  7. package/examples/extensions/README.md +143 -0
  8. package/examples/extensions/api-demo.ts +89 -0
  9. package/examples/extensions/chalk-logger.ts +25 -0
  10. package/examples/extensions/hello.ts +32 -0
  11. package/examples/extensions/pirate.ts +43 -0
  12. package/examples/extensions/plan-mode.ts +550 -0
  13. package/examples/extensions/reload-runtime.ts +37 -0
  14. package/examples/extensions/todo.ts +296 -0
  15. package/examples/extensions/tools.ts +144 -0
  16. package/examples/extensions/with-deps/index.ts +35 -0
  17. package/examples/extensions/with-deps/package-lock.json +31 -0
  18. package/examples/extensions/with-deps/package.json +16 -0
  19. package/examples/hooks/README.md +56 -0
  20. package/examples/hooks/auto-commit-on-exit.ts +48 -0
  21. package/examples/hooks/confirm-destructive.ts +58 -0
  22. package/examples/hooks/custom-compaction.ts +116 -0
  23. package/examples/hooks/dirty-repo-guard.ts +51 -0
  24. package/examples/hooks/file-trigger.ts +40 -0
  25. package/examples/hooks/git-checkpoint.ts +52 -0
  26. package/examples/hooks/handoff.ts +150 -0
  27. package/examples/hooks/permission-gate.ts +33 -0
  28. package/examples/hooks/protected-paths.ts +29 -0
  29. package/examples/hooks/qna.ts +119 -0
  30. package/examples/hooks/status-line.ts +39 -0
  31. package/examples/sdk/01-minimal.ts +21 -0
  32. package/examples/sdk/02-custom-model.ts +49 -0
  33. package/examples/sdk/03-custom-prompt.ts +43 -0
  34. package/examples/sdk/04-skills.ts +43 -0
  35. package/examples/sdk/06-extensions.ts +80 -0
  36. package/examples/sdk/06-hooks.ts +61 -0
  37. package/examples/sdk/07-context-files.ts +35 -0
  38. package/examples/sdk/08-prompt-templates.ts +36 -0
  39. package/examples/sdk/08-slash-commands.ts +41 -0
  40. package/examples/sdk/09-api-keys-and-oauth.ts +54 -0
  41. package/examples/sdk/11-sessions.ts +47 -0
  42. package/examples/sdk/README.md +150 -0
  43. package/package.json +464 -0
  44. package/scripts/format-prompts.ts +184 -0
  45. package/scripts/generate-docs-index.ts +40 -0
  46. package/scripts/generate-template.ts +32 -0
  47. package/src/bun-imports.d.ts +22 -0
  48. package/src/capability/context-file.ts +39 -0
  49. package/src/capability/extension-module.ts +33 -0
  50. package/src/capability/extension.ts +47 -0
  51. package/src/capability/fs.ts +89 -0
  52. package/src/capability/hook.ts +39 -0
  53. package/src/capability/index.ts +432 -0
  54. package/src/capability/instruction.ts +36 -0
  55. package/src/capability/mcp.ts +60 -0
  56. package/src/capability/prompt.ts +34 -0
  57. package/src/capability/rule.ts +223 -0
  58. package/src/capability/settings.ts +34 -0
  59. package/src/capability/skill.ts +48 -0
  60. package/src/capability/slash-command.ts +39 -0
  61. package/src/capability/ssh.ts +41 -0
  62. package/src/capability/system-prompt.ts +34 -0
  63. package/src/capability/tool.ts +37 -0
  64. package/src/capability/types.ts +156 -0
  65. package/src/cli/args.ts +259 -0
  66. package/src/cli/config-cli.ts +357 -0
  67. package/src/cli/file-processor.ts +124 -0
  68. package/src/cli/grep-cli.ts +152 -0
  69. package/src/cli/jupyter-cli.ts +106 -0
  70. package/src/cli/list-models.ts +103 -0
  71. package/src/cli/plugin-cli.ts +661 -0
  72. package/src/cli/session-picker.ts +42 -0
  73. package/src/cli/setup-cli.ts +376 -0
  74. package/src/cli/shell-cli.ts +174 -0
  75. package/src/cli/ssh-cli.ts +179 -0
  76. package/src/cli/stats-cli.ts +197 -0
  77. package/src/cli/update-cli.ts +286 -0
  78. package/src/cli/web-search-cli.ts +143 -0
  79. package/src/cli.ts +65 -0
  80. package/src/commands/commit.ts +36 -0
  81. package/src/commands/config.ts +51 -0
  82. package/src/commands/grep.ts +41 -0
  83. package/src/commands/jupyter.ts +32 -0
  84. package/src/commands/launch.ts +139 -0
  85. package/src/commands/plugin.ts +70 -0
  86. package/src/commands/setup.ts +42 -0
  87. package/src/commands/shell.ts +29 -0
  88. package/src/commands/ssh.ts +60 -0
  89. package/src/commands/stats.ts +29 -0
  90. package/src/commands/update.ts +21 -0
  91. package/src/commands/web-search.ts +42 -0
  92. package/src/commit/agentic/agent.ts +311 -0
  93. package/src/commit/agentic/fallback.ts +96 -0
  94. package/src/commit/agentic/index.ts +359 -0
  95. package/src/commit/agentic/prompts/analyze-file.md +22 -0
  96. package/src/commit/agentic/prompts/session-user.md +25 -0
  97. package/src/commit/agentic/prompts/split-confirm.md +1 -0
  98. package/src/commit/agentic/prompts/system.md +38 -0
  99. package/src/commit/agentic/state.ts +69 -0
  100. package/src/commit/agentic/tools/analyze-file.ts +118 -0
  101. package/src/commit/agentic/tools/git-file-diff.ts +194 -0
  102. package/src/commit/agentic/tools/git-hunk.ts +50 -0
  103. package/src/commit/agentic/tools/git-overview.ts +84 -0
  104. package/src/commit/agentic/tools/index.ts +56 -0
  105. package/src/commit/agentic/tools/propose-changelog.ts +128 -0
  106. package/src/commit/agentic/tools/propose-commit.ts +154 -0
  107. package/src/commit/agentic/tools/recent-commits.ts +81 -0
  108. package/src/commit/agentic/tools/split-commit.ts +280 -0
  109. package/src/commit/agentic/topo-sort.ts +44 -0
  110. package/src/commit/agentic/trivial.ts +51 -0
  111. package/src/commit/agentic/validation.ts +200 -0
  112. package/src/commit/analysis/conventional.ts +165 -0
  113. package/src/commit/analysis/index.ts +4 -0
  114. package/src/commit/analysis/scope.ts +242 -0
  115. package/src/commit/analysis/summary.ts +112 -0
  116. package/src/commit/analysis/validation.ts +66 -0
  117. package/src/commit/changelog/detect.ts +37 -0
  118. package/src/commit/changelog/generate.ts +110 -0
  119. package/src/commit/changelog/index.ts +234 -0
  120. package/src/commit/changelog/parse.ts +44 -0
  121. package/src/commit/cli.ts +93 -0
  122. package/src/commit/git/diff.ts +148 -0
  123. package/src/commit/git/errors.ts +9 -0
  124. package/src/commit/git/index.ts +211 -0
  125. package/src/commit/git/operations.ts +54 -0
  126. package/src/commit/index.ts +5 -0
  127. package/src/commit/map-reduce/index.ts +64 -0
  128. package/src/commit/map-reduce/map-phase.ts +178 -0
  129. package/src/commit/map-reduce/reduce-phase.ts +145 -0
  130. package/src/commit/map-reduce/utils.ts +9 -0
  131. package/src/commit/message.ts +11 -0
  132. package/src/commit/model-selection.ts +69 -0
  133. package/src/commit/pipeline.ts +243 -0
  134. package/src/commit/prompts/analysis-system.md +148 -0
  135. package/src/commit/prompts/analysis-user.md +38 -0
  136. package/src/commit/prompts/changelog-system.md +50 -0
  137. package/src/commit/prompts/changelog-user.md +18 -0
  138. package/src/commit/prompts/file-observer-system.md +24 -0
  139. package/src/commit/prompts/file-observer-user.md +8 -0
  140. package/src/commit/prompts/reduce-system.md +50 -0
  141. package/src/commit/prompts/reduce-user.md +17 -0
  142. package/src/commit/prompts/summary-retry.md +3 -0
  143. package/src/commit/prompts/summary-system.md +38 -0
  144. package/src/commit/prompts/summary-user.md +13 -0
  145. package/src/commit/prompts/types-description.md +2 -0
  146. package/src/commit/types.ts +109 -0
  147. package/src/commit/utils/exclusions.ts +42 -0
  148. package/src/config/file-lock.ts +121 -0
  149. package/src/config/keybindings.ts +280 -0
  150. package/src/config/model-registry.ts +1140 -0
  151. package/src/config/model-resolver.ts +812 -0
  152. package/src/config/prompt-templates.ts +526 -0
  153. package/src/config/resolve-config-value.ts +92 -0
  154. package/src/config/settings-schema.ts +1236 -0
  155. package/src/config/settings.ts +706 -0
  156. package/src/config.ts +414 -0
  157. package/src/cursor.ts +239 -0
  158. package/src/debug/index.ts +431 -0
  159. package/src/debug/log-formatting.ts +60 -0
  160. package/src/debug/log-viewer.ts +903 -0
  161. package/src/debug/profiler.ts +158 -0
  162. package/src/debug/report-bundle.ts +366 -0
  163. package/src/debug/system-info.ts +112 -0
  164. package/src/discovery/agents-md.ts +68 -0
  165. package/src/discovery/agents.ts +199 -0
  166. package/src/discovery/builtin.ts +815 -0
  167. package/src/discovery/claude-plugins.ts +205 -0
  168. package/src/discovery/claude.ts +506 -0
  169. package/src/discovery/cline.ts +83 -0
  170. package/src/discovery/codex.ts +532 -0
  171. package/src/discovery/cursor.ts +218 -0
  172. package/src/discovery/gemini.ts +395 -0
  173. package/src/discovery/github.ts +117 -0
  174. package/src/discovery/helpers.ts +698 -0
  175. package/src/discovery/index.ts +89 -0
  176. package/src/discovery/mcp-json.ts +156 -0
  177. package/src/discovery/opencode.ts +394 -0
  178. package/src/discovery/ssh.ts +160 -0
  179. package/src/discovery/vscode.ts +103 -0
  180. package/src/discovery/windsurf.ts +145 -0
  181. package/src/exa/company.ts +57 -0
  182. package/src/exa/index.ts +62 -0
  183. package/src/exa/linkedin.ts +57 -0
  184. package/src/exa/mcp-client.ts +289 -0
  185. package/src/exa/render.ts +244 -0
  186. package/src/exa/researcher.ts +89 -0
  187. package/src/exa/search.ts +330 -0
  188. package/src/exa/types.ts +166 -0
  189. package/src/exa/websets.ts +247 -0
  190. package/src/exec/bash-executor.ts +184 -0
  191. package/src/exec/exec.ts +53 -0
  192. package/src/export/custom-share.ts +65 -0
  193. package/src/export/html/index.ts +162 -0
  194. package/src/export/html/template.css +889 -0
  195. package/src/export/html/template.generated.ts +2 -0
  196. package/src/export/html/template.html +45 -0
  197. package/src/export/html/template.js +1329 -0
  198. package/src/export/html/template.macro.ts +24 -0
  199. package/src/export/html/vendor/highlight.min.js +1213 -0
  200. package/src/export/html/vendor/marked.min.js +6 -0
  201. package/src/export/ttsr.ts +434 -0
  202. package/src/extensibility/custom-commands/bundled/review/index.ts +433 -0
  203. package/src/extensibility/custom-commands/index.ts +15 -0
  204. package/src/extensibility/custom-commands/loader.ts +231 -0
  205. package/src/extensibility/custom-commands/types.ts +111 -0
  206. package/src/extensibility/custom-tools/index.ts +22 -0
  207. package/src/extensibility/custom-tools/loader.ts +235 -0
  208. package/src/extensibility/custom-tools/types.ts +226 -0
  209. package/src/extensibility/custom-tools/wrapper.ts +45 -0
  210. package/src/extensibility/extensions/index.ts +136 -0
  211. package/src/extensibility/extensions/loader.ts +520 -0
  212. package/src/extensibility/extensions/runner.ts +774 -0
  213. package/src/extensibility/extensions/types.ts +1293 -0
  214. package/src/extensibility/extensions/wrapper.ts +188 -0
  215. package/src/extensibility/hooks/index.ts +16 -0
  216. package/src/extensibility/hooks/loader.ts +273 -0
  217. package/src/extensibility/hooks/runner.ts +441 -0
  218. package/src/extensibility/hooks/tool-wrapper.ts +106 -0
  219. package/src/extensibility/hooks/types.ts +817 -0
  220. package/src/extensibility/plugins/doctor.ts +65 -0
  221. package/src/extensibility/plugins/git-url.ts +281 -0
  222. package/src/extensibility/plugins/index.ts +33 -0
  223. package/src/extensibility/plugins/installer.ts +192 -0
  224. package/src/extensibility/plugins/loader.ts +338 -0
  225. package/src/extensibility/plugins/manager.ts +716 -0
  226. package/src/extensibility/plugins/parser.ts +105 -0
  227. package/src/extensibility/plugins/types.ts +190 -0
  228. package/src/extensibility/skills.ts +385 -0
  229. package/src/extensibility/slash-commands.ts +287 -0
  230. package/src/extensibility/tool-proxy.ts +25 -0
  231. package/src/index.ts +275 -0
  232. package/src/internal-urls/agent-protocol.ts +136 -0
  233. package/src/internal-urls/artifact-protocol.ts +97 -0
  234. package/src/internal-urls/docs-index.generated.ts +54 -0
  235. package/src/internal-urls/docs-protocol.ts +84 -0
  236. package/src/internal-urls/index.ts +31 -0
  237. package/src/internal-urls/json-query.ts +126 -0
  238. package/src/internal-urls/memory-protocol.ts +133 -0
  239. package/src/internal-urls/router.ts +70 -0
  240. package/src/internal-urls/rule-protocol.ts +55 -0
  241. package/src/internal-urls/skill-protocol.ts +111 -0
  242. package/src/internal-urls/types.ts +52 -0
  243. package/src/ipy/executor.ts +556 -0
  244. package/src/ipy/gateway-coordinator.ts +426 -0
  245. package/src/ipy/kernel.ts +892 -0
  246. package/src/ipy/modules.ts +109 -0
  247. package/src/ipy/prelude.py +831 -0
  248. package/src/ipy/prelude.ts +3 -0
  249. package/src/ipy/runtime.ts +222 -0
  250. package/src/lsp/client.ts +867 -0
  251. package/src/lsp/clients/biome-client.ts +202 -0
  252. package/src/lsp/clients/index.ts +50 -0
  253. package/src/lsp/clients/lsp-linter-client.ts +93 -0
  254. package/src/lsp/clients/swiftlint-client.ts +120 -0
  255. package/src/lsp/config.ts +397 -0
  256. package/src/lsp/defaults.json +464 -0
  257. package/src/lsp/edits.ts +109 -0
  258. package/src/lsp/index.ts +1268 -0
  259. package/src/lsp/lspmux.ts +250 -0
  260. package/src/lsp/render.ts +689 -0
  261. package/src/lsp/types.ts +414 -0
  262. package/src/lsp/utils.ts +549 -0
  263. package/src/main.ts +773 -0
  264. package/src/mcp/client.ts +239 -0
  265. package/src/mcp/config-writer.ts +215 -0
  266. package/src/mcp/config.ts +363 -0
  267. package/src/mcp/index.ts +55 -0
  268. package/src/mcp/json-rpc.ts +84 -0
  269. package/src/mcp/loader.ts +124 -0
  270. package/src/mcp/manager.ts +490 -0
  271. package/src/mcp/oauth-discovery.ts +274 -0
  272. package/src/mcp/oauth-flow.ts +229 -0
  273. package/src/mcp/render.ts +123 -0
  274. package/src/mcp/tool-bridge.ts +372 -0
  275. package/src/mcp/tool-cache.ts +121 -0
  276. package/src/mcp/transports/http.ts +332 -0
  277. package/src/mcp/transports/index.ts +6 -0
  278. package/src/mcp/transports/stdio.ts +281 -0
  279. package/src/mcp/types.ts +248 -0
  280. package/src/memories/index.ts +1099 -0
  281. package/src/memories/storage.ts +563 -0
  282. package/src/modes/components/agent-dashboard.ts +1130 -0
  283. package/src/modes/components/assistant-message.ts +144 -0
  284. package/src/modes/components/bash-execution.ts +218 -0
  285. package/src/modes/components/bordered-loader.ts +41 -0
  286. package/src/modes/components/branch-summary-message.ts +45 -0
  287. package/src/modes/components/codemode-group.ts +369 -0
  288. package/src/modes/components/compaction-summary-message.ts +51 -0
  289. package/src/modes/components/countdown-timer.ts +46 -0
  290. package/src/modes/components/custom-editor.ts +181 -0
  291. package/src/modes/components/custom-message.ts +91 -0
  292. package/src/modes/components/diff.ts +186 -0
  293. package/src/modes/components/dynamic-border.ts +25 -0
  294. package/src/modes/components/extensions/extension-dashboard.ts +325 -0
  295. package/src/modes/components/extensions/extension-list.ts +484 -0
  296. package/src/modes/components/extensions/index.ts +9 -0
  297. package/src/modes/components/extensions/inspector-panel.ts +321 -0
  298. package/src/modes/components/extensions/state-manager.ts +586 -0
  299. package/src/modes/components/extensions/types.ts +191 -0
  300. package/src/modes/components/footer.ts +315 -0
  301. package/src/modes/components/history-search.ts +157 -0
  302. package/src/modes/components/hook-editor.ts +101 -0
  303. package/src/modes/components/hook-input.ts +72 -0
  304. package/src/modes/components/hook-message.ts +100 -0
  305. package/src/modes/components/hook-selector.ts +155 -0
  306. package/src/modes/components/index.ts +41 -0
  307. package/src/modes/components/keybinding-hints.ts +65 -0
  308. package/src/modes/components/login-dialog.ts +164 -0
  309. package/src/modes/components/mcp-add-wizard.ts +1295 -0
  310. package/src/modes/components/model-selector.ts +625 -0
  311. package/src/modes/components/oauth-selector.ts +210 -0
  312. package/src/modes/components/plugin-settings.ts +477 -0
  313. package/src/modes/components/python-execution.ts +196 -0
  314. package/src/modes/components/queue-mode-selector.ts +56 -0
  315. package/src/modes/components/read-tool-group.ts +119 -0
  316. package/src/modes/components/session-selector.ts +242 -0
  317. package/src/modes/components/settings-defs.ts +340 -0
  318. package/src/modes/components/settings-selector.ts +529 -0
  319. package/src/modes/components/show-images-selector.ts +45 -0
  320. package/src/modes/components/skill-message.ts +90 -0
  321. package/src/modes/components/status-line/index.ts +4 -0
  322. package/src/modes/components/status-line/presets.ts +94 -0
  323. package/src/modes/components/status-line/segments.ts +352 -0
  324. package/src/modes/components/status-line/separators.ts +55 -0
  325. package/src/modes/components/status-line/types.ts +75 -0
  326. package/src/modes/components/status-line-segment-editor.ts +354 -0
  327. package/src/modes/components/status-line.ts +421 -0
  328. package/src/modes/components/theme-selector.ts +63 -0
  329. package/src/modes/components/thinking-selector.ts +64 -0
  330. package/src/modes/components/todo-display.ts +115 -0
  331. package/src/modes/components/todo-reminder.ts +40 -0
  332. package/src/modes/components/tool-execution.ts +703 -0
  333. package/src/modes/components/tree-selector.ts +904 -0
  334. package/src/modes/components/ttsr-notification.ts +80 -0
  335. package/src/modes/components/user-message-selector.ts +146 -0
  336. package/src/modes/components/user-message.ts +22 -0
  337. package/src/modes/components/visual-truncate.ts +63 -0
  338. package/src/modes/components/welcome.ts +247 -0
  339. package/src/modes/controllers/command-controller.ts +1120 -0
  340. package/src/modes/controllers/event-controller.ts +479 -0
  341. package/src/modes/controllers/extension-ui-controller.ts +778 -0
  342. package/src/modes/controllers/input-controller.ts +671 -0
  343. package/src/modes/controllers/mcp-command-controller.ts +1315 -0
  344. package/src/modes/controllers/selector-controller.ts +712 -0
  345. package/src/modes/controllers/ssh-command-controller.ts +452 -0
  346. package/src/modes/index.ts +15 -0
  347. package/src/modes/interactive-mode.ts +1027 -0
  348. package/src/modes/print-mode.ts +191 -0
  349. package/src/modes/rpc/rpc-client.ts +583 -0
  350. package/src/modes/rpc/rpc-mode.ts +700 -0
  351. package/src/modes/rpc/rpc-types.ts +236 -0
  352. package/src/modes/theme/dark.json +95 -0
  353. package/src/modes/theme/defaults/alabaster.json +93 -0
  354. package/src/modes/theme/defaults/amethyst.json +96 -0
  355. package/src/modes/theme/defaults/anthracite.json +93 -0
  356. package/src/modes/theme/defaults/basalt.json +91 -0
  357. package/src/modes/theme/defaults/birch.json +95 -0
  358. package/src/modes/theme/defaults/dark-abyss.json +91 -0
  359. package/src/modes/theme/defaults/dark-arctic.json +104 -0
  360. package/src/modes/theme/defaults/dark-aurora.json +95 -0
  361. package/src/modes/theme/defaults/dark-catppuccin.json +107 -0
  362. package/src/modes/theme/defaults/dark-cavern.json +91 -0
  363. package/src/modes/theme/defaults/dark-copper.json +95 -0
  364. package/src/modes/theme/defaults/dark-cosmos.json +90 -0
  365. package/src/modes/theme/defaults/dark-cyberpunk.json +102 -0
  366. package/src/modes/theme/defaults/dark-dracula.json +98 -0
  367. package/src/modes/theme/defaults/dark-eclipse.json +91 -0
  368. package/src/modes/theme/defaults/dark-ember.json +95 -0
  369. package/src/modes/theme/defaults/dark-equinox.json +90 -0
  370. package/src/modes/theme/defaults/dark-forest.json +96 -0
  371. package/src/modes/theme/defaults/dark-github.json +105 -0
  372. package/src/modes/theme/defaults/dark-gruvbox.json +112 -0
  373. package/src/modes/theme/defaults/dark-lavender.json +95 -0
  374. package/src/modes/theme/defaults/dark-lunar.json +89 -0
  375. package/src/modes/theme/defaults/dark-midnight.json +95 -0
  376. package/src/modes/theme/defaults/dark-monochrome.json +94 -0
  377. package/src/modes/theme/defaults/dark-monokai.json +98 -0
  378. package/src/modes/theme/defaults/dark-nebula.json +90 -0
  379. package/src/modes/theme/defaults/dark-nord.json +97 -0
  380. package/src/modes/theme/defaults/dark-ocean.json +101 -0
  381. package/src/modes/theme/defaults/dark-one.json +100 -0
  382. package/src/modes/theme/defaults/dark-rainforest.json +91 -0
  383. package/src/modes/theme/defaults/dark-reef.json +91 -0
  384. package/src/modes/theme/defaults/dark-retro.json +92 -0
  385. package/src/modes/theme/defaults/dark-rose-pine.json +96 -0
  386. package/src/modes/theme/defaults/dark-sakura.json +95 -0
  387. package/src/modes/theme/defaults/dark-slate.json +95 -0
  388. package/src/modes/theme/defaults/dark-solarized.json +97 -0
  389. package/src/modes/theme/defaults/dark-solstice.json +90 -0
  390. package/src/modes/theme/defaults/dark-starfall.json +91 -0
  391. package/src/modes/theme/defaults/dark-sunset.json +99 -0
  392. package/src/modes/theme/defaults/dark-swamp.json +90 -0
  393. package/src/modes/theme/defaults/dark-synthwave.json +103 -0
  394. package/src/modes/theme/defaults/dark-taiga.json +91 -0
  395. package/src/modes/theme/defaults/dark-terminal.json +95 -0
  396. package/src/modes/theme/defaults/dark-tokyo-night.json +101 -0
  397. package/src/modes/theme/defaults/dark-tundra.json +91 -0
  398. package/src/modes/theme/defaults/dark-twilight.json +91 -0
  399. package/src/modes/theme/defaults/dark-volcanic.json +91 -0
  400. package/src/modes/theme/defaults/graphite.json +92 -0
  401. package/src/modes/theme/defaults/index.ts +195 -0
  402. package/src/modes/theme/defaults/light-arctic.json +107 -0
  403. package/src/modes/theme/defaults/light-aurora-day.json +91 -0
  404. package/src/modes/theme/defaults/light-canyon.json +91 -0
  405. package/src/modes/theme/defaults/light-catppuccin.json +106 -0
  406. package/src/modes/theme/defaults/light-cirrus.json +90 -0
  407. package/src/modes/theme/defaults/light-coral.json +95 -0
  408. package/src/modes/theme/defaults/light-cyberpunk.json +96 -0
  409. package/src/modes/theme/defaults/light-dawn.json +90 -0
  410. package/src/modes/theme/defaults/light-dunes.json +91 -0
  411. package/src/modes/theme/defaults/light-eucalyptus.json +95 -0
  412. package/src/modes/theme/defaults/light-forest.json +100 -0
  413. package/src/modes/theme/defaults/light-frost.json +95 -0
  414. package/src/modes/theme/defaults/light-github.json +115 -0
  415. package/src/modes/theme/defaults/light-glacier.json +91 -0
  416. package/src/modes/theme/defaults/light-gruvbox.json +108 -0
  417. package/src/modes/theme/defaults/light-haze.json +90 -0
  418. package/src/modes/theme/defaults/light-honeycomb.json +95 -0
  419. package/src/modes/theme/defaults/light-lagoon.json +91 -0
  420. package/src/modes/theme/defaults/light-lavender.json +95 -0
  421. package/src/modes/theme/defaults/light-meadow.json +91 -0
  422. package/src/modes/theme/defaults/light-mint.json +95 -0
  423. package/src/modes/theme/defaults/light-monochrome.json +101 -0
  424. package/src/modes/theme/defaults/light-ocean.json +99 -0
  425. package/src/modes/theme/defaults/light-one.json +99 -0
  426. package/src/modes/theme/defaults/light-opal.json +91 -0
  427. package/src/modes/theme/defaults/light-orchard.json +91 -0
  428. package/src/modes/theme/defaults/light-paper.json +95 -0
  429. package/src/modes/theme/defaults/light-prism.json +90 -0
  430. package/src/modes/theme/defaults/light-retro.json +98 -0
  431. package/src/modes/theme/defaults/light-sand.json +95 -0
  432. package/src/modes/theme/defaults/light-savanna.json +91 -0
  433. package/src/modes/theme/defaults/light-solarized.json +102 -0
  434. package/src/modes/theme/defaults/light-soleil.json +90 -0
  435. package/src/modes/theme/defaults/light-sunset.json +99 -0
  436. package/src/modes/theme/defaults/light-synthwave.json +98 -0
  437. package/src/modes/theme/defaults/light-tokyo-night.json +111 -0
  438. package/src/modes/theme/defaults/light-wetland.json +91 -0
  439. package/src/modes/theme/defaults/light-zenith.json +89 -0
  440. package/src/modes/theme/defaults/limestone.json +94 -0
  441. package/src/modes/theme/defaults/mahogany.json +97 -0
  442. package/src/modes/theme/defaults/marble.json +93 -0
  443. package/src/modes/theme/defaults/obsidian.json +91 -0
  444. package/src/modes/theme/defaults/onyx.json +91 -0
  445. package/src/modes/theme/defaults/pearl.json +93 -0
  446. package/src/modes/theme/defaults/porcelain.json +91 -0
  447. package/src/modes/theme/defaults/quartz.json +96 -0
  448. package/src/modes/theme/defaults/sandstone.json +95 -0
  449. package/src/modes/theme/defaults/titanium.json +90 -0
  450. package/src/modes/theme/light.json +93 -0
  451. package/src/modes/theme/mermaid-cache.ts +111 -0
  452. package/src/modes/theme/theme-schema.json +429 -0
  453. package/src/modes/theme/theme.ts +2333 -0
  454. package/src/modes/types.ts +216 -0
  455. package/src/modes/utils/ui-helpers.ts +529 -0
  456. package/src/patch/applicator.ts +1482 -0
  457. package/src/patch/diff.ts +425 -0
  458. package/src/patch/fuzzy.ts +784 -0
  459. package/src/patch/hashline.ts +972 -0
  460. package/src/patch/index.ts +964 -0
  461. package/src/patch/normalize.ts +397 -0
  462. package/src/patch/normative.ts +72 -0
  463. package/src/patch/parser.ts +532 -0
  464. package/src/patch/shared.ts +400 -0
  465. package/src/patch/types.ts +292 -0
  466. package/src/priority.json +35 -0
  467. package/src/prompts/agents/explore.md +48 -0
  468. package/src/prompts/agents/frontmatter.md +9 -0
  469. package/src/prompts/agents/init.md +36 -0
  470. package/src/prompts/agents/librarian.md +53 -0
  471. package/src/prompts/agents/oracle.md +51 -0
  472. package/src/prompts/agents/reviewer.md +70 -0
  473. package/src/prompts/agents/task.md +14 -0
  474. package/src/prompts/compaction/branch-summary-context.md +5 -0
  475. package/src/prompts/compaction/branch-summary-preamble.md +2 -0
  476. package/src/prompts/compaction/branch-summary.md +30 -0
  477. package/src/prompts/compaction/compaction-short-summary.md +9 -0
  478. package/src/prompts/compaction/compaction-summary-context.md +5 -0
  479. package/src/prompts/compaction/compaction-summary.md +38 -0
  480. package/src/prompts/compaction/compaction-turn-prefix.md +17 -0
  481. package/src/prompts/compaction/compaction-update-summary.md +45 -0
  482. package/src/prompts/memories/consolidation.md +30 -0
  483. package/src/prompts/memories/read_path.md +11 -0
  484. package/src/prompts/memories/stage_one_input.md +6 -0
  485. package/src/prompts/memories/stage_one_system.md +21 -0
  486. package/src/prompts/review-request.md +64 -0
  487. package/src/prompts/system/agent-creation-architect.md +65 -0
  488. package/src/prompts/system/agent-creation-user.md +6 -0
  489. package/src/prompts/system/custom-system-prompt.md +68 -0
  490. package/src/prompts/system/file-operations.md +10 -0
  491. package/src/prompts/system/subagent-submit-reminder.md +11 -0
  492. package/src/prompts/system/subagent-system-prompt.md +31 -0
  493. package/src/prompts/system/subagent-user-prompt.md +8 -0
  494. package/src/prompts/system/summarization-system.md +3 -0
  495. package/src/prompts/system/system-prompt.md +300 -0
  496. package/src/prompts/system/title-system.md +2 -0
  497. package/src/prompts/system/ttsr-interrupt.md +7 -0
  498. package/src/prompts/system/web-search.md +28 -0
  499. package/src/prompts/tools/ask.md +44 -0
  500. package/src/prompts/tools/bash.md +24 -0
  501. package/src/prompts/tools/browser.md +33 -0
  502. package/src/prompts/tools/calculator.md +12 -0
  503. package/src/prompts/tools/explore.md +29 -0
  504. package/src/prompts/tools/fetch.md +16 -0
  505. package/src/prompts/tools/find.md +18 -0
  506. package/src/prompts/tools/gemini-image.md +23 -0
  507. package/src/prompts/tools/grep.md +28 -0
  508. package/src/prompts/tools/hashline.md +232 -0
  509. package/src/prompts/tools/librarian.md +24 -0
  510. package/src/prompts/tools/lsp.md +28 -0
  511. package/src/prompts/tools/oracle.md +26 -0
  512. package/src/prompts/tools/patch.md +74 -0
  513. package/src/prompts/tools/python.md +66 -0
  514. package/src/prompts/tools/read.md +36 -0
  515. package/src/prompts/tools/replace.md +38 -0
  516. package/src/prompts/tools/reviewer.md +41 -0
  517. package/src/prompts/tools/ssh.md +51 -0
  518. package/src/prompts/tools/task-summary.md +28 -0
  519. package/src/prompts/tools/task.md +275 -0
  520. package/src/prompts/tools/todo-write.md +65 -0
  521. package/src/prompts/tools/undo-edit.md +7 -0
  522. package/src/prompts/tools/web-search.md +19 -0
  523. package/src/prompts/tools/write.md +18 -0
  524. package/src/sdk.ts +1287 -0
  525. package/src/secrets/index.ts +116 -0
  526. package/src/secrets/obfuscator.ts +269 -0
  527. package/src/secrets/regex.ts +21 -0
  528. package/src/session/agent-session.ts +4669 -0
  529. package/src/session/agent-storage.ts +621 -0
  530. package/src/session/artifacts.ts +132 -0
  531. package/src/session/auth-storage.ts +1433 -0
  532. package/src/session/blob-store.ts +103 -0
  533. package/src/session/compaction/branch-summarization.ts +315 -0
  534. package/src/session/compaction/compaction.ts +864 -0
  535. package/src/session/compaction/index.ts +7 -0
  536. package/src/session/compaction/pruning.ts +91 -0
  537. package/src/session/compaction/utils.ts +171 -0
  538. package/src/session/history-storage.ts +170 -0
  539. package/src/session/messages.ts +317 -0
  540. package/src/session/session-manager.ts +2276 -0
  541. package/src/session/session-storage.ts +342 -0
  542. package/src/session/streaming-output.ts +565 -0
  543. package/src/slash-commands/builtin-registry.ts +439 -0
  544. package/src/ssh/config-writer.ts +183 -0
  545. package/src/ssh/connection-manager.ts +444 -0
  546. package/src/ssh/ssh-executor.ts +127 -0
  547. package/src/ssh/sshfs-mount.ts +135 -0
  548. package/src/stt/downloader.ts +71 -0
  549. package/src/stt/index.ts +3 -0
  550. package/src/stt/recorder.ts +351 -0
  551. package/src/stt/setup.ts +52 -0
  552. package/src/stt/stt-controller.ts +160 -0
  553. package/src/stt/transcribe.py +70 -0
  554. package/src/stt/transcriber.ts +91 -0
  555. package/src/system-prompt.ts +685 -0
  556. package/src/task/agents.ts +155 -0
  557. package/src/task/batch.ts +102 -0
  558. package/src/task/commands.ts +134 -0
  559. package/src/task/discovery.ts +126 -0
  560. package/src/task/executor.ts +908 -0
  561. package/src/task/index.ts +223 -0
  562. package/src/task/output-manager.ts +107 -0
  563. package/src/task/parallel.ts +84 -0
  564. package/src/task/render.ts +326 -0
  565. package/src/task/subprocess-tool-registry.ts +88 -0
  566. package/src/task/template.ts +32 -0
  567. package/src/task/types.ts +144 -0
  568. package/src/tools/ask.ts +523 -0
  569. package/src/tools/bash-interactive.ts +419 -0
  570. package/src/tools/bash-interceptor.ts +105 -0
  571. package/src/tools/bash-normalize.ts +107 -0
  572. package/src/tools/bash-skill-urls.ts +177 -0
  573. package/src/tools/bash.ts +347 -0
  574. package/src/tools/browser.ts +1374 -0
  575. package/src/tools/calculator.ts +537 -0
  576. package/src/tools/context.ts +39 -0
  577. package/src/tools/explore.ts +23 -0
  578. package/src/tools/fetch.ts +1091 -0
  579. package/src/tools/find.ts +540 -0
  580. package/src/tools/fs-cache-invalidation.ts +28 -0
  581. package/src/tools/gemini-image.ts +907 -0
  582. package/src/tools/grep.ts +489 -0
  583. package/src/tools/index.ts +337 -0
  584. package/src/tools/json-tree.ts +231 -0
  585. package/src/tools/jtd-to-json-schema.ts +247 -0
  586. package/src/tools/jtd-to-typescript.ts +198 -0
  587. package/src/tools/librarian.ts +33 -0
  588. package/src/tools/list-limit.ts +40 -0
  589. package/src/tools/notebook.ts +287 -0
  590. package/src/tools/oracle.ts +40 -0
  591. package/src/tools/output-meta.ts +459 -0
  592. package/src/tools/output-utils.ts +63 -0
  593. package/src/tools/path-utils.ts +116 -0
  594. package/src/tools/puppeteer/00_stealth_tampering.txt +63 -0
  595. package/src/tools/puppeteer/01_stealth_activity.txt +20 -0
  596. package/src/tools/puppeteer/02_stealth_hairline.txt +11 -0
  597. package/src/tools/puppeteer/03_stealth_botd.txt +384 -0
  598. package/src/tools/puppeteer/04_stealth_iframe.txt +81 -0
  599. package/src/tools/puppeteer/05_stealth_webgl.txt +75 -0
  600. package/src/tools/puppeteer/06_stealth_screen.txt +72 -0
  601. package/src/tools/puppeteer/07_stealth_fonts.txt +97 -0
  602. package/src/tools/puppeteer/08_stealth_audio.txt +51 -0
  603. package/src/tools/puppeteer/09_stealth_locale.txt +46 -0
  604. package/src/tools/puppeteer/10_stealth_plugins.txt +206 -0
  605. package/src/tools/puppeteer/11_stealth_hardware.txt +8 -0
  606. package/src/tools/puppeteer/12_stealth_codecs.txt +40 -0
  607. package/src/tools/puppeteer/13_stealth_worker.txt +74 -0
  608. package/src/tools/python.ts +1118 -0
  609. package/src/tools/read.ts +1193 -0
  610. package/src/tools/render-utils.ts +680 -0
  611. package/src/tools/renderers.ts +60 -0
  612. package/src/tools/reviewer-tool.ts +41 -0
  613. package/src/tools/ssh.ts +326 -0
  614. package/src/tools/subagent-tool.ts +169 -0
  615. package/src/tools/submit-result.ts +152 -0
  616. package/src/tools/todo-write.ts +255 -0
  617. package/src/tools/tool-errors.ts +92 -0
  618. package/src/tools/tool-result.ts +86 -0
  619. package/src/tools/undo-edit.ts +145 -0
  620. package/src/tools/undo-history.ts +22 -0
  621. package/src/tools/write.ts +274 -0
  622. package/src/tui/code-cell.ts +108 -0
  623. package/src/tui/file-list.ts +47 -0
  624. package/src/tui/index.ts +11 -0
  625. package/src/tui/output-block.ts +144 -0
  626. package/src/tui/status-line.ts +39 -0
  627. package/src/tui/tree-list.ts +53 -0
  628. package/src/tui/types.ts +16 -0
  629. package/src/tui/utils.ts +116 -0
  630. package/src/utils/changelog.ts +98 -0
  631. package/src/utils/event-bus.ts +33 -0
  632. package/src/utils/external-editor.ts +59 -0
  633. package/src/utils/file-display-mode.ts +36 -0
  634. package/src/utils/file-mentions.ts +384 -0
  635. package/src/utils/frontmatter.ts +101 -0
  636. package/src/utils/fuzzy.ts +108 -0
  637. package/src/utils/ignore-files.ts +119 -0
  638. package/src/utils/image-convert.ts +27 -0
  639. package/src/utils/image-resize.ts +236 -0
  640. package/src/utils/mime.ts +30 -0
  641. package/src/utils/open.ts +20 -0
  642. package/src/utils/shell-snapshot.ts +199 -0
  643. package/src/utils/timings.ts +26 -0
  644. package/src/utils/title-generator.ts +167 -0
  645. package/src/utils/tools-manager.ts +362 -0
  646. package/src/web/scrapers/artifacthub.ts +215 -0
  647. package/src/web/scrapers/arxiv.ts +88 -0
  648. package/src/web/scrapers/aur.ts +175 -0
  649. package/src/web/scrapers/biorxiv.ts +141 -0
  650. package/src/web/scrapers/bluesky.ts +284 -0
  651. package/src/web/scrapers/brew.ts +177 -0
  652. package/src/web/scrapers/cheatsh.ts +78 -0
  653. package/src/web/scrapers/chocolatey.ts +158 -0
  654. package/src/web/scrapers/choosealicense.ts +110 -0
  655. package/src/web/scrapers/cisa-kev.ts +100 -0
  656. package/src/web/scrapers/clojars.ts +180 -0
  657. package/src/web/scrapers/coingecko.ts +184 -0
  658. package/src/web/scrapers/crates-io.ts +128 -0
  659. package/src/web/scrapers/crossref.ts +149 -0
  660. package/src/web/scrapers/devto.ts +177 -0
  661. package/src/web/scrapers/discogs.ts +307 -0
  662. package/src/web/scrapers/discourse.ts +221 -0
  663. package/src/web/scrapers/dockerhub.ts +160 -0
  664. package/src/web/scrapers/fdroid.ts +158 -0
  665. package/src/web/scrapers/firefox-addons.ts +214 -0
  666. package/src/web/scrapers/flathub.ts +239 -0
  667. package/src/web/scrapers/github-gist.ts +68 -0
  668. package/src/web/scrapers/github.ts +490 -0
  669. package/src/web/scrapers/gitlab.ts +456 -0
  670. package/src/web/scrapers/go-pkg.ts +275 -0
  671. package/src/web/scrapers/hackage.ts +94 -0
  672. package/src/web/scrapers/hackernews.ts +208 -0
  673. package/src/web/scrapers/hex.ts +121 -0
  674. package/src/web/scrapers/huggingface.ts +385 -0
  675. package/src/web/scrapers/iacr.ts +86 -0
  676. package/src/web/scrapers/index.ts +249 -0
  677. package/src/web/scrapers/jetbrains-marketplace.ts +169 -0
  678. package/src/web/scrapers/lemmy.ts +220 -0
  679. package/src/web/scrapers/lobsters.ts +186 -0
  680. package/src/web/scrapers/mastodon.ts +310 -0
  681. package/src/web/scrapers/maven.ts +152 -0
  682. package/src/web/scrapers/mdn.ts +172 -0
  683. package/src/web/scrapers/metacpan.ts +253 -0
  684. package/src/web/scrapers/musicbrainz.ts +272 -0
  685. package/src/web/scrapers/npm.ts +114 -0
  686. package/src/web/scrapers/nuget.ts +205 -0
  687. package/src/web/scrapers/nvd.ts +243 -0
  688. package/src/web/scrapers/ollama.ts +265 -0
  689. package/src/web/scrapers/open-vsx.ts +119 -0
  690. package/src/web/scrapers/opencorporates.ts +275 -0
  691. package/src/web/scrapers/openlibrary.ts +319 -0
  692. package/src/web/scrapers/orcid.ts +298 -0
  693. package/src/web/scrapers/osv.ts +192 -0
  694. package/src/web/scrapers/packagist.ts +174 -0
  695. package/src/web/scrapers/pub-dev.ts +185 -0
  696. package/src/web/scrapers/pubmed.ts +177 -0
  697. package/src/web/scrapers/pypi.ts +129 -0
  698. package/src/web/scrapers/rawg.ts +124 -0
  699. package/src/web/scrapers/readthedocs.ts +125 -0
  700. package/src/web/scrapers/reddit.ts +104 -0
  701. package/src/web/scrapers/repology.ts +262 -0
  702. package/src/web/scrapers/rfc.ts +209 -0
  703. package/src/web/scrapers/rubygems.ts +117 -0
  704. package/src/web/scrapers/searchcode.ts +217 -0
  705. package/src/web/scrapers/sec-edgar.ts +274 -0
  706. package/src/web/scrapers/semantic-scholar.ts +190 -0
  707. package/src/web/scrapers/snapcraft.ts +200 -0
  708. package/src/web/scrapers/sourcegraph.ts +373 -0
  709. package/src/web/scrapers/spdx.ts +121 -0
  710. package/src/web/scrapers/spotify.ts +217 -0
  711. package/src/web/scrapers/stackoverflow.ts +124 -0
  712. package/src/web/scrapers/terraform.ts +304 -0
  713. package/src/web/scrapers/tldr.ts +51 -0
  714. package/src/web/scrapers/twitter.ts +97 -0
  715. package/src/web/scrapers/types.ts +200 -0
  716. package/src/web/scrapers/utils.ts +142 -0
  717. package/src/web/scrapers/vimeo.ts +152 -0
  718. package/src/web/scrapers/vscode-marketplace.ts +195 -0
  719. package/src/web/scrapers/w3c.ts +163 -0
  720. package/src/web/scrapers/wikidata.ts +357 -0
  721. package/src/web/scrapers/wikipedia.ts +95 -0
  722. package/src/web/scrapers/youtube.ts +312 -0
  723. package/src/web/search/auth.ts +178 -0
  724. package/src/web/search/index.ts +598 -0
  725. package/src/web/search/provider.ts +77 -0
  726. package/src/web/search/providers/anthropic.ts +284 -0
  727. package/src/web/search/providers/base.ts +22 -0
  728. package/src/web/search/providers/brave.ts +165 -0
  729. package/src/web/search/providers/codex.ts +377 -0
  730. package/src/web/search/providers/exa.ts +158 -0
  731. package/src/web/search/providers/gemini.ts +437 -0
  732. package/src/web/search/providers/jina.ts +99 -0
  733. package/src/web/search/providers/kimi.ts +196 -0
  734. package/src/web/search/providers/perplexity.ts +546 -0
  735. package/src/web/search/providers/synthetic.ts +136 -0
  736. package/src/web/search/providers/zai.ts +352 -0
  737. package/src/web/search/render.ts +299 -0
  738. package/src/web/search/types.ts +437 -0
@@ -0,0 +1,1433 @@
1
+ /**
2
+ * Credential storage for API keys and OAuth tokens.
3
+ * Handles loading, saving, and refreshing credentials from agent.db.
4
+ */
5
+ import {
6
+ antigravityUsageProvider,
7
+ claudeUsageProvider,
8
+ getEnvApiKey,
9
+ getOAuthApiKey,
10
+ getOAuthProvider,
11
+ githubCopilotUsageProvider,
12
+ googleGeminiCliUsageProvider,
13
+ kimiUsageProvider,
14
+ loginAnthropic,
15
+ loginAntigravity,
16
+ loginCerebras,
17
+ loginCloudflareAiGateway,
18
+ loginCursor,
19
+ loginGeminiCli,
20
+ loginGitHubCopilot,
21
+ loginHuggingface,
22
+ loginKimi,
23
+ loginLiteLLM,
24
+ loginMiniMaxCode,
25
+ loginMiniMaxCodeCn,
26
+ loginMoonshot,
27
+ loginNanoGPT,
28
+ loginNvidia,
29
+ loginOllama,
30
+ loginOpenAICodex,
31
+ loginOpenCode,
32
+ loginPerplexity,
33
+ loginQianfan,
34
+ loginQwenPortal,
35
+ loginSynthetic,
36
+ loginTogether,
37
+ loginVenice,
38
+ loginVllm,
39
+ loginXiaomi,
40
+ loginZai,
41
+ type OAuthController,
42
+ type OAuthCredentials,
43
+ type OAuthProvider,
44
+ type OAuthProviderId,
45
+ openaiCodexUsageProvider,
46
+ type Provider,
47
+ type UsageCache,
48
+ type UsageCacheEntry,
49
+ type UsageCredential,
50
+ type UsageLimit,
51
+ type UsageLogger,
52
+ type UsageProvider,
53
+ type UsageReport,
54
+ zaiUsageProvider,
55
+ } from "@nghyane/arcane-ai";
56
+ import { logger } from "@nghyane/arcane-utils";
57
+ import { resolveConfigValue } from "../config/resolve-config-value";
58
+ import { AgentStorage } from "./agent-storage";
59
+
60
+ export type ApiKeyCredential = {
61
+ type: "api_key";
62
+ key: string;
63
+ };
64
+
65
+ export type OAuthCredential = {
66
+ type: "oauth";
67
+ } & OAuthCredentials;
68
+
69
+ export type AuthCredential = ApiKeyCredential | OAuthCredential;
70
+
71
+ export type AuthCredentialEntry = AuthCredential | AuthCredential[];
72
+
73
+ export type AuthStorageData = Record<string, AuthCredentialEntry>;
74
+
75
+ /**
76
+ * Serialized representation of AuthStorage for passing to subagent workers.
77
+ * Contains only the essential credential data, not runtime state.
78
+ */
79
+ export interface SerializedAuthStorage {
80
+ credentials: Record<
81
+ string,
82
+ Array<{
83
+ id: number;
84
+ type: "api_key" | "oauth";
85
+ data: Record<string, unknown>;
86
+ }>
87
+ >;
88
+ runtimeOverrides?: Record<string, string>;
89
+ dbPath?: string;
90
+ }
91
+
92
+ /**
93
+ * In-memory representation pairing DB row ID with credential.
94
+ * The ID is required for update/delete operations against agent.db.
95
+ */
96
+ type StoredCredential = { id: number; credential: AuthCredential };
97
+
98
+ export type AuthStorageOptions = {
99
+ usageProviderResolver?: (provider: Provider) => UsageProvider | undefined;
100
+ usageCache?: UsageCache;
101
+ usageFetch?: typeof fetch;
102
+ usageNow?: () => number;
103
+ usageLogger?: UsageLogger;
104
+ };
105
+
106
+ const DEFAULT_USAGE_PROVIDERS: UsageProvider[] = [
107
+ openaiCodexUsageProvider,
108
+ kimiUsageProvider,
109
+ antigravityUsageProvider,
110
+ googleGeminiCliUsageProvider,
111
+ claudeUsageProvider,
112
+ zaiUsageProvider,
113
+ githubCopilotUsageProvider,
114
+ ];
115
+
116
+ const DEFAULT_USAGE_PROVIDER_MAP = new Map<Provider, UsageProvider>(
117
+ DEFAULT_USAGE_PROVIDERS.map(provider => [provider.id, provider]),
118
+ );
119
+
120
+ const USAGE_CACHE_PREFIX = "usage_cache:";
121
+
122
+ function resolveDefaultUsageProvider(provider: Provider): UsageProvider | undefined {
123
+ return DEFAULT_USAGE_PROVIDER_MAP.get(provider);
124
+ }
125
+
126
+ function parseUsageCacheEntry(raw: string): UsageCacheEntry | undefined {
127
+ try {
128
+ const parsed = JSON.parse(raw) as { value?: UsageReport | null; expiresAt?: unknown };
129
+ const expiresAt = typeof parsed.expiresAt === "number" ? parsed.expiresAt : undefined;
130
+ if (!expiresAt || !Number.isFinite(expiresAt)) return undefined;
131
+ return { value: parsed.value ?? null, expiresAt };
132
+ } catch {
133
+ return undefined;
134
+ }
135
+ }
136
+
137
+ class AuthStorageUsageCache implements UsageCache {
138
+ constructor(private storage: AgentStorage) {}
139
+
140
+ get(key: string): UsageCacheEntry | undefined {
141
+ const raw = this.storage.getCache(`${USAGE_CACHE_PREFIX}${key}`);
142
+ if (!raw) return undefined;
143
+ const entry = parseUsageCacheEntry(raw);
144
+ if (!entry) return undefined;
145
+ if (entry.expiresAt <= Date.now()) return undefined;
146
+ return entry;
147
+ }
148
+
149
+ set(key: string, entry: UsageCacheEntry): void {
150
+ const payload = JSON.stringify({ value: entry.value ?? null, expiresAt: entry.expiresAt });
151
+ this.storage.setCache(`${USAGE_CACHE_PREFIX}${key}`, payload, Math.floor(entry.expiresAt / 1000));
152
+ }
153
+
154
+ cleanup(): void {
155
+ this.storage.cleanExpiredCache();
156
+ }
157
+ }
158
+
159
+ /**
160
+ * Credential storage backed by agent.db.
161
+ * Reads from SQLite (agent.db).
162
+ */
163
+ export class AuthStorage {
164
+ static readonly #defaultBackoffMs = 60_000; // Default backoff when no reset time available
165
+
166
+ /** Provider -> credentials cache, populated from agent.db on reload(). */
167
+ #data: Map<string, StoredCredential[]> = new Map();
168
+ #runtimeOverrides: Map<string, string> = new Map();
169
+ /** Tracks next credential index per provider:type key for round-robin distribution (non-session use). */
170
+ #providerRoundRobinIndex: Map<string, number> = new Map();
171
+ /** Tracks the last used credential per provider for a session (used for rate-limit switching). */
172
+ #sessionLastCredential: Map<string, Map<string, { type: AuthCredential["type"]; index: number }>> = new Map();
173
+ /** Maps provider:type -> credentialIndex -> blockedUntilMs for temporary backoff. */
174
+ #credentialBackoff: Map<string, Map<number, number>> = new Map();
175
+ #usageProviderResolver?: (provider: Provider) => UsageProvider | undefined;
176
+ #usageCache?: UsageCache;
177
+ #usageFetch: typeof fetch;
178
+ #usageNow: () => number;
179
+ #usageLogger?: UsageLogger;
180
+ #fallbackResolver?: (provider: string) => string | undefined;
181
+
182
+ private constructor(
183
+ private storage: AgentStorage,
184
+ options: AuthStorageOptions = {},
185
+ ) {
186
+ this.#usageProviderResolver = options.usageProviderResolver ?? resolveDefaultUsageProvider;
187
+ this.#usageCache = options.usageCache ?? new AuthStorageUsageCache(this.storage);
188
+ this.#usageFetch = options.usageFetch ?? fetch;
189
+ this.#usageNow = options.usageNow ?? Date.now;
190
+ this.#usageLogger =
191
+ options.usageLogger ??
192
+ ({
193
+ debug: (message, meta) => logger.debug(message, meta),
194
+ warn: (message, meta) => logger.warn(message, meta),
195
+ } satisfies UsageLogger);
196
+ }
197
+
198
+ /**
199
+ * Create an AuthStorage instance.
200
+ * @param dbPath - Path to agent.db
201
+ */
202
+ static async create(dbPath: string, options: AuthStorageOptions = {}): Promise<AuthStorage> {
203
+ const storage = await AgentStorage.open(dbPath);
204
+ return new AuthStorage(storage, options);
205
+ }
206
+
207
+ /**
208
+ * Set a runtime API key override (not persisted to disk).
209
+ * Used for CLI --api-key flag.
210
+ */
211
+ setRuntimeApiKey(provider: string, apiKey: string): void {
212
+ this.#runtimeOverrides.set(provider, apiKey);
213
+ }
214
+
215
+ /**
216
+ * Remove a runtime API key override.
217
+ */
218
+ removeRuntimeApiKey(provider: string): void {
219
+ this.#runtimeOverrides.delete(provider);
220
+ }
221
+
222
+ /**
223
+ * Set a fallback resolver for API keys not found in agent.db or env vars.
224
+ * Used for custom provider keys from models.json.
225
+ */
226
+ setFallbackResolver(resolver: (provider: string) => string | undefined): void {
227
+ this.#fallbackResolver = resolver;
228
+ }
229
+
230
+ /**
231
+ * Reload credentials from agent.db.
232
+ * Reloads credentials from the database.
233
+ */
234
+ async reload(): Promise<void> {
235
+ const records = this.storage.listAuthCredentials();
236
+ const grouped = new Map<string, StoredCredential[]>();
237
+ for (const record of records) {
238
+ const list = grouped.get(record.provider) ?? [];
239
+ list.push({ id: record.id, credential: record.credential });
240
+ grouped.set(record.provider, list);
241
+ }
242
+
243
+ const dedupedGrouped = new Map<string, StoredCredential[]>();
244
+ for (const [provider, entries] of grouped.entries()) {
245
+ const deduped = this.#pruneDuplicateStoredCredentials(provider, entries);
246
+ if (deduped.length > 0) {
247
+ dedupedGrouped.set(provider, deduped);
248
+ }
249
+ }
250
+ this.#data = dedupedGrouped;
251
+ }
252
+
253
+ /**
254
+ * Gets cached credentials for a provider.
255
+ * @param provider - Provider name (e.g., "anthropic", "openai")
256
+ * @returns Array of stored credentials, empty if none exist
257
+ */
258
+ #getStoredCredentials(provider: string): StoredCredential[] {
259
+ return this.#data.get(provider) ?? [];
260
+ }
261
+
262
+ /**
263
+ * Updates in-memory credential cache for a provider.
264
+ * Removes the provider entry entirely if credentials array is empty.
265
+ * @param provider - Provider name (e.g., "anthropic", "openai")
266
+ * @param credentials - Array of stored credentials to cache
267
+ */
268
+ #setStoredCredentials(provider: string, credentials: StoredCredential[]): void {
269
+ if (credentials.length === 0) {
270
+ this.#data.delete(provider);
271
+ } else {
272
+ this.#data.set(provider, credentials);
273
+ }
274
+ }
275
+
276
+ #getOAuthIdentifiers(credential: OAuthCredential): string[] {
277
+ const identifiers: string[] = [];
278
+ const accountId = credential.accountId?.trim();
279
+ if (accountId) identifiers.push(`account:${accountId}`);
280
+ const email = credential.email?.trim().toLowerCase();
281
+ if (email) identifiers.push(`email:${email}`);
282
+ if (identifiers.length > 0) return identifiers;
283
+ const tokenIdentifiers = this.#getOAuthIdentifiersFromToken(credential.access) ?? [];
284
+ for (const identifier of tokenIdentifiers) {
285
+ identifiers.push(identifier);
286
+ }
287
+ if (identifiers.length > 0) return identifiers;
288
+ const refreshIdentifiers = this.#getOAuthIdentifiersFromToken(credential.refresh) ?? [];
289
+ for (const identifier of refreshIdentifiers) {
290
+ identifiers.push(identifier);
291
+ }
292
+ return identifiers;
293
+ }
294
+
295
+ #getOAuthIdentifiersFromToken(token: string | undefined): string[] | undefined {
296
+ if (!token) return undefined;
297
+ const parts = token.split(".");
298
+ if (parts.length !== 3) return undefined;
299
+ const payloadRaw = parts[1];
300
+ const decoder = new TextDecoder("utf-8");
301
+ try {
302
+ const payload = JSON.parse(
303
+ decoder.decode(Uint8Array.fromBase64(payloadRaw, { alphabet: "base64url" })),
304
+ ) as Record<string, unknown>;
305
+ if (!payload || typeof payload !== "object") return undefined;
306
+ const identifiers: string[] = [];
307
+ const email = typeof payload.email === "string" ? payload.email.trim().toLowerCase() : undefined;
308
+ if (email) identifiers.push(`email:${email}`);
309
+ const accountId =
310
+ typeof payload.account_id === "string"
311
+ ? payload.account_id
312
+ : typeof payload.accountId === "string"
313
+ ? payload.accountId
314
+ : typeof payload.user_id === "string"
315
+ ? payload.user_id
316
+ : typeof payload.sub === "string"
317
+ ? payload.sub
318
+ : undefined;
319
+ const trimmedAccountId = accountId?.trim();
320
+ if (trimmedAccountId) identifiers.push(`account:${trimmedAccountId}`);
321
+ return identifiers.length > 0 ? identifiers : undefined;
322
+ } catch {
323
+ return undefined;
324
+ }
325
+ }
326
+
327
+ #dedupeOAuthCredentials(credentials: AuthCredential[]): AuthCredential[] {
328
+ const seen = new Set<string>();
329
+ const deduped: AuthCredential[] = [];
330
+ for (let index = credentials.length - 1; index >= 0; index -= 1) {
331
+ const credential = credentials[index];
332
+ if (credential.type !== "oauth") {
333
+ deduped.push(credential);
334
+ continue;
335
+ }
336
+ const identifiers = this.#getOAuthIdentifiers(credential);
337
+ if (identifiers.length === 0) {
338
+ deduped.push(credential);
339
+ continue;
340
+ }
341
+ if (identifiers.some(identifier => seen.has(identifier))) {
342
+ continue;
343
+ }
344
+ for (const identifier of identifiers) {
345
+ seen.add(identifier);
346
+ }
347
+ deduped.push(credential);
348
+ }
349
+ return deduped.reverse();
350
+ }
351
+
352
+ #pruneDuplicateStoredCredentials(provider: string, entries: StoredCredential[]): StoredCredential[] {
353
+ const seen = new Set<string>();
354
+ const kept: StoredCredential[] = [];
355
+ const removed: StoredCredential[] = [];
356
+ for (let index = entries.length - 1; index >= 0; index -= 1) {
357
+ const entry = entries[index];
358
+ const credential = entry.credential;
359
+ if (credential.type !== "oauth") {
360
+ kept.push(entry);
361
+ continue;
362
+ }
363
+ const identifiers = this.#getOAuthIdentifiers(credential);
364
+ if (identifiers.length === 0) {
365
+ kept.push(entry);
366
+ continue;
367
+ }
368
+ if (identifiers.some(identifier => seen.has(identifier))) {
369
+ removed.push(entry);
370
+ continue;
371
+ }
372
+ for (const identifier of identifiers) {
373
+ seen.add(identifier);
374
+ }
375
+ kept.push(entry);
376
+ }
377
+ if (removed.length > 0) {
378
+ for (const entry of removed) {
379
+ this.storage.deleteAuthCredential(entry.id);
380
+ }
381
+ this.#resetProviderAssignments(provider);
382
+ }
383
+ return kept.reverse();
384
+ }
385
+
386
+ /** Returns all credentials for a provider as an array */
387
+ #getCredentialsForProvider(provider: string): AuthCredential[] {
388
+ return this.#getStoredCredentials(provider).map(entry => entry.credential);
389
+ }
390
+
391
+ /** Composite key for round-robin tracking: "anthropic:oauth" or "openai:api_key" */
392
+ #getProviderTypeKey(provider: string, type: AuthCredential["type"]): string {
393
+ return `${provider}:${type}`;
394
+ }
395
+
396
+ /**
397
+ * Returns next index in round-robin sequence for load distribution.
398
+ * Increments stored counter and wraps at total.
399
+ */
400
+ #getNextRoundRobinIndex(providerKey: string, total: number): number {
401
+ if (total <= 1) return 0;
402
+ const current = this.#providerRoundRobinIndex.get(providerKey) ?? -1;
403
+ const next = (current + 1) % total;
404
+ this.#providerRoundRobinIndex.set(providerKey, next);
405
+ return next;
406
+ }
407
+
408
+ /**
409
+ * FNV-1a hash for deterministic session-to-credential mapping.
410
+ * Ensures the same session always starts with the same credential.
411
+ */
412
+ #getHashedIndex(sessionId: string, total: number): number {
413
+ if (total <= 1) return 0;
414
+ let hash = 2166136261; // FNV offset basis
415
+ for (let i = 0; i < sessionId.length; i++) {
416
+ hash ^= sessionId.charCodeAt(i);
417
+ hash = Math.imul(hash, 16777619); // FNV prime
418
+ }
419
+ return (hash >>> 0) % total;
420
+ }
421
+
422
+ /**
423
+ * Returns credential indices in priority order for selection.
424
+ * With sessionId: starts from hashed index (consistent per session).
425
+ * Without sessionId: starts from round-robin index (load balancing).
426
+ * Order wraps around so all credentials are tried if earlier ones are blocked.
427
+ */
428
+ #getCredentialOrder(providerKey: string, sessionId: string | undefined, total: number): number[] {
429
+ if (total <= 1) return [0];
430
+ const start = sessionId
431
+ ? this.#getHashedIndex(sessionId, total)
432
+ : this.#getNextRoundRobinIndex(providerKey, total);
433
+ const order: number[] = [];
434
+ for (let i = 0; i < total; i++) {
435
+ order.push((start + i) % total);
436
+ }
437
+ return order;
438
+ }
439
+
440
+ /** Checks if a credential is temporarily blocked due to usage limits. */
441
+ #isCredentialBlocked(providerKey: string, credentialIndex: number): boolean {
442
+ const backoffMap = this.#credentialBackoff.get(providerKey);
443
+ if (!backoffMap) return false;
444
+ const blockedUntil = backoffMap.get(credentialIndex);
445
+ if (!blockedUntil) return false;
446
+ if (blockedUntil <= Date.now()) {
447
+ backoffMap.delete(credentialIndex);
448
+ if (backoffMap.size === 0) {
449
+ this.#credentialBackoff.delete(providerKey);
450
+ }
451
+ return false;
452
+ }
453
+ return true;
454
+ }
455
+
456
+ /** Marks a credential as blocked until the specified time. */
457
+ #markCredentialBlocked(providerKey: string, credentialIndex: number, blockedUntilMs: number): void {
458
+ const backoffMap = this.#credentialBackoff.get(providerKey) ?? new Map<number, number>();
459
+ const existing = backoffMap.get(credentialIndex) ?? 0;
460
+ backoffMap.set(credentialIndex, Math.max(existing, blockedUntilMs));
461
+ this.#credentialBackoff.set(providerKey, backoffMap);
462
+ }
463
+
464
+ /** Records which credential was used for a session (for rate-limit switching). */
465
+ #recordSessionCredential(
466
+ provider: string,
467
+ sessionId: string | undefined,
468
+ type: AuthCredential["type"],
469
+ index: number,
470
+ ): void {
471
+ if (!sessionId) return;
472
+ const sessionMap = this.#sessionLastCredential.get(provider) ?? new Map();
473
+ sessionMap.set(sessionId, { type, index });
474
+ this.#sessionLastCredential.set(provider, sessionMap);
475
+ }
476
+
477
+ /** Retrieves the last credential used by a session. */
478
+ #getSessionCredential(
479
+ provider: string,
480
+ sessionId: string | undefined,
481
+ ): { type: AuthCredential["type"]; index: number } | undefined {
482
+ if (!sessionId) return undefined;
483
+ return this.#sessionLastCredential.get(provider)?.get(sessionId);
484
+ }
485
+
486
+ /**
487
+ * Selects a credential of the specified type for a provider.
488
+ * Returns both the credential and its index in the original array (for updates/removal).
489
+ * Uses deterministic hashing for session stickiness and skips blocked credentials when possible.
490
+ */
491
+ #selectCredentialByType<T extends AuthCredential["type"]>(
492
+ provider: string,
493
+ type: T,
494
+ sessionId?: string,
495
+ ): { credential: Extract<AuthCredential, { type: T }>; index: number } | undefined {
496
+ const credentials = this.#getCredentialsForProvider(provider)
497
+ .map((credential, index) => ({ credential, index }))
498
+ .filter(
499
+ (entry): entry is { credential: Extract<AuthCredential, { type: T }>; index: number } =>
500
+ entry.credential.type === type,
501
+ );
502
+
503
+ if (credentials.length === 0) return undefined;
504
+ if (credentials.length === 1) return credentials[0];
505
+
506
+ const providerKey = this.#getProviderTypeKey(provider, type);
507
+ const order = this.#getCredentialOrder(providerKey, sessionId, credentials.length);
508
+ const fallback = credentials[order[0]];
509
+
510
+ for (const idx of order) {
511
+ const candidate = credentials[idx];
512
+ if (!this.#isCredentialBlocked(providerKey, candidate.index)) {
513
+ return candidate;
514
+ }
515
+ }
516
+
517
+ return fallback;
518
+ }
519
+
520
+ /**
521
+ * Clears round-robin and session assignment state for a provider.
522
+ * Called when credentials are added/removed to prevent stale index references.
523
+ */
524
+ #resetProviderAssignments(provider: string): void {
525
+ for (const key of this.#providerRoundRobinIndex.keys()) {
526
+ if (key.startsWith(`${provider}:`)) {
527
+ this.#providerRoundRobinIndex.delete(key);
528
+ }
529
+ }
530
+ this.#sessionLastCredential.delete(provider);
531
+ for (const key of this.#credentialBackoff.keys()) {
532
+ if (key.startsWith(`${provider}:`)) {
533
+ this.#credentialBackoff.delete(key);
534
+ }
535
+ }
536
+ }
537
+
538
+ /** Updates credential at index in-place (used for OAuth token refresh) */
539
+ #replaceCredentialAt(provider: string, index: number, credential: AuthCredential): void {
540
+ const entries = this.#getStoredCredentials(provider);
541
+ if (index < 0 || index >= entries.length) return;
542
+ const target = entries[index];
543
+ this.storage.updateAuthCredential(target.id, credential);
544
+ const updated = [...entries];
545
+ updated[index] = { id: target.id, credential };
546
+ this.#setStoredCredentials(provider, updated);
547
+ }
548
+
549
+ /**
550
+ * Disables credential at index (used when OAuth refresh fails).
551
+ * The credential remains in the database but is excluded from active queries.
552
+ * Cleans up provider entry if last credential disabled.
553
+ */
554
+ #removeCredentialAt(provider: string, index: number): void {
555
+ const entries = this.#getStoredCredentials(provider);
556
+ if (index < 0 || index >= entries.length) return;
557
+ this.storage.disableAuthCredential(entries[index].id);
558
+ const updated = entries.filter((_value, idx) => idx !== index);
559
+ this.#setStoredCredentials(provider, updated);
560
+ this.#resetProviderAssignments(provider);
561
+ }
562
+
563
+ /**
564
+ * Get credential for a provider (first entry if multiple).
565
+ */
566
+ get(provider: string): AuthCredential | undefined {
567
+ return this.#getCredentialsForProvider(provider)[0];
568
+ }
569
+
570
+ /**
571
+ * Set credential for a provider.
572
+ */
573
+ async set(provider: string, credential: AuthCredentialEntry): Promise<void> {
574
+ const normalized = Array.isArray(credential) ? credential : [credential];
575
+ const deduped = this.#dedupeOAuthCredentials(normalized);
576
+ const stored = this.storage.replaceAuthCredentialsForProvider(provider, deduped);
577
+ this.#setStoredCredentials(
578
+ provider,
579
+ stored.map(record => ({ id: record.id, credential: record.credential })),
580
+ );
581
+ this.#resetProviderAssignments(provider);
582
+ }
583
+
584
+ /**
585
+ * Remove credential for a provider.
586
+ */
587
+ async remove(provider: string): Promise<void> {
588
+ this.storage.deleteAuthCredentialsForProvider(provider);
589
+ this.#data.delete(provider);
590
+ this.#resetProviderAssignments(provider);
591
+ }
592
+
593
+ /**
594
+ * List all providers with credentials.
595
+ */
596
+ list(): string[] {
597
+ return [...this.#data.keys()];
598
+ }
599
+
600
+ /**
601
+ * Check if credentials exist for a provider in agent.db.
602
+ */
603
+ has(provider: string): boolean {
604
+ return this.#getCredentialsForProvider(provider).length > 0;
605
+ }
606
+
607
+ /**
608
+ * Check if any form of auth is configured for a provider.
609
+ * Unlike getApiKey(), this doesn't refresh OAuth tokens.
610
+ */
611
+ hasAuth(provider: string): boolean {
612
+ if (this.#runtimeOverrides.has(provider)) return true;
613
+ if (this.#getCredentialsForProvider(provider).length > 0) return true;
614
+ if (getEnvApiKey(provider)) return true;
615
+ if (this.#fallbackResolver?.(provider)) return true;
616
+ return false;
617
+ }
618
+
619
+ /**
620
+ * Check if OAuth credentials are configured for a provider.
621
+ */
622
+ hasOAuth(provider: string): boolean {
623
+ return this.#getCredentialsForProvider(provider).some(credential => credential.type === "oauth");
624
+ }
625
+
626
+ /**
627
+ * Get OAuth credentials for a provider.
628
+ */
629
+ getOAuthCredential(provider: string): OAuthCredential | undefined {
630
+ return this.#getCredentialsForProvider(provider).find(
631
+ (credential): credential is OAuthCredential => credential.type === "oauth",
632
+ );
633
+ }
634
+
635
+ /**
636
+ * Get all credentials.
637
+ */
638
+ getAll(): AuthStorageData {
639
+ const result: AuthStorageData = {};
640
+ for (const [provider, entries] of this.#data.entries()) {
641
+ const credentials = entries.map(entry => entry.credential);
642
+ if (credentials.length === 1) {
643
+ result[provider] = credentials[0];
644
+ } else if (credentials.length > 1) {
645
+ result[provider] = credentials;
646
+ }
647
+ }
648
+ return result;
649
+ }
650
+
651
+ /**
652
+ * Login to an OAuth provider.
653
+ */
654
+ async login(
655
+ provider: OAuthProviderId,
656
+ ctrl: OAuthController & {
657
+ /** onAuth is required by auth-storage but optional in OAuthController */
658
+ onAuth: (info: { url: string; instructions?: string }) => void;
659
+ /** onPrompt is required for some providers (github-copilot, openai-codex) */
660
+ onPrompt: (prompt: { message: string; placeholder?: string }) => Promise<string>;
661
+ },
662
+ ): Promise<void> {
663
+ let credentials: OAuthCredentials;
664
+ const saveApiKeyCredential = async (apiKey: string): Promise<void> => {
665
+ const newCredential: ApiKeyCredential = { type: "api_key", key: apiKey };
666
+ const existing = this.#getCredentialsForProvider(provider);
667
+ if (existing.length === 0) {
668
+ await this.set(provider, newCredential);
669
+ return;
670
+ }
671
+ await this.set(provider, [...existing, newCredential]);
672
+ };
673
+ switch (provider) {
674
+ case "anthropic":
675
+ credentials = await loginAnthropic({
676
+ ...ctrl,
677
+ onManualCodeInput: async () =>
678
+ ctrl.onPrompt({ message: "Paste the authorization code (or full redirect URL):" }),
679
+ });
680
+ break;
681
+ case "github-copilot":
682
+ credentials = await loginGitHubCopilot({
683
+ onAuth: (url, instructions) => ctrl.onAuth({ url, instructions }),
684
+ onPrompt: ctrl.onPrompt,
685
+ onProgress: ctrl.onProgress,
686
+ signal: ctrl.signal,
687
+ });
688
+ break;
689
+ case "google-gemini-cli":
690
+ credentials = await loginGeminiCli(ctrl);
691
+ break;
692
+ case "google-antigravity":
693
+ credentials = await loginAntigravity(ctrl);
694
+ break;
695
+ case "openai-codex":
696
+ credentials = await loginOpenAICodex(ctrl);
697
+ break;
698
+ case "kimi-code":
699
+ credentials = await loginKimi(ctrl);
700
+ break;
701
+ case "cursor":
702
+ credentials = await loginCursor(
703
+ url => ctrl.onAuth({ url }),
704
+ ctrl.onProgress ? () => ctrl.onProgress?.("Waiting for browser authentication...") : undefined,
705
+ );
706
+ break;
707
+ case "perplexity":
708
+ credentials = await loginPerplexity(ctrl);
709
+ break;
710
+ case "huggingface": {
711
+ const apiKey = await loginHuggingface(ctrl);
712
+ await saveApiKeyCredential(apiKey);
713
+ return;
714
+ }
715
+ case "opencode": {
716
+ const apiKey = await loginOpenCode(ctrl);
717
+ await saveApiKeyCredential(apiKey);
718
+ return;
719
+ }
720
+ case "ollama": {
721
+ const apiKey = await loginOllama(ctrl);
722
+ if (!apiKey) {
723
+ return;
724
+ }
725
+ await saveApiKeyCredential(apiKey);
726
+ return;
727
+ }
728
+ case "cerebras": {
729
+ const apiKey = await loginCerebras(ctrl);
730
+ await saveApiKeyCredential(apiKey);
731
+ return;
732
+ }
733
+ case "zai": {
734
+ const apiKey = await loginZai(ctrl);
735
+ await saveApiKeyCredential(apiKey);
736
+ return;
737
+ }
738
+ case "qianfan": {
739
+ const apiKey = await loginQianfan(ctrl);
740
+ await saveApiKeyCredential(apiKey);
741
+ return;
742
+ }
743
+ case "minimax-code": {
744
+ const apiKey = await loginMiniMaxCode(ctrl);
745
+ await saveApiKeyCredential(apiKey);
746
+ return;
747
+ }
748
+ case "minimax-code-cn": {
749
+ const apiKey = await loginMiniMaxCodeCn(ctrl);
750
+ await saveApiKeyCredential(apiKey);
751
+ return;
752
+ }
753
+ case "synthetic": {
754
+ const apiKey = await loginSynthetic(ctrl);
755
+ await saveApiKeyCredential(apiKey);
756
+ return;
757
+ }
758
+ case "venice": {
759
+ const apiKey = await loginVenice(ctrl);
760
+ await saveApiKeyCredential(apiKey);
761
+ return;
762
+ }
763
+ case "litellm": {
764
+ const apiKey = await loginLiteLLM(ctrl);
765
+ await saveApiKeyCredential(apiKey);
766
+ return;
767
+ }
768
+ case "moonshot": {
769
+ const apiKey = await loginMoonshot(ctrl);
770
+ await saveApiKeyCredential(apiKey);
771
+ return;
772
+ }
773
+ case "nanogpt": {
774
+ const apiKey = await loginNanoGPT(ctrl);
775
+ await saveApiKeyCredential(apiKey);
776
+ return;
777
+ }
778
+ case "together": {
779
+ const apiKey = await loginTogether(ctrl);
780
+ await saveApiKeyCredential(apiKey);
781
+ return;
782
+ }
783
+ case "cloudflare-ai-gateway": {
784
+ const apiKey = await loginCloudflareAiGateway(ctrl);
785
+ await saveApiKeyCredential(apiKey);
786
+ return;
787
+ }
788
+ case "vllm": {
789
+ const apiKey = await loginVllm(ctrl);
790
+ await saveApiKeyCredential(apiKey);
791
+ return;
792
+ }
793
+ case "qwen-portal": {
794
+ const apiKey = await loginQwenPortal(ctrl);
795
+ await saveApiKeyCredential(apiKey);
796
+ return;
797
+ }
798
+ case "nvidia": {
799
+ const apiKey = await loginNvidia(ctrl);
800
+ await saveApiKeyCredential(apiKey);
801
+ return;
802
+ }
803
+ case "xiaomi": {
804
+ const apiKey = await loginXiaomi(ctrl);
805
+ await saveApiKeyCredential(apiKey);
806
+ return;
807
+ }
808
+ default: {
809
+ const customProvider = getOAuthProvider(provider);
810
+ if (!customProvider) {
811
+ throw new Error(`Unknown OAuth provider: ${provider}`);
812
+ }
813
+ const customLoginResult = await customProvider.login({
814
+ onAuth: info => ctrl.onAuth(info),
815
+ onProgress: ctrl.onProgress,
816
+ onPrompt: ctrl.onPrompt,
817
+ onManualCodeInput: async () =>
818
+ ctrl.onPrompt({ message: "Paste the authorization code (or full redirect URL):" }),
819
+ signal: ctrl.signal,
820
+ });
821
+ if (typeof customLoginResult === "string") {
822
+ await saveApiKeyCredential(customLoginResult);
823
+ return;
824
+ }
825
+ credentials = customLoginResult;
826
+ break;
827
+ }
828
+ }
829
+ const newCredential: OAuthCredential = { type: "oauth", ...credentials };
830
+ const existing = this.#getCredentialsForProvider(provider);
831
+ if (existing.length === 0) {
832
+ await this.set(provider, newCredential);
833
+ return;
834
+ }
835
+ await this.set(provider, [...existing, newCredential]);
836
+ }
837
+
838
+ /**
839
+ * Logout from a provider.
840
+ */
841
+ async logout(provider: string): Promise<void> {
842
+ await this.remove(provider);
843
+ }
844
+
845
+ // ─────────────────────────────────────────────────────────────────────────────
846
+ // Usage API Integration
847
+ // Queries provider usage endpoints to detect rate limits before they occur.
848
+ // ─────────────────────────────────────────────────────────────────────────────
849
+
850
+ #buildUsageCredential(credential: OAuthCredential): UsageCredential {
851
+ return {
852
+ type: "oauth",
853
+ accessToken: credential.access,
854
+ refreshToken: credential.refresh,
855
+ expiresAt: credential.expires,
856
+ accountId: credential.accountId,
857
+ projectId: credential.projectId,
858
+ email: credential.email,
859
+ enterpriseUrl: credential.enterpriseUrl,
860
+ };
861
+ }
862
+
863
+ #getUsageReportMetadataValue(report: UsageReport, key: string): string | undefined {
864
+ const metadata = report.metadata;
865
+ if (!metadata || typeof metadata !== "object") return undefined;
866
+ const value = metadata[key];
867
+ return typeof value === "string" ? value.trim() : undefined;
868
+ }
869
+
870
+ #getUsageReportScopeAccountId(report: UsageReport): string | undefined {
871
+ const ids = new Set<string>();
872
+ for (const limit of report.limits) {
873
+ const accountId = limit.scope.accountId?.trim();
874
+ if (accountId) ids.add(accountId);
875
+ }
876
+ if (ids.size === 1) return [...ids][0];
877
+ return undefined;
878
+ }
879
+
880
+ #getUsageReportIdentifiers(report: UsageReport): string[] {
881
+ const identifiers: string[] = [];
882
+ const email = this.#getUsageReportMetadataValue(report, "email");
883
+ if (email) identifiers.push(`email:${email.toLowerCase()}`);
884
+ const accountId = this.#getUsageReportMetadataValue(report, "accountId");
885
+ if (accountId) identifiers.push(`account:${accountId}`);
886
+ const account = this.#getUsageReportMetadataValue(report, "account");
887
+ if (account) identifiers.push(`account:${account}`);
888
+ const user = this.#getUsageReportMetadataValue(report, "user");
889
+ if (user) identifiers.push(`account:${user}`);
890
+ const username = this.#getUsageReportMetadataValue(report, "username");
891
+ if (username) identifiers.push(`account:${username}`);
892
+ const scopeAccountId = this.#getUsageReportScopeAccountId(report);
893
+ if (scopeAccountId) identifiers.push(`account:${scopeAccountId}`);
894
+ return identifiers.map(identifier => `${report.provider}:${identifier.toLowerCase()}`);
895
+ }
896
+
897
+ #mergeUsageReportGroup(reports: UsageReport[]): UsageReport {
898
+ if (reports.length === 1) return reports[0];
899
+ const sorted = [...reports].sort((a, b) => {
900
+ const limitDiff = b.limits.length - a.limits.length;
901
+ if (limitDiff !== 0) return limitDiff;
902
+ return (b.fetchedAt ?? 0) - (a.fetchedAt ?? 0);
903
+ });
904
+ const base = sorted[0];
905
+ const mergedLimits = [...base.limits];
906
+ const limitIds = new Set(mergedLimits.map(limit => limit.id));
907
+ const mergedMetadata: Record<string, unknown> = { ...(base.metadata ?? {}) };
908
+ let fetchedAt = base.fetchedAt;
909
+
910
+ for (const report of sorted.slice(1)) {
911
+ fetchedAt = Math.max(fetchedAt, report.fetchedAt);
912
+ for (const limit of report.limits) {
913
+ if (!limitIds.has(limit.id)) {
914
+ limitIds.add(limit.id);
915
+ mergedLimits.push(limit);
916
+ }
917
+ }
918
+ if (report.metadata) {
919
+ for (const [key, value] of Object.entries(report.metadata)) {
920
+ if (mergedMetadata[key] === undefined) {
921
+ mergedMetadata[key] = value;
922
+ }
923
+ }
924
+ }
925
+ }
926
+
927
+ return {
928
+ ...base,
929
+ fetchedAt,
930
+ limits: mergedLimits,
931
+ metadata: Object.keys(mergedMetadata).length > 0 ? mergedMetadata : undefined,
932
+ };
933
+ }
934
+
935
+ #dedupeUsageReports(reports: UsageReport[]): UsageReport[] {
936
+ const groups: UsageReport[][] = [];
937
+ const idToGroup = new Map<string, number>();
938
+
939
+ for (const report of reports) {
940
+ const identifiers = this.#getUsageReportIdentifiers(report);
941
+ let groupIndex: number | undefined;
942
+ for (const identifier of identifiers) {
943
+ const existing = idToGroup.get(identifier);
944
+ if (existing !== undefined) {
945
+ groupIndex = existing;
946
+ break;
947
+ }
948
+ }
949
+ if (groupIndex === undefined) {
950
+ groupIndex = groups.length;
951
+ groups.push([]);
952
+ }
953
+ groups[groupIndex].push(report);
954
+ for (const identifier of identifiers) {
955
+ idToGroup.set(identifier, groupIndex);
956
+ }
957
+ }
958
+
959
+ const deduped = groups.map(group => this.#mergeUsageReportGroup(group));
960
+ if (deduped.length !== reports.length) {
961
+ this.#usageLogger?.debug("Usage reports deduped", {
962
+ before: reports.length,
963
+ after: deduped.length,
964
+ });
965
+ }
966
+ return deduped;
967
+ }
968
+
969
+ #isUsageLimitExhausted(limit: UsageLimit): boolean {
970
+ if (limit.status === "exhausted") return true;
971
+ const amount = limit.amount;
972
+ if (amount.usedFraction !== undefined && amount.usedFraction >= 1) return true;
973
+ if (amount.remainingFraction !== undefined && amount.remainingFraction <= 0) return true;
974
+ if (amount.used !== undefined && amount.limit !== undefined && amount.used >= amount.limit) return true;
975
+ if (amount.remaining !== undefined && amount.remaining <= 0) return true;
976
+ if (amount.unit === "percent" && amount.used !== undefined && amount.used >= 100) return true;
977
+ return false;
978
+ }
979
+
980
+ /** Returns true if usage indicates rate limit has been reached. */
981
+ #isUsageLimitReached(report: UsageReport): boolean {
982
+ return report.limits.some(limit => this.#isUsageLimitExhausted(limit));
983
+ }
984
+
985
+ /** Extracts the earliest reset timestamp from exhausted windows (in ms). */
986
+ #getUsageResetAtMs(report: UsageReport, nowMs: number): number | undefined {
987
+ const candidates: number[] = [];
988
+ for (const limit of report.limits) {
989
+ if (!this.#isUsageLimitExhausted(limit)) continue;
990
+ const window = limit.window;
991
+ if (window?.resetsAt && window.resetsAt > nowMs) {
992
+ candidates.push(window.resetsAt);
993
+ }
994
+ if (window?.resetInMs && window.resetInMs > 0) {
995
+ const resetAt = nowMs + window.resetInMs;
996
+ if (resetAt > nowMs) candidates.push(resetAt);
997
+ }
998
+ }
999
+ if (candidates.length === 0) return undefined;
1000
+ return Math.min(...candidates);
1001
+ }
1002
+
1003
+ async #getUsageReport(
1004
+ provider: Provider,
1005
+ credential: OAuthCredential,
1006
+ options?: { baseUrl?: string },
1007
+ ): Promise<UsageReport | null> {
1008
+ const resolver = this.#usageProviderResolver;
1009
+ const cache = this.#usageCache;
1010
+ if (!resolver || !cache) return null;
1011
+
1012
+ const providerImpl = resolver(provider);
1013
+ if (!providerImpl) return null;
1014
+
1015
+ const params = {
1016
+ provider,
1017
+ credential: this.#buildUsageCredential(credential),
1018
+ baseUrl: options?.baseUrl,
1019
+ };
1020
+
1021
+ if (providerImpl.supports && !providerImpl.supports(params)) return null;
1022
+
1023
+ try {
1024
+ return await providerImpl.fetchUsage(params, {
1025
+ cache,
1026
+ fetch: this.#usageFetch,
1027
+ now: this.#usageNow,
1028
+ logger: this.#usageLogger,
1029
+ });
1030
+ } catch (error) {
1031
+ logger.debug("AuthStorage usage fetch failed", {
1032
+ provider,
1033
+ error: String(error),
1034
+ });
1035
+ return null;
1036
+ }
1037
+ }
1038
+
1039
+ async fetchUsageReports(options?: {
1040
+ baseUrlResolver?: (provider: Provider) => string | undefined;
1041
+ }): Promise<UsageReport[] | null> {
1042
+ const resolver = this.#usageProviderResolver;
1043
+ const cache = this.#usageCache;
1044
+ if (!resolver || !cache) return null;
1045
+
1046
+ const tasks: Array<Promise<UsageReport | null>> = [];
1047
+ const providers = new Set<string>([
1048
+ ...this.#data.keys(),
1049
+ ...DEFAULT_USAGE_PROVIDERS.map(provider => provider.id),
1050
+ ]);
1051
+ this.#usageLogger?.debug("Usage fetch requested", {
1052
+ providers: Array.from(providers).sort(),
1053
+ });
1054
+ for (const provider of providers) {
1055
+ const providerImpl = resolver(provider as Provider);
1056
+ if (!providerImpl) continue;
1057
+ const baseUrl = options?.baseUrlResolver?.(provider as Provider);
1058
+ let entries = this.#getStoredCredentials(provider);
1059
+ if (entries.length > 0) {
1060
+ const dedupedEntries = this.#pruneDuplicateStoredCredentials(provider, entries);
1061
+ if (dedupedEntries.length !== entries.length) {
1062
+ this.#setStoredCredentials(provider, dedupedEntries);
1063
+ }
1064
+ entries = dedupedEntries;
1065
+ }
1066
+
1067
+ if (entries.length === 0) {
1068
+ const runtimeKey = this.#runtimeOverrides.get(provider);
1069
+ const envKey = getEnvApiKey(provider);
1070
+ const apiKey = runtimeKey ?? envKey;
1071
+ if (!apiKey) {
1072
+ continue;
1073
+ }
1074
+ const params = {
1075
+ provider: provider as Provider,
1076
+ credential: { type: "api_key", apiKey } satisfies UsageCredential,
1077
+ baseUrl,
1078
+ };
1079
+ if (providerImpl.supports && !providerImpl.supports(params)) {
1080
+ continue;
1081
+ }
1082
+ this.#usageLogger?.debug("Usage fetch queued", {
1083
+ provider,
1084
+ credentialType: "api_key",
1085
+ baseUrl,
1086
+ });
1087
+ tasks.push(
1088
+ providerImpl
1089
+ .fetchUsage(params, {
1090
+ cache,
1091
+ fetch: this.#usageFetch,
1092
+ now: this.#usageNow,
1093
+ logger: this.#usageLogger,
1094
+ })
1095
+ .catch(error => {
1096
+ logger.debug("AuthStorage usage fetch failed", {
1097
+ provider,
1098
+ error: String(error),
1099
+ });
1100
+ return null;
1101
+ }),
1102
+ );
1103
+ continue;
1104
+ }
1105
+
1106
+ for (const entry of entries) {
1107
+ const credential = entry.credential;
1108
+ const usageCredential: UsageCredential =
1109
+ credential.type === "api_key"
1110
+ ? { type: "api_key", apiKey: credential.key }
1111
+ : this.#buildUsageCredential(credential);
1112
+ const params = {
1113
+ provider: provider as Provider,
1114
+ credential: usageCredential,
1115
+ baseUrl,
1116
+ };
1117
+
1118
+ if (providerImpl.supports && !providerImpl.supports(params)) {
1119
+ continue;
1120
+ }
1121
+
1122
+ this.#usageLogger?.debug("Usage fetch queued", {
1123
+ provider,
1124
+ credentialType: usageCredential.type,
1125
+ baseUrl,
1126
+ accountId: usageCredential.accountId,
1127
+ email: usageCredential.email,
1128
+ });
1129
+
1130
+ tasks.push(
1131
+ providerImpl
1132
+ .fetchUsage(params, {
1133
+ cache,
1134
+ fetch: this.#usageFetch,
1135
+ now: this.#usageNow,
1136
+ logger: this.#usageLogger,
1137
+ })
1138
+ .catch(error => {
1139
+ logger.debug("AuthStorage usage fetch failed", {
1140
+ provider,
1141
+ error: String(error),
1142
+ });
1143
+ return null;
1144
+ }),
1145
+ );
1146
+ }
1147
+ }
1148
+
1149
+ if (tasks.length === 0) return [];
1150
+ const results = await Promise.all(tasks);
1151
+ const reports = results.filter((report): report is UsageReport => report !== null);
1152
+ const deduped = this.#dedupeUsageReports(reports);
1153
+ this.#usageLogger?.debug("Usage fetch resolved", {
1154
+ reports: deduped.map(report => {
1155
+ const accountLabel =
1156
+ this.#getUsageReportMetadataValue(report, "email") ??
1157
+ this.#getUsageReportMetadataValue(report, "accountId") ??
1158
+ this.#getUsageReportMetadataValue(report, "account") ??
1159
+ this.#getUsageReportMetadataValue(report, "user") ??
1160
+ this.#getUsageReportMetadataValue(report, "username") ??
1161
+ this.#getUsageReportScopeAccountId(report);
1162
+ return {
1163
+ provider: report.provider,
1164
+ limits: report.limits.length,
1165
+ account: accountLabel,
1166
+ };
1167
+ }),
1168
+ });
1169
+ return deduped;
1170
+ }
1171
+
1172
+ /**
1173
+ * Marks the current session's credential as temporarily blocked due to usage limits.
1174
+ * Uses usage reports to determine accurate reset time when available.
1175
+ * Returns true if a credential was blocked, enabling automatic fallback to the next credential.
1176
+ */
1177
+ async markUsageLimitReached(
1178
+ provider: string,
1179
+ sessionId: string | undefined,
1180
+ options?: { retryAfterMs?: number; baseUrl?: string },
1181
+ ): Promise<boolean> {
1182
+ const sessionCredential = this.#getSessionCredential(provider, sessionId);
1183
+ if (!sessionCredential) return false;
1184
+
1185
+ const providerKey = this.#getProviderTypeKey(provider, sessionCredential.type);
1186
+ const now = this.#usageNow();
1187
+ let blockedUntil = now + (options?.retryAfterMs ?? AuthStorage.#defaultBackoffMs);
1188
+
1189
+ if (provider === "openai-codex" && sessionCredential.type === "oauth") {
1190
+ const credential = this.#getCredentialsForProvider(provider)[sessionCredential.index];
1191
+ if (credential?.type === "oauth") {
1192
+ const report = await this.#getUsageReport(provider, credential, options);
1193
+ if (report && this.#isUsageLimitReached(report)) {
1194
+ const resetAtMs = this.#getUsageResetAtMs(report, this.#usageNow());
1195
+ if (resetAtMs && resetAtMs > blockedUntil) {
1196
+ blockedUntil = resetAtMs;
1197
+ }
1198
+ }
1199
+ }
1200
+ }
1201
+
1202
+ this.#markCredentialBlocked(providerKey, sessionCredential.index, blockedUntil);
1203
+
1204
+ const remainingCredentials = this.#getCredentialsForProvider(provider)
1205
+ .map((credential, index) => ({ credential, index }))
1206
+ .filter(
1207
+ (entry): entry is { credential: AuthCredential; index: number } =>
1208
+ entry.credential.type === sessionCredential.type && entry.index !== sessionCredential.index,
1209
+ );
1210
+
1211
+ return remainingCredentials.some(candidate => !this.#isCredentialBlocked(providerKey, candidate.index));
1212
+ }
1213
+
1214
+ /**
1215
+ * Resolves an OAuth API key, trying credentials in priority order.
1216
+ * Skips blocked credentials and checks usage limits for providers with usage data.
1217
+ * Falls back to earliest-unblocking credential if all are blocked.
1218
+ */
1219
+ async #resolveOAuthApiKey(
1220
+ provider: string,
1221
+ sessionId?: string,
1222
+ options?: { baseUrl?: string },
1223
+ ): Promise<string | undefined> {
1224
+ const credentials = this.#getCredentialsForProvider(provider)
1225
+ .map((credential, index) => ({ credential, index }))
1226
+ .filter((entry): entry is { credential: OAuthCredential; index: number } => entry.credential.type === "oauth");
1227
+
1228
+ if (credentials.length === 0) return undefined;
1229
+
1230
+ const providerKey = this.#getProviderTypeKey(provider, "oauth");
1231
+ const order = this.#getCredentialOrder(providerKey, sessionId, credentials.length);
1232
+ const fallback = credentials[order[0]];
1233
+ const checkUsage = provider === "openai-codex" && credentials.length > 1;
1234
+
1235
+ for (const idx of order) {
1236
+ const selection = credentials[idx];
1237
+ const apiKey = await this.#tryOAuthCredential(
1238
+ provider,
1239
+ selection,
1240
+ providerKey,
1241
+ sessionId,
1242
+ options,
1243
+ checkUsage,
1244
+ false,
1245
+ );
1246
+ if (apiKey) return apiKey;
1247
+ }
1248
+
1249
+ if (fallback && this.#isCredentialBlocked(providerKey, fallback.index)) {
1250
+ return this.#tryOAuthCredential(provider, fallback, providerKey, sessionId, options, checkUsage, true);
1251
+ }
1252
+
1253
+ return undefined;
1254
+ }
1255
+
1256
+ /** Attempts to use a single OAuth credential, checking usage and refreshing token. */
1257
+ async #tryOAuthCredential(
1258
+ provider: string,
1259
+ selection: { credential: OAuthCredential; index: number },
1260
+ providerKey: string,
1261
+ sessionId: string | undefined,
1262
+ options: { baseUrl?: string } | undefined,
1263
+ checkUsage: boolean,
1264
+ allowBlocked: boolean,
1265
+ ): Promise<string | undefined> {
1266
+ if (!allowBlocked && this.#isCredentialBlocked(providerKey, selection.index)) {
1267
+ return undefined;
1268
+ }
1269
+
1270
+ let usage: UsageReport | null = null;
1271
+ let usageChecked = false;
1272
+
1273
+ if (checkUsage && !allowBlocked) {
1274
+ usage = await this.#getUsageReport(provider, selection.credential, options);
1275
+ usageChecked = true;
1276
+ if (usage && this.#isUsageLimitReached(usage)) {
1277
+ const resetAtMs = this.#getUsageResetAtMs(usage, this.#usageNow());
1278
+ this.#markCredentialBlocked(
1279
+ providerKey,
1280
+ selection.index,
1281
+ resetAtMs ?? this.#usageNow() + AuthStorage.#defaultBackoffMs,
1282
+ );
1283
+ return undefined;
1284
+ }
1285
+ }
1286
+
1287
+ try {
1288
+ let result: { newCredentials: OAuthCredentials; apiKey: string } | null;
1289
+ const customProvider = getOAuthProvider(provider);
1290
+ if (customProvider) {
1291
+ let refreshedCredentials: OAuthCredentials = selection.credential;
1292
+ if (Date.now() >= refreshedCredentials.expires) {
1293
+ if (!customProvider.refreshToken) {
1294
+ throw new Error(`OAuth provider "${provider}" does not support token refresh`);
1295
+ }
1296
+ refreshedCredentials = await customProvider.refreshToken(refreshedCredentials);
1297
+ }
1298
+ const apiKey = customProvider.getApiKey
1299
+ ? customProvider.getApiKey(refreshedCredentials)
1300
+ : refreshedCredentials.access;
1301
+ result = { newCredentials: refreshedCredentials, apiKey };
1302
+ } else {
1303
+ const oauthCreds: Record<string, OAuthCredentials> = {
1304
+ [provider]: selection.credential,
1305
+ };
1306
+ result = await getOAuthApiKey(provider as OAuthProvider, oauthCreds);
1307
+ }
1308
+ if (!result) return undefined;
1309
+ const updated: OAuthCredential = {
1310
+ type: "oauth",
1311
+ access: result.newCredentials.access,
1312
+ refresh: result.newCredentials.refresh,
1313
+ expires: result.newCredentials.expires,
1314
+ accountId: result.newCredentials.accountId ?? selection.credential.accountId,
1315
+ email: result.newCredentials.email ?? selection.credential.email,
1316
+ projectId: result.newCredentials.projectId ?? selection.credential.projectId,
1317
+ enterpriseUrl: result.newCredentials.enterpriseUrl ?? selection.credential.enterpriseUrl,
1318
+ };
1319
+ this.#replaceCredentialAt(provider, selection.index, updated);
1320
+ if (checkUsage && !allowBlocked) {
1321
+ const sameAccount = selection.credential.accountId === updated.accountId;
1322
+ if (!usageChecked || !sameAccount) {
1323
+ usage = await this.#getUsageReport(provider, updated, options);
1324
+ }
1325
+ if (usage && this.#isUsageLimitReached(usage)) {
1326
+ const resetAtMs = this.#getUsageResetAtMs(usage, this.#usageNow());
1327
+ this.#markCredentialBlocked(
1328
+ providerKey,
1329
+ selection.index,
1330
+ resetAtMs ?? this.#usageNow() + AuthStorage.#defaultBackoffMs,
1331
+ );
1332
+ return undefined;
1333
+ }
1334
+ }
1335
+ this.#recordSessionCredential(provider, sessionId, "oauth", selection.index);
1336
+ return result.apiKey;
1337
+ } catch (error) {
1338
+ const errorMsg = String(error);
1339
+ // Only remove credentials for definitive auth failures
1340
+ // Keep credentials for transient errors (network, 5xx) and block temporarily
1341
+ const isDefinitiveFailure =
1342
+ /invalid_grant|invalid_token|revoked|unauthorized|expired.*refresh|refresh.*expired/i.test(errorMsg) ||
1343
+ (/\b(401|403)\b/.test(errorMsg) && !/timeout|network|fetch failed|ECONNREFUSED/i.test(errorMsg));
1344
+
1345
+ logger.warn("OAuth token refresh failed", {
1346
+ provider,
1347
+ index: selection.index,
1348
+ error: errorMsg,
1349
+ isDefinitiveFailure,
1350
+ });
1351
+
1352
+ if (isDefinitiveFailure) {
1353
+ // Permanently remove invalid credentials
1354
+ this.#removeCredentialAt(provider, selection.index);
1355
+ if (this.#getCredentialsForProvider(provider).some(credential => credential.type === "oauth")) {
1356
+ return this.getApiKey(provider, sessionId, options);
1357
+ }
1358
+ } else {
1359
+ // Block temporarily for transient failures (5 minutes)
1360
+ this.#markCredentialBlocked(providerKey, selection.index, this.#usageNow() + 5 * 60 * 1000);
1361
+ }
1362
+ }
1363
+
1364
+ return undefined;
1365
+ }
1366
+
1367
+ /**
1368
+ * Peek at API key for a provider without refreshing OAuth tokens.
1369
+ * Used for model discovery where we only need to know if credentials exist
1370
+ * and get a best-effort token. The actual refresh happens lazily when the
1371
+ * provider is used for an API call.
1372
+ */
1373
+ async peekApiKey(provider: string): Promise<string | undefined> {
1374
+ const runtimeKey = this.#runtimeOverrides.get(provider);
1375
+ if (runtimeKey) {
1376
+ return runtimeKey;
1377
+ }
1378
+
1379
+ const apiKeySelection = this.#selectCredentialByType(provider, "api_key");
1380
+ if (apiKeySelection) {
1381
+ return resolveConfigValue(apiKeySelection.credential.key);
1382
+ }
1383
+
1384
+ // Return current OAuth access token only if it is not already expired.
1385
+ const oauthSelection = this.#selectCredentialByType(provider, "oauth");
1386
+ if (oauthSelection) {
1387
+ const expiresAt = oauthSelection.credential.expires;
1388
+ if (Number.isFinite(expiresAt) && expiresAt > Date.now()) {
1389
+ return oauthSelection.credential.access;
1390
+ }
1391
+ }
1392
+
1393
+ const envKey = getEnvApiKey(provider);
1394
+ if (envKey) return envKey;
1395
+
1396
+ return this.#fallbackResolver?.(provider) ?? undefined;
1397
+ }
1398
+
1399
+ /**
1400
+ * Get API key for a provider.
1401
+ * Priority:
1402
+ * 1. Runtime override (CLI --api-key)
1403
+ * 2. API key from agent.db
1404
+ * 3. OAuth token from agent.db (auto-refreshed)
1405
+ * 4. Environment variable
1406
+ * 5. Fallback resolver (models.json custom providers)
1407
+ */
1408
+ async getApiKey(provider: string, sessionId?: string, options?: { baseUrl?: string }): Promise<string | undefined> {
1409
+ // Runtime override takes highest priority
1410
+ const runtimeKey = this.#runtimeOverrides.get(provider);
1411
+ if (runtimeKey) {
1412
+ return runtimeKey;
1413
+ }
1414
+
1415
+ const apiKeySelection = this.#selectCredentialByType(provider, "api_key", sessionId);
1416
+ if (apiKeySelection) {
1417
+ this.#recordSessionCredential(provider, sessionId, "api_key", apiKeySelection.index);
1418
+ return resolveConfigValue(apiKeySelection.credential.key);
1419
+ }
1420
+
1421
+ const oauthKey = await this.#resolveOAuthApiKey(provider, sessionId, options);
1422
+ if (oauthKey) {
1423
+ return oauthKey;
1424
+ }
1425
+
1426
+ // Fall back to environment variable
1427
+ const envKey = getEnvApiKey(provider);
1428
+ if (envKey) return envKey;
1429
+
1430
+ // Fall back to custom resolver (e.g., models.json custom providers)
1431
+ return this.#fallbackResolver?.(provider) ?? undefined;
1432
+ }
1433
+ }