@clawpump/claw-agent 0.1.5 → 0.1.7

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 (1212) hide show
  1. package/agent/.dockerignore +67 -0
  2. package/agent/.envrc +1 -1
  3. package/agent/.gitattributes +8 -0
  4. package/agent/AGENTS.md +216 -4
  5. package/agent/CONTRIBUTING.md +46 -8
  6. package/agent/Dockerfile +78 -35
  7. package/agent/MANIFEST.in +2 -0
  8. package/agent/README.md +12 -5
  9. package/agent/README.ur-pk.md +261 -0
  10. package/agent/README.zh-CN.md +11 -8
  11. package/agent/SECURITY.md +5 -4
  12. package/agent/acp_adapter/provenance.py +127 -0
  13. package/agent/acp_adapter/server.py +112 -5
  14. package/agent/acp_adapter/session.py +1 -6
  15. package/agent/acp_registry/agent.json +2 -2
  16. package/agent/agent/account_usage.py +313 -1
  17. package/agent/agent/agent_init.py +140 -37
  18. package/agent/agent/agent_runtime_helpers.py +342 -83
  19. package/agent/agent/anthropic_adapter.py +320 -33
  20. package/agent/agent/auxiliary_client.py +525 -105
  21. package/agent/agent/background_review.py +157 -19
  22. package/agent/agent/bedrock_adapter.py +71 -6
  23. package/agent/agent/billing_view.py +295 -0
  24. package/agent/agent/chat_completion_helpers.py +229 -4
  25. package/agent/agent/codex_responses_adapter.py +86 -10
  26. package/agent/agent/codex_runtime.py +153 -1
  27. package/agent/agent/coding_context.py +738 -0
  28. package/agent/agent/context_compressor.py +392 -44
  29. package/agent/agent/context_references.py +34 -1
  30. package/agent/agent/conversation_compression.py +159 -22
  31. package/agent/agent/conversation_loop.py +643 -908
  32. package/agent/agent/copilot_acp_client.py +4 -11
  33. package/agent/agent/credential_pool.py +5 -3
  34. package/agent/agent/credits_tracker.py +794 -0
  35. package/agent/agent/curator.py +91 -18
  36. package/agent/agent/curator_backup.py +26 -10
  37. package/agent/agent/display.py +42 -1
  38. package/agent/agent/error_classifier.py +52 -3
  39. package/agent/agent/errors.py +3 -0
  40. package/agent/agent/file_safety.py +0 -17
  41. package/agent/agent/gemini_native_adapter.py +31 -1
  42. package/agent/agent/i18n.py +48 -4
  43. package/agent/agent/image_gen_provider.py +74 -5
  44. package/agent/agent/image_routing.py +29 -0
  45. package/agent/agent/insights.py +8 -17
  46. package/agent/agent/lsp/install.py +3 -0
  47. package/agent/agent/memory_manager.py +326 -31
  48. package/agent/agent/message_content.py +50 -0
  49. package/agent/agent/model_metadata.py +214 -3
  50. package/agent/agent/moonshot_schema.py +8 -1
  51. package/agent/agent/onboarding.py +60 -0
  52. package/agent/agent/prompt_builder.py +327 -37
  53. package/agent/agent/redact.py +1 -0
  54. package/agent/agent/runtime_cwd.py +34 -5
  55. package/agent/agent/secret_scope.py +205 -0
  56. package/agent/agent/secret_sources/bitwarden.py +34 -2
  57. package/agent/agent/skill_commands.py +90 -1
  58. package/agent/agent/skill_preprocessing.py +1 -0
  59. package/agent/agent/skill_utils.py +209 -36
  60. package/agent/agent/ssl_guard.py +94 -0
  61. package/agent/agent/system_prompt.py +133 -5
  62. package/agent/agent/tool_executor.py +496 -70
  63. package/agent/agent/transports/anthropic.py +83 -21
  64. package/agent/agent/transports/chat_completions.py +94 -5
  65. package/agent/agent/transports/codex.py +67 -2
  66. package/agent/agent/transports/codex_app_server.py +1 -0
  67. package/agent/agent/transports/codex_app_server_session.py +30 -0
  68. package/agent/agent/transports/types.py +12 -0
  69. package/agent/agent/turn_context.py +408 -0
  70. package/agent/agent/turn_finalizer.py +428 -0
  71. package/agent/agent/turn_retry_state.py +68 -0
  72. package/agent/agent/usage_pricing.py +3 -0
  73. package/agent/apps/bootstrap-installer/package.json +6 -5
  74. package/agent/apps/bootstrap-installer/src/routes/failure.tsx +12 -5
  75. package/agent/apps/bootstrap-installer/src/routes/progress.tsx +1 -3
  76. package/agent/apps/bootstrap-installer/src/store.ts +3 -2
  77. package/agent/apps/bootstrap-installer/src-tauri/src/bootstrap.rs +172 -7
  78. package/agent/apps/bootstrap-installer/src-tauri/src/events.rs +14 -1
  79. package/agent/apps/bootstrap-installer/src-tauri/src/paths.rs +29 -0
  80. package/agent/apps/bootstrap-installer/src-tauri/src/powershell.rs +93 -3
  81. package/agent/apps/bootstrap-installer/src-tauri/src/update.rs +695 -39
  82. package/agent/apps/bootstrap-installer/tsconfig.json +3 -4
  83. package/agent/apps/desktop/DESIGN.md +167 -0
  84. package/agent/apps/desktop/README.md +20 -16
  85. package/agent/apps/desktop/assets/icon.icns +0 -0
  86. package/agent/apps/desktop/assets/icon.ico +0 -0
  87. package/agent/apps/desktop/assets/icon.png +0 -0
  88. package/agent/apps/desktop/electron/backend-env.cjs +112 -0
  89. package/agent/apps/desktop/electron/backend-env.test.cjs +111 -0
  90. package/agent/apps/desktop/electron/backend-probes.test.cjs +3 -1
  91. package/agent/apps/desktop/electron/backend-ready.cjs +66 -0
  92. package/agent/apps/desktop/electron/bootstrap-platform.cjs +52 -0
  93. package/agent/apps/desktop/electron/bootstrap-platform.test.cjs +59 -1
  94. package/agent/apps/desktop/electron/bootstrap-runner.cjs +176 -38
  95. package/agent/apps/desktop/electron/bootstrap-runner.test.cjs +112 -1
  96. package/agent/apps/desktop/electron/connection-config.cjs +288 -0
  97. package/agent/apps/desktop/electron/connection-config.test.cjs +396 -0
  98. package/agent/apps/desktop/electron/dashboard-token.cjs +99 -0
  99. package/agent/apps/desktop/electron/dashboard-token.test.cjs +142 -0
  100. package/agent/apps/desktop/electron/desktop-uninstall.cjs +232 -0
  101. package/agent/apps/desktop/electron/desktop-uninstall.test.cjs +246 -0
  102. package/agent/apps/desktop/electron/entitlements.mac.inherit.plist +2 -0
  103. package/agent/apps/desktop/electron/fs-read-dir.cjs +109 -0
  104. package/agent/apps/desktop/electron/fs-read-dir.test.cjs +364 -0
  105. package/agent/apps/desktop/electron/gateway-ws-probe.cjs +188 -0
  106. package/agent/apps/desktop/electron/gateway-ws-probe.test.cjs +122 -0
  107. package/agent/apps/desktop/electron/git-root.cjs +54 -0
  108. package/agent/apps/desktop/electron/git-root.test.cjs +40 -0
  109. package/agent/apps/desktop/electron/git-worktrees.cjs +174 -0
  110. package/agent/apps/desktop/electron/hardening.cjs +123 -28
  111. package/agent/apps/desktop/electron/hardening.test.cjs +163 -0
  112. package/agent/apps/desktop/electron/main.cjs +3121 -331
  113. package/agent/apps/desktop/electron/oauth-net-request.cjs +20 -0
  114. package/agent/apps/desktop/electron/oauth-net-request.test.cjs +34 -0
  115. package/agent/apps/desktop/electron/preload.cjs +52 -2
  116. package/agent/apps/desktop/electron/session-windows.cjs +124 -0
  117. package/agent/apps/desktop/electron/session-windows.test.cjs +199 -0
  118. package/agent/apps/desktop/electron/update-rebuild.cjs +29 -0
  119. package/agent/apps/desktop/electron/update-rebuild.test.cjs +55 -0
  120. package/agent/apps/desktop/electron/update-remote.cjs +56 -0
  121. package/agent/apps/desktop/electron/update-remote.test.cjs +78 -0
  122. package/agent/apps/desktop/electron/vscode-marketplace.cjs +331 -0
  123. package/agent/apps/desktop/electron/vscode-marketplace.test.cjs +113 -0
  124. package/agent/apps/desktop/electron/windows-child-process.test.cjs +57 -0
  125. package/agent/apps/desktop/electron/windows-user-env.cjs +76 -0
  126. package/agent/apps/desktop/electron/windows-user-env.test.cjs +90 -0
  127. package/agent/apps/desktop/electron/workspace-cwd.cjs +38 -0
  128. package/agent/apps/desktop/electron/workspace-cwd.test.cjs +45 -0
  129. package/agent/apps/desktop/eslint.config.mjs +0 -3
  130. package/agent/apps/desktop/index.html +27 -2
  131. package/agent/apps/desktop/package.json +31 -11
  132. package/agent/apps/desktop/pr-assets/session-source-folders.png +0 -0
  133. package/agent/apps/desktop/public/apple-touch-icon.png +0 -0
  134. package/agent/apps/desktop/public/nous-girl.jpg +0 -0
  135. package/agent/apps/desktop/scripts/assert-dist-built.cjs +70 -0
  136. package/agent/apps/desktop/scripts/assert-dist-built.test.cjs +84 -0
  137. package/agent/apps/desktop/scripts/before-pack.cjs +78 -0
  138. package/agent/apps/desktop/scripts/before-pack.test.cjs +53 -0
  139. package/agent/apps/desktop/scripts/diag-scroll-reset.mjs +229 -0
  140. package/agent/apps/desktop/scripts/patch-electron-builder-mac-binary.cjs +64 -0
  141. package/agent/apps/desktop/scripts/run-electron-builder.cjs +57 -0
  142. package/agent/apps/desktop/src/app/agents/index.tsx +53 -45
  143. package/agent/apps/desktop/src/app/artifacts/index.tsx +102 -83
  144. package/agent/apps/desktop/src/app/chat/chat-drop-overlay.tsx +29 -8
  145. package/agent/apps/desktop/src/app/chat/chat-swap-overlay.tsx +47 -0
  146. package/agent/apps/desktop/src/app/chat/composer/attachments.tsx +81 -45
  147. package/agent/apps/desktop/src/app/chat/composer/completion-drawer.tsx +13 -24
  148. package/agent/apps/desktop/src/app/chat/composer/context-menu.tsx +138 -88
  149. package/agent/apps/desktop/src/app/chat/composer/controls.tsx +138 -90
  150. package/agent/apps/desktop/src/app/chat/composer/enter-submit-dom-race.test.tsx +218 -0
  151. package/agent/apps/desktop/src/app/chat/composer/focus.ts +32 -0
  152. package/agent/apps/desktop/src/app/chat/composer/help-hint.tsx +38 -25
  153. package/agent/apps/desktop/src/app/chat/composer/hooks/use-live-completion-adapter.ts +7 -0
  154. package/agent/apps/desktop/src/app/chat/composer/hooks/use-mic-recorder.ts +22 -12
  155. package/agent/apps/desktop/src/app/chat/composer/hooks/use-slash-completions.ts +142 -14
  156. package/agent/apps/desktop/src/app/chat/composer/hooks/use-voice-conversation.ts +14 -11
  157. package/agent/apps/desktop/src/app/chat/composer/hooks/use-voice-recorder.ts +9 -6
  158. package/agent/apps/desktop/src/app/chat/composer/ime-composition-dom-repro.test.tsx +108 -0
  159. package/agent/apps/desktop/src/app/chat/composer/index.tsx +930 -180
  160. package/agent/apps/desktop/src/app/chat/composer/inline-refs.ts +136 -32
  161. package/agent/apps/desktop/src/app/chat/composer/model-pill.tsx +86 -0
  162. package/agent/apps/desktop/src/app/chat/composer/queue-panel.tsx +54 -75
  163. package/agent/apps/desktop/src/app/chat/composer/rich-editor.test.ts +117 -1
  164. package/agent/apps/desktop/src/app/chat/composer/rich-editor.ts +117 -6
  165. package/agent/apps/desktop/src/app/chat/composer/slash-nav-dom-repro.test.tsx +186 -0
  166. package/agent/apps/desktop/src/app/chat/composer/status-stack/index.tsx +202 -0
  167. package/agent/apps/desktop/src/app/chat/composer/status-stack/status-row.tsx +155 -0
  168. package/agent/apps/desktop/src/app/chat/composer/text-utils.test.ts +104 -0
  169. package/agent/apps/desktop/src/app/chat/composer/text-utils.ts +37 -9
  170. package/agent/apps/desktop/src/app/chat/composer/trigger-popover.test.tsx +50 -0
  171. package/agent/apps/desktop/src/app/chat/composer/trigger-popover.tsx +105 -40
  172. package/agent/apps/desktop/src/app/chat/composer/types.ts +5 -0
  173. package/agent/apps/desktop/src/app/chat/composer/url-dialog.tsx +11 -15
  174. package/agent/apps/desktop/src/app/chat/composer/voice-activity.tsx +8 -4
  175. package/agent/apps/desktop/src/app/chat/hooks/use-composer-actions.test.ts +57 -0
  176. package/agent/apps/desktop/src/app/chat/hooks/use-composer-actions.ts +70 -16
  177. package/agent/apps/desktop/src/app/chat/hooks/use-file-drop-zone.ts +52 -16
  178. package/agent/apps/desktop/src/app/chat/index.tsx +234 -81
  179. package/agent/apps/desktop/src/app/chat/perf-probe.tsx +69 -21
  180. package/agent/apps/desktop/src/app/chat/right-rail/preview-console.tsx +44 -40
  181. package/agent/apps/desktop/src/app/chat/right-rail/preview-file.tsx +71 -25
  182. package/agent/apps/desktop/src/app/chat/right-rail/preview-pane.test.tsx +40 -1
  183. package/agent/apps/desktop/src/app/chat/right-rail/preview-pane.tsx +55 -53
  184. package/agent/apps/desktop/src/app/chat/right-rail/preview.tsx +35 -17
  185. package/agent/apps/desktop/src/app/chat/scroll-to-bottom-button.test.tsx +67 -0
  186. package/agent/apps/desktop/src/app/chat/scroll-to-bottom-button.tsx +74 -0
  187. package/agent/apps/desktop/src/app/chat/sidebar/cron-jobs-section.tsx +356 -0
  188. package/agent/apps/desktop/src/app/chat/sidebar/index.tsx +1189 -364
  189. package/agent/apps/desktop/src/app/chat/sidebar/load-more-row.tsx +30 -0
  190. package/agent/apps/desktop/src/app/chat/sidebar/order.test.ts +21 -0
  191. package/agent/apps/desktop/src/app/chat/sidebar/order.ts +17 -0
  192. package/agent/apps/desktop/src/app/chat/sidebar/profile-switcher.tsx +524 -0
  193. package/agent/apps/desktop/src/app/chat/sidebar/session-actions-menu.tsx +80 -45
  194. package/agent/apps/desktop/src/app/chat/sidebar/session-row.tsx +120 -25
  195. package/agent/apps/desktop/src/app/chat/sidebar/virtual-session-list.tsx +7 -13
  196. package/agent/apps/desktop/src/app/chat/sidebar/workspace-groups.test.ts +149 -0
  197. package/agent/apps/desktop/src/app/chat/sidebar/workspace-groups.ts +326 -0
  198. package/agent/apps/desktop/src/app/chat/thread-loading.ts +7 -2
  199. package/agent/apps/desktop/src/app/command-center/index.tsx +320 -581
  200. package/agent/apps/desktop/src/app/command-palette/index.tsx +681 -0
  201. package/agent/apps/desktop/src/app/command-palette/marketplace-theme-page.tsx +157 -0
  202. package/agent/apps/desktop/src/app/cron/index.tsx +392 -324
  203. package/agent/apps/desktop/src/app/cron/job-state.ts +29 -0
  204. package/agent/apps/desktop/src/app/desktop-controller.tsx +618 -123
  205. package/agent/apps/desktop/src/app/floating-hud.ts +22 -0
  206. package/agent/apps/desktop/src/app/gateway/hooks/use-gateway-boot.test.tsx +265 -0
  207. package/agent/apps/desktop/src/app/gateway/hooks/use-gateway-boot.ts +260 -14
  208. package/agent/apps/desktop/src/app/gateway/hooks/use-gateway-request.ts +48 -4
  209. package/agent/apps/desktop/src/app/hooks/use-keybinds.ts +270 -0
  210. package/agent/apps/desktop/src/app/hooks/use-refresh-hotkey.ts +45 -0
  211. package/agent/apps/desktop/src/app/layout-constants.ts +19 -0
  212. package/agent/apps/desktop/src/app/messaging/index.tsx +136 -241
  213. package/agent/apps/desktop/src/app/messaging/platform-icon.tsx +95 -0
  214. package/agent/apps/desktop/src/app/model-visibility-overlay.tsx +31 -0
  215. package/agent/apps/desktop/src/app/overlays/overlay-search-input.tsx +18 -62
  216. package/agent/apps/desktop/src/app/overlays/overlay-split-layout.tsx +59 -7
  217. package/agent/apps/desktop/src/app/overlays/overlay-view.tsx +9 -5
  218. package/agent/apps/desktop/src/app/page-search-shell.tsx +42 -20
  219. package/agent/apps/desktop/src/app/profiles/create-profile-dialog.tsx +165 -0
  220. package/agent/apps/desktop/src/app/profiles/delete-profile-dialog.tsx +65 -0
  221. package/agent/apps/desktop/src/app/profiles/index.tsx +174 -199
  222. package/agent/apps/desktop/src/app/profiles/rename-profile-dialog.tsx +125 -0
  223. package/agent/apps/desktop/src/app/right-sidebar/files/dnd-manager.ts +27 -0
  224. package/agent/apps/desktop/src/app/right-sidebar/files/ipc.test.ts +100 -0
  225. package/agent/apps/desktop/src/app/right-sidebar/files/ipc.ts +12 -18
  226. package/agent/apps/desktop/src/app/right-sidebar/files/remote-picker.tsx +177 -0
  227. package/agent/apps/desktop/src/app/right-sidebar/files/tree.tsx +35 -21
  228. package/agent/apps/desktop/src/app/right-sidebar/files/use-project-tree.test.ts +75 -3
  229. package/agent/apps/desktop/src/app/right-sidebar/files/use-project-tree.ts +152 -5
  230. package/agent/apps/desktop/src/app/right-sidebar/index.test.tsx +75 -0
  231. package/agent/apps/desktop/src/app/right-sidebar/index.tsx +166 -129
  232. package/agent/apps/desktop/src/app/right-sidebar/store.ts +19 -4
  233. package/agent/apps/desktop/src/app/right-sidebar/terminal/buffer.ts +65 -0
  234. package/agent/apps/desktop/src/app/right-sidebar/terminal/index.tsx +29 -34
  235. package/agent/apps/desktop/src/app/right-sidebar/terminal/persistent.tsx +18 -6
  236. package/agent/apps/desktop/src/app/right-sidebar/terminal/selection.ts +93 -32
  237. package/agent/apps/desktop/src/app/right-sidebar/terminal/use-terminal-session.ts +381 -119
  238. package/agent/apps/desktop/src/app/routes.ts +9 -0
  239. package/agent/apps/desktop/src/app/session/hooks/use-cwd-actions.ts +17 -7
  240. package/agent/apps/desktop/src/app/session/hooks/use-message-stream.ts +365 -47
  241. package/agent/apps/desktop/src/app/session/hooks/use-model-controls.test.tsx +198 -0
  242. package/agent/apps/desktop/src/app/session/hooks/use-model-controls.ts +70 -34
  243. package/agent/apps/desktop/src/app/session/hooks/use-prompt-actions.test.tsx +1061 -0
  244. package/agent/apps/desktop/src/app/session/hooks/use-prompt-actions.ts +1143 -165
  245. package/agent/apps/desktop/src/app/session/hooks/use-route-resume.test.tsx +341 -2
  246. package/agent/apps/desktop/src/app/session/hooks/use-route-resume.ts +176 -5
  247. package/agent/apps/desktop/src/app/session/hooks/use-session-actions.test.tsx +259 -0
  248. package/agent/apps/desktop/src/app/session/hooks/use-session-actions.ts +452 -149
  249. package/agent/apps/desktop/src/app/session/hooks/use-session-state-cache.test.tsx +327 -0
  250. package/agent/apps/desktop/src/app/session/hooks/use-session-state-cache.ts +133 -4
  251. package/agent/apps/desktop/src/app/session-picker-overlay.tsx +32 -0
  252. package/agent/apps/desktop/src/app/session-switcher.tsx +107 -0
  253. package/agent/apps/desktop/src/app/settings/about-settings.tsx +45 -36
  254. package/agent/apps/desktop/src/app/settings/appearance-settings.tsx +243 -162
  255. package/agent/apps/desktop/src/app/settings/config-settings.tsx +86 -66
  256. package/agent/apps/desktop/src/app/settings/constants.ts +459 -122
  257. package/agent/apps/desktop/src/app/settings/credential-key-ui.tsx +373 -0
  258. package/agent/apps/desktop/src/app/settings/env-credentials.tsx +198 -0
  259. package/agent/apps/desktop/src/app/settings/env-var-actions-menu.tsx +136 -0
  260. package/agent/apps/desktop/src/app/settings/field-copy.ts +56 -0
  261. package/agent/apps/desktop/src/app/settings/gateway-settings.tsx +385 -72
  262. package/agent/apps/desktop/src/app/settings/helpers.test.ts +156 -1
  263. package/agent/apps/desktop/src/app/settings/helpers.ts +30 -2
  264. package/agent/apps/desktop/src/app/settings/index.tsx +118 -84
  265. package/agent/apps/desktop/src/app/settings/keys-settings.tsx +62 -419
  266. package/agent/apps/desktop/src/app/settings/mcp-settings.tsx +65 -60
  267. package/agent/apps/desktop/src/app/settings/model-settings.test.tsx +129 -5
  268. package/agent/apps/desktop/src/app/settings/model-settings.tsx +370 -65
  269. package/agent/apps/desktop/src/app/settings/notifications-settings.tsx +150 -0
  270. package/agent/apps/desktop/src/app/settings/primitives.tsx +5 -11
  271. package/agent/apps/desktop/src/app/settings/provider-config-panel.test.tsx +142 -0
  272. package/agent/apps/desktop/src/app/settings/provider-config-panel.tsx +182 -0
  273. package/agent/apps/desktop/src/app/settings/providers-settings.test.tsx +171 -0
  274. package/agent/apps/desktop/src/app/settings/providers-settings.tsx +471 -0
  275. package/agent/apps/desktop/src/app/settings/sessions-settings.tsx +183 -71
  276. package/agent/apps/desktop/src/app/settings/toolset-config-panel.test.tsx +135 -1
  277. package/agent/apps/desktop/src/app/settings/toolset-config-panel.tsx +180 -57
  278. package/agent/apps/desktop/src/app/settings/types.ts +9 -6
  279. package/agent/apps/desktop/src/app/settings/uninstall-section.tsx +185 -0
  280. package/agent/apps/desktop/src/app/settings/use-deep-link-highlight.ts +60 -0
  281. package/agent/apps/desktop/src/app/shell/app-shell.tsx +59 -13
  282. package/agent/apps/desktop/src/app/shell/gateway-menu-panel.tsx +37 -32
  283. package/agent/apps/desktop/src/app/shell/hooks/use-overlay-routing.ts +6 -3
  284. package/agent/apps/desktop/src/app/shell/hooks/use-statusbar-items.tsx +212 -53
  285. package/agent/apps/desktop/src/app/shell/keybind-panel.tsx +215 -0
  286. package/agent/apps/desktop/src/app/shell/model-edit-submenu.test.tsx +84 -0
  287. package/agent/apps/desktop/src/app/shell/model-edit-submenu.tsx +244 -0
  288. package/agent/apps/desktop/src/app/shell/model-menu-panel.tsx +392 -0
  289. package/agent/apps/desktop/src/app/shell/statusbar-controls.tsx +23 -33
  290. package/agent/apps/desktop/src/app/shell/titlebar-controls.tsx +79 -95
  291. package/agent/apps/desktop/src/app/shell/titlebar.ts +8 -2
  292. package/agent/apps/desktop/src/app/skills/index.test.tsx +11 -0
  293. package/agent/apps/desktop/src/app/skills/index.tsx +79 -64
  294. package/agent/apps/desktop/src/app/types.ts +85 -0
  295. package/agent/apps/desktop/src/app/updates-overlay.tsx +110 -105
  296. package/agent/apps/desktop/src/components/assistant-ui/ansi-text.tsx +34 -0
  297. package/agent/apps/desktop/src/components/assistant-ui/block-direction.test.tsx +129 -0
  298. package/agent/apps/desktop/src/components/assistant-ui/clarify-tool.tsx +102 -81
  299. package/agent/apps/desktop/src/components/assistant-ui/directive-text.tsx +92 -15
  300. package/agent/apps/desktop/src/components/assistant-ui/markdown-text.test.ts +38 -0
  301. package/agent/apps/desktop/src/components/assistant-ui/markdown-text.tsx +304 -45
  302. package/agent/apps/desktop/src/components/assistant-ui/message-render-boundary.test.tsx +80 -0
  303. package/agent/apps/desktop/src/components/assistant-ui/message-render-boundary.tsx +48 -0
  304. package/agent/apps/desktop/src/components/assistant-ui/streaming.test.tsx +142 -90
  305. package/agent/apps/desktop/src/components/assistant-ui/thread-list.tsx +337 -0
  306. package/agent/apps/desktop/src/components/assistant-ui/thread.tsx +667 -190
  307. package/agent/apps/desktop/src/components/assistant-ui/tool-approval-group.test.tsx +299 -0
  308. package/agent/apps/desktop/src/components/assistant-ui/tool-approval.test.tsx +133 -0
  309. package/agent/apps/desktop/src/components/assistant-ui/tool-approval.tsx +239 -0
  310. package/agent/apps/desktop/src/components/assistant-ui/tool-fallback-model.test.ts +31 -0
  311. package/agent/apps/desktop/src/components/assistant-ui/tool-fallback-model.ts +152 -134
  312. package/agent/apps/desktop/src/components/assistant-ui/tool-fallback.tsx +142 -150
  313. package/agent/apps/desktop/src/components/assistant-ui/tooltip-icon-button.tsx +14 -12
  314. package/agent/apps/desktop/src/components/assistant-ui/user-message-edit.test.tsx +141 -0
  315. package/agent/apps/desktop/src/components/assistant-ui/user-message-text.tsx +152 -0
  316. package/agent/apps/desktop/src/components/boot-failure-overlay.tsx +150 -33
  317. package/agent/apps/desktop/src/components/boot-failure-reauth.test.ts +100 -0
  318. package/agent/apps/desktop/src/components/boot-failure-reauth.ts +81 -0
  319. package/agent/apps/desktop/src/components/brand-mark.tsx +19 -0
  320. package/agent/apps/desktop/src/components/chat/code-card.tsx +1 -1
  321. package/agent/apps/desktop/src/components/chat/composer-dock.ts +31 -0
  322. package/agent/apps/desktop/src/components/chat/diff-lines.tsx +1 -1
  323. package/agent/apps/desktop/src/components/chat/disclosure-row.tsx +13 -3
  324. package/agent/apps/desktop/src/components/chat/expandable-block.tsx +52 -0
  325. package/agent/apps/desktop/src/components/chat/generated-image-result.tsx +174 -0
  326. package/agent/apps/desktop/src/components/chat/image-generation-placeholder.tsx +70 -37
  327. package/agent/apps/desktop/src/components/chat/intro.tsx +8 -7
  328. package/agent/apps/desktop/src/components/chat/preview-attachment.tsx +4 -2
  329. package/agent/apps/desktop/src/components/chat/shiki-highlighter.test.ts +37 -0
  330. package/agent/apps/desktop/src/components/chat/shiki-highlighter.tsx +96 -22
  331. package/agent/apps/desktop/src/components/chat/status-row.tsx +70 -0
  332. package/agent/apps/desktop/src/components/chat/status-section.tsx +42 -0
  333. package/agent/apps/desktop/src/components/chat/terminal-output.tsx +54 -0
  334. package/agent/apps/desktop/src/components/chat/zoomable-image.tsx +70 -109
  335. package/agent/apps/desktop/src/components/desktop-install-overlay.tsx +154 -84
  336. package/agent/apps/desktop/src/components/desktop-onboarding-overlay.test.tsx +38 -8
  337. package/agent/apps/desktop/src/components/desktop-onboarding-overlay.tsx +789 -233
  338. package/agent/apps/desktop/src/components/error-boundary.tsx +77 -0
  339. package/agent/apps/desktop/src/components/gateway-connecting-overlay.test.tsx +144 -0
  340. package/agent/apps/desktop/src/components/gateway-connecting-overlay.tsx +7 -1
  341. package/agent/apps/desktop/src/components/haptics-provider.tsx +24 -0
  342. package/agent/apps/desktop/src/components/language-switcher.test.tsx +53 -0
  343. package/agent/apps/desktop/src/components/language-switcher.tsx +175 -0
  344. package/agent/apps/desktop/src/components/model-picker.tsx +42 -40
  345. package/agent/apps/desktop/src/components/model-visibility-dialog.tsx +166 -0
  346. package/agent/apps/desktop/src/components/notifications.tsx +48 -27
  347. package/agent/apps/desktop/src/components/pane-shell/index.ts +1 -1
  348. package/agent/apps/desktop/src/components/pane-shell/pane-shell.tsx +146 -9
  349. package/agent/apps/desktop/src/components/prompt-overlays.tsx +234 -0
  350. package/agent/apps/desktop/src/components/session-picker.tsx +108 -0
  351. package/agent/apps/desktop/src/components/ui/action-status.tsx +25 -0
  352. package/agent/apps/desktop/src/components/ui/badge.tsx +35 -0
  353. package/agent/apps/desktop/src/components/ui/button.tsx +37 -13
  354. package/agent/apps/desktop/src/components/ui/confirm-dialog.tsx +109 -0
  355. package/agent/apps/desktop/src/components/ui/control.ts +25 -0
  356. package/agent/apps/desktop/src/components/ui/copy-button.test.tsx +36 -0
  357. package/agent/apps/desktop/src/components/ui/copy-button.tsx +38 -27
  358. package/agent/apps/desktop/src/components/ui/dialog.tsx +39 -11
  359. package/agent/apps/desktop/src/components/ui/dropdown-menu.tsx +98 -24
  360. package/agent/apps/desktop/src/components/ui/error-state.tsx +50 -0
  361. package/agent/apps/desktop/src/components/ui/fade-text.tsx +9 -2
  362. package/agent/apps/desktop/src/components/ui/{braille-spinner.tsx → glyph-spinner.tsx} +15 -13
  363. package/agent/apps/desktop/src/components/ui/input.tsx +5 -2
  364. package/agent/apps/desktop/src/components/ui/kbd.tsx +83 -12
  365. package/agent/apps/desktop/src/components/ui/log-view.tsx +19 -0
  366. package/agent/apps/desktop/src/components/ui/pagination.tsx +12 -5
  367. package/agent/apps/desktop/src/components/ui/popover.tsx +44 -0
  368. package/agent/apps/desktop/src/components/ui/search-field.tsx +80 -0
  369. package/agent/apps/desktop/src/components/ui/segmented-control.tsx +51 -0
  370. package/agent/apps/desktop/src/components/ui/select.tsx +10 -3
  371. package/agent/apps/desktop/src/components/ui/sheet.tsx +8 -2
  372. package/agent/apps/desktop/src/components/ui/sidebar.tsx +18 -25
  373. package/agent/apps/desktop/src/components/ui/switch.tsx +38 -15
  374. package/agent/apps/desktop/src/components/ui/textarea.tsx +4 -11
  375. package/agent/apps/desktop/src/components/ui/tool-icon.tsx +65 -0
  376. package/agent/apps/desktop/src/components/ui/tooltip.tsx +31 -4
  377. package/agent/apps/desktop/src/fonts/JetBrainsMono-Bold.woff2 +0 -0
  378. package/agent/apps/desktop/src/fonts/JetBrainsMono-Italic.woff2 +0 -0
  379. package/agent/apps/desktop/src/fonts/JetBrainsMono-Regular.woff2 +0 -0
  380. package/agent/apps/desktop/src/global.d.ts +181 -4
  381. package/agent/apps/desktop/src/hermes.test.ts +60 -0
  382. package/agent/apps/desktop/src/hermes.ts +190 -13
  383. package/agent/apps/desktop/src/hooks/use-image-download.ts +85 -0
  384. package/agent/apps/desktop/src/hooks/use-resize-observer.ts +13 -4
  385. package/agent/apps/desktop/src/hooks/use-worktree-info.ts +68 -0
  386. package/agent/apps/desktop/src/i18n/catalog.ts +12 -0
  387. package/agent/apps/desktop/src/i18n/context.test.tsx +232 -0
  388. package/agent/apps/desktop/src/i18n/context.tsx +183 -0
  389. package/agent/apps/desktop/src/i18n/define-locale.ts +41 -0
  390. package/agent/apps/desktop/src/i18n/en.ts +1921 -0
  391. package/agent/apps/desktop/src/i18n/index.ts +20 -0
  392. package/agent/apps/desktop/src/i18n/ja.ts +2053 -0
  393. package/agent/apps/desktop/src/i18n/languages.test.ts +43 -0
  394. package/agent/apps/desktop/src/i18n/languages.ts +86 -0
  395. package/agent/apps/desktop/src/i18n/runtime.test.ts +75 -0
  396. package/agent/apps/desktop/src/i18n/runtime.ts +53 -0
  397. package/agent/apps/desktop/src/i18n/types.ts +1559 -0
  398. package/agent/apps/desktop/src/i18n/zh-hant.ts +1992 -0
  399. package/agent/apps/desktop/src/i18n/zh.ts +2099 -0
  400. package/agent/apps/desktop/src/lib/ansi.test.ts +123 -0
  401. package/agent/apps/desktop/src/lib/ansi.ts +186 -0
  402. package/agent/apps/desktop/src/lib/chat-messages.test.ts +79 -0
  403. package/agent/apps/desktop/src/lib/chat-messages.ts +68 -29
  404. package/agent/apps/desktop/src/lib/chat-runtime.test.ts +65 -1
  405. package/agent/apps/desktop/src/lib/chat-runtime.ts +39 -3
  406. package/agent/apps/desktop/src/lib/completion-sound.ts +519 -0
  407. package/agent/apps/desktop/src/lib/desktop-fs.test.ts +116 -0
  408. package/agent/apps/desktop/src/lib/desktop-fs.ts +113 -0
  409. package/agent/apps/desktop/src/lib/desktop-slash-commands.test.ts +89 -6
  410. package/agent/apps/desktop/src/lib/desktop-slash-commands.ts +270 -131
  411. package/agent/apps/desktop/src/lib/external-link.test.tsx +27 -0
  412. package/agent/apps/desktop/src/lib/external-link.tsx +9 -2
  413. package/agent/apps/desktop/src/lib/gateway-events.test.ts +27 -0
  414. package/agent/apps/desktop/src/lib/gateway-events.ts +16 -0
  415. package/agent/apps/desktop/src/lib/gateway-ws-url.test.ts +78 -0
  416. package/agent/apps/desktop/src/lib/gateway-ws-url.ts +91 -0
  417. package/agent/apps/desktop/src/lib/generated-images.test.ts +97 -0
  418. package/agent/apps/desktop/src/lib/generated-images.ts +116 -0
  419. package/agent/apps/desktop/src/lib/haptics.ts +17 -0
  420. package/agent/apps/desktop/src/lib/icons.ts +10 -2
  421. package/agent/apps/desktop/src/lib/keybinds/actions.ts +137 -0
  422. package/agent/apps/desktop/src/lib/keybinds/combo.test.ts +86 -0
  423. package/agent/apps/desktop/src/lib/keybinds/combo.ts +195 -0
  424. package/agent/apps/desktop/src/lib/local-preview.ts +23 -2
  425. package/agent/apps/desktop/src/lib/markdown-preprocess.ts +20 -7
  426. package/agent/apps/desktop/src/lib/media.remote.test.ts +90 -0
  427. package/agent/apps/desktop/src/lib/media.ts +40 -1
  428. package/agent/apps/desktop/src/lib/model-status-label.test.ts +59 -0
  429. package/agent/apps/desktop/src/lib/model-status-label.ts +122 -0
  430. package/agent/apps/desktop/src/lib/mutable-ref.ts +6 -0
  431. package/agent/apps/desktop/src/lib/profile-color.ts +58 -0
  432. package/agent/apps/desktop/src/lib/query-client.ts +13 -0
  433. package/agent/apps/desktop/src/lib/remend-tail.test.ts +105 -0
  434. package/agent/apps/desktop/src/lib/remend-tail.ts +108 -0
  435. package/agent/apps/desktop/src/lib/session-export.ts +6 -3
  436. package/agent/apps/desktop/src/lib/session-ids.test.ts +44 -0
  437. package/agent/apps/desktop/src/lib/session-ids.ts +26 -0
  438. package/agent/apps/desktop/src/lib/session-search.test.ts +66 -0
  439. package/agent/apps/desktop/src/lib/session-search.ts +21 -0
  440. package/agent/apps/desktop/src/lib/session-source.ts +126 -0
  441. package/agent/apps/desktop/src/lib/storage.test.ts +25 -0
  442. package/agent/apps/desktop/src/lib/storage.ts +35 -1
  443. package/agent/apps/desktop/src/lib/todos.test.ts +46 -1
  444. package/agent/apps/desktop/src/lib/todos.ts +37 -0
  445. package/agent/apps/desktop/src/lib/tool-result-summary.ts +5 -1
  446. package/agent/apps/desktop/src/lib/update-copy.test.ts +38 -0
  447. package/agent/apps/desktop/src/lib/update-copy.ts +44 -0
  448. package/agent/apps/desktop/src/lib/use-enter-animation.ts +2 -2
  449. package/agent/apps/desktop/src/lib/yolo-session.ts +50 -0
  450. package/agent/apps/desktop/src/main.tsx +19 -19
  451. package/agent/apps/desktop/src/store/boot.ts +4 -3
  452. package/agent/apps/desktop/src/store/clarify.test.ts +81 -0
  453. package/agent/apps/desktop/src/store/clarify.ts +50 -13
  454. package/agent/apps/desktop/src/store/command-palette.ts +20 -0
  455. package/agent/apps/desktop/src/store/compaction.test.ts +53 -0
  456. package/agent/apps/desktop/src/store/compaction.ts +38 -0
  457. package/agent/apps/desktop/src/store/completion-sound.ts +32 -0
  458. package/agent/apps/desktop/src/store/composer-input-history.test.ts +147 -0
  459. package/agent/apps/desktop/src/store/composer-input-history.ts +158 -0
  460. package/agent/apps/desktop/src/store/composer-queue.test.ts +68 -0
  461. package/agent/apps/desktop/src/store/composer-queue.ts +76 -0
  462. package/agent/apps/desktop/src/store/composer-status.test.ts +99 -0
  463. package/agent/apps/desktop/src/store/composer-status.ts +277 -0
  464. package/agent/apps/desktop/src/store/composer.test.ts +106 -0
  465. package/agent/apps/desktop/src/store/composer.ts +116 -0
  466. package/agent/apps/desktop/src/store/cron.ts +19 -0
  467. package/agent/apps/desktop/src/store/gateway.ts +280 -6
  468. package/agent/apps/desktop/src/store/keybinds.ts +143 -0
  469. package/agent/apps/desktop/src/store/layout.ts +107 -9
  470. package/agent/apps/desktop/src/store/model-presets.test.ts +51 -0
  471. package/agent/apps/desktop/src/store/model-presets.ts +86 -0
  472. package/agent/apps/desktop/src/store/model-visibility.test.ts +99 -0
  473. package/agent/apps/desktop/src/store/model-visibility.ts +161 -0
  474. package/agent/apps/desktop/src/store/native-notifications.test.ts +192 -0
  475. package/agent/apps/desktop/src/store/native-notifications.ts +203 -0
  476. package/agent/apps/desktop/src/store/notifications.ts +10 -7
  477. package/agent/apps/desktop/src/store/onboarding.test.ts +271 -1
  478. package/agent/apps/desktop/src/store/onboarding.ts +268 -38
  479. package/agent/apps/desktop/src/store/preview.ts +10 -1
  480. package/agent/apps/desktop/src/store/profile.test.ts +89 -0
  481. package/agent/apps/desktop/src/store/profile.ts +395 -0
  482. package/agent/apps/desktop/src/store/prompts.test.ts +127 -0
  483. package/agent/apps/desktop/src/store/prompts.ts +117 -0
  484. package/agent/apps/desktop/src/store/session-switcher.test.ts +115 -0
  485. package/agent/apps/desktop/src/store/session-switcher.ts +128 -0
  486. package/agent/apps/desktop/src/store/session-sync.ts +25 -0
  487. package/agent/apps/desktop/src/store/session.test.ts +268 -2
  488. package/agent/apps/desktop/src/store/session.ts +392 -18
  489. package/agent/apps/desktop/src/store/subagents.ts +3 -0
  490. package/agent/apps/desktop/src/store/system-actions.ts +48 -0
  491. package/agent/apps/desktop/src/store/thread-scroll.ts +58 -5
  492. package/agent/apps/desktop/src/store/todos.test.ts +47 -0
  493. package/agent/apps/desktop/src/store/todos.ts +64 -0
  494. package/agent/apps/desktop/src/store/tool-dismiss.ts +45 -0
  495. package/agent/apps/desktop/src/store/translucency.ts +38 -0
  496. package/agent/apps/desktop/src/store/updates.test.ts +187 -2
  497. package/agent/apps/desktop/src/store/updates.ts +268 -18
  498. package/agent/apps/desktop/src/store/windows.test.ts +143 -0
  499. package/agent/apps/desktop/src/store/windows.ts +115 -0
  500. package/agent/apps/desktop/src/styles.css +510 -119
  501. package/agent/apps/desktop/src/themes/color.ts +142 -0
  502. package/agent/apps/desktop/src/themes/context.tsx +128 -75
  503. package/agent/apps/desktop/src/themes/install.test.ts +119 -0
  504. package/agent/apps/desktop/src/themes/install.ts +95 -0
  505. package/agent/apps/desktop/src/themes/presets.test.ts +33 -0
  506. package/agent/apps/desktop/src/themes/presets.ts +13 -4
  507. package/agent/apps/desktop/src/themes/profile-theme.test.ts +41 -0
  508. package/agent/apps/desktop/src/themes/types.ts +35 -0
  509. package/agent/apps/desktop/src/themes/user-themes.test.ts +63 -0
  510. package/agent/apps/desktop/src/themes/user-themes.ts +122 -0
  511. package/agent/apps/desktop/src/themes/vscode.test.ts +171 -0
  512. package/agent/apps/desktop/src/themes/vscode.ts +343 -0
  513. package/agent/apps/desktop/src/types/hermes.ts +138 -1
  514. package/agent/apps/desktop/tsconfig.json +2 -2
  515. package/agent/apps/desktop/vite.config.ts +18 -0
  516. package/agent/apps/shared/package.json +1 -1
  517. package/agent/apps/shared/src/json-rpc-gateway.ts +63 -2
  518. package/agent/apps/shared/tsconfig.json +2 -2
  519. package/agent/cli-config.yaml.example +78 -1
  520. package/agent/cli.py +2177 -3162
  521. package/agent/cron/blueprint_catalog.py +713 -0
  522. package/agent/cron/jobs.py +226 -110
  523. package/agent/cron/scheduler.py +468 -193
  524. package/agent/cron/scheduler_provider.py +177 -0
  525. package/agent/cron/scripts/__init__.py +1 -0
  526. package/agent/cron/scripts/classify_items.py +226 -0
  527. package/agent/cron/suggestion_catalog.py +154 -0
  528. package/agent/cron/suggestions.py +257 -0
  529. package/agent/docs/chronos-managed-cron-contract.md +196 -0
  530. package/agent/docs/design/profile-builder.md +146 -0
  531. package/agent/docs/middleware/README.md +260 -0
  532. package/agent/docs/observability/README.md +316 -0
  533. package/agent/docs/plans/2026-06-09-003-fix-telegram-stream-overflow-continuations-plan.md +240 -0
  534. package/agent/docs/rca-ssl-cacert-post-git-pull.md +54 -0
  535. package/agent/docs/relay-connector-contract.md +285 -0
  536. package/agent/gateway/authz_mixin.py +536 -0
  537. package/agent/gateway/channel_directory.py +65 -3
  538. package/agent/gateway/config.py +222 -12
  539. package/agent/gateway/display_config.py +10 -0
  540. package/agent/gateway/hooks.py +17 -0
  541. package/agent/gateway/kanban_watchers.py +1146 -0
  542. package/agent/gateway/message_timestamps.py +166 -0
  543. package/agent/gateway/platforms/ADDING_A_PLATFORM.md +29 -0
  544. package/agent/gateway/platforms/api_server.py +216 -38
  545. package/agent/gateway/platforms/base.py +210 -58
  546. package/agent/gateway/platforms/email.py +122 -12
  547. package/agent/gateway/platforms/feishu.py +80 -11
  548. package/agent/gateway/platforms/feishu_meeting_invite.py +212 -0
  549. package/agent/gateway/platforms/matrix.py +1498 -297
  550. package/agent/gateway/platforms/qqbot/adapter.py +6 -0
  551. package/agent/gateway/platforms/signal.py +8 -0
  552. package/agent/gateway/platforms/slack.py +308 -12
  553. package/agent/gateway/platforms/telegram.py +831 -24
  554. package/agent/gateway/platforms/webhook.py +109 -21
  555. package/agent/gateway/platforms/weixin.py +113 -2
  556. package/agent/gateway/platforms/whatsapp.py +94 -288
  557. package/agent/gateway/platforms/whatsapp_cloud.py +1956 -0
  558. package/agent/gateway/platforms/whatsapp_common.py +367 -0
  559. package/agent/gateway/platforms/yuanbao.py +608 -191
  560. package/agent/gateway/platforms/yuanbao_proto.py +232 -23
  561. package/agent/gateway/relay/__init__.py +375 -0
  562. package/agent/gateway/relay/adapter.py +222 -0
  563. package/agent/gateway/relay/auth.py +168 -0
  564. package/agent/gateway/relay/descriptor.py +118 -0
  565. package/agent/gateway/relay/transport.py +101 -0
  566. package/agent/gateway/relay/ws_transport.py +327 -0
  567. package/agent/gateway/response_filters.py +53 -0
  568. package/agent/gateway/rich_sent_store.py +80 -0
  569. package/agent/gateway/run.py +2940 -5001
  570. package/agent/gateway/session.py +109 -8
  571. package/agent/gateway/session_context.py +22 -4
  572. package/agent/gateway/slash_commands.py +3854 -0
  573. package/agent/gateway/status.py +141 -21
  574. package/agent/gateway/stream_consumer.py +288 -31
  575. package/agent/hermes-already-has-routines.md +1 -1
  576. package/agent/hermes_cli/__init__.py +62 -17
  577. package/agent/hermes_cli/_parser.py +30 -0
  578. package/agent/hermes_cli/_subprocess_compat.py +61 -0
  579. package/agent/hermes_cli/active_sessions.py +320 -0
  580. package/agent/hermes_cli/auth.py +707 -59
  581. package/agent/hermes_cli/auth_commands.py +39 -22
  582. package/agent/hermes_cli/backup.py +109 -7
  583. package/agent/hermes_cli/banner.py +88 -0
  584. package/agent/hermes_cli/blueprint_cmd.py +318 -0
  585. package/agent/hermes_cli/cli_agent_setup_mixin.py +684 -0
  586. package/agent/hermes_cli/cli_commands_mixin.py +2293 -0
  587. package/agent/hermes_cli/commands.py +215 -91
  588. package/agent/hermes_cli/config.py +967 -130
  589. package/agent/hermes_cli/container_boot.py +76 -11
  590. package/agent/hermes_cli/cron.py +5 -11
  591. package/agent/hermes_cli/curator.py +21 -0
  592. package/agent/hermes_cli/dashboard_auth/__init__.py +2 -0
  593. package/agent/hermes_cli/dashboard_auth/base.py +62 -0
  594. package/agent/hermes_cli/dashboard_auth/cookies.py +32 -19
  595. package/agent/hermes_cli/dashboard_auth/login_page.py +156 -6
  596. package/agent/hermes_cli/dashboard_auth/middleware.py +28 -4
  597. package/agent/hermes_cli/dashboard_auth/prefix.py +46 -2
  598. package/agent/hermes_cli/dashboard_auth/public_paths.py +6 -0
  599. package/agent/hermes_cli/dashboard_auth/routes.py +158 -2
  600. package/agent/hermes_cli/dashboard_auth/ws_tickets.py +85 -11
  601. package/agent/hermes_cli/dashboard_register.py +427 -0
  602. package/agent/hermes_cli/debug.py +155 -50
  603. package/agent/hermes_cli/doctor.py +255 -14
  604. package/agent/hermes_cli/dump.py +60 -6
  605. package/agent/hermes_cli/env_loader.py +33 -0
  606. package/agent/hermes_cli/gateway.py +755 -103
  607. package/agent/hermes_cli/gateway_enroll.py +250 -0
  608. package/agent/hermes_cli/gateway_windows.py +254 -11
  609. package/agent/hermes_cli/gui_uninstall.py +285 -0
  610. package/agent/hermes_cli/inventory.py +105 -4
  611. package/agent/hermes_cli/kanban.py +58 -71
  612. package/agent/hermes_cli/kanban_db.py +391 -14
  613. package/agent/hermes_cli/kanban_decompose.py +2 -2
  614. package/agent/hermes_cli/kanban_specify.py +3 -1
  615. package/agent/hermes_cli/logs.py +2 -0
  616. package/agent/hermes_cli/main.py +2889 -5287
  617. package/agent/hermes_cli/managed_scope.py +214 -0
  618. package/agent/hermes_cli/managed_uv.py +254 -0
  619. package/agent/hermes_cli/mcp_catalog.py +6 -3
  620. package/agent/hermes_cli/mcp_config.py +145 -21
  621. package/agent/hermes_cli/mcp_security.py +96 -0
  622. package/agent/hermes_cli/mcp_startup.py +32 -3
  623. package/agent/hermes_cli/memory_providers.py +149 -0
  624. package/agent/hermes_cli/memory_setup.py +97 -42
  625. package/agent/hermes_cli/middleware.py +313 -0
  626. package/agent/hermes_cli/model_catalog.py +31 -0
  627. package/agent/hermes_cli/model_cost_guard.py +134 -0
  628. package/agent/hermes_cli/model_normalize.py +2 -1
  629. package/agent/hermes_cli/model_setup_flows.py +2759 -0
  630. package/agent/hermes_cli/model_switch.py +242 -27
  631. package/agent/hermes_cli/models.py +284 -44
  632. package/agent/hermes_cli/nous_account.py +33 -6
  633. package/agent/hermes_cli/nous_billing.py +406 -0
  634. package/agent/hermes_cli/nous_subscription.py +202 -5
  635. package/agent/hermes_cli/platforms.py +1 -0
  636. package/agent/hermes_cli/plugins.py +218 -18
  637. package/agent/hermes_cli/plugins_cmd.py +249 -105
  638. package/agent/hermes_cli/portal_cli.py +56 -16
  639. package/agent/hermes_cli/profile_distribution.py +6 -1
  640. package/agent/hermes_cli/profiles.py +283 -32
  641. package/agent/hermes_cli/provider_catalog.py +170 -0
  642. package/agent/hermes_cli/providers.py +4 -1
  643. package/agent/hermes_cli/pty_bridge.py +53 -4
  644. package/agent/hermes_cli/runtime_provider.py +216 -34
  645. package/agent/hermes_cli/secret_prompt.py +4 -4
  646. package/agent/hermes_cli/secrets_cli.py +24 -0
  647. package/agent/hermes_cli/send_cmd.py +28 -2
  648. package/agent/hermes_cli/service_manager.py +166 -19
  649. package/agent/hermes_cli/session_listing.py +97 -0
  650. package/agent/hermes_cli/setup.py +158 -94
  651. package/agent/hermes_cli/setup_whatsapp_cloud.py +541 -0
  652. package/agent/hermes_cli/skills_config.py +8 -2
  653. package/agent/hermes_cli/skills_hub.py +149 -7
  654. package/agent/hermes_cli/status.py +2 -2
  655. package/agent/hermes_cli/subcommands/__init__.py +18 -0
  656. package/agent/hermes_cli/subcommands/_shared.py +29 -0
  657. package/agent/hermes_cli/subcommands/acp.py +52 -0
  658. package/agent/hermes_cli/subcommands/auth.py +109 -0
  659. package/agent/hermes_cli/subcommands/backup.py +38 -0
  660. package/agent/hermes_cli/subcommands/claw.py +92 -0
  661. package/agent/hermes_cli/subcommands/config.py +49 -0
  662. package/agent/hermes_cli/subcommands/cron.py +163 -0
  663. package/agent/hermes_cli/subcommands/dashboard.py +143 -0
  664. package/agent/hermes_cli/subcommands/debug.py +77 -0
  665. package/agent/hermes_cli/subcommands/doctor.py +35 -0
  666. package/agent/hermes_cli/subcommands/dump.py +28 -0
  667. package/agent/hermes_cli/subcommands/gateway.py +332 -0
  668. package/agent/hermes_cli/subcommands/gui.py +63 -0
  669. package/agent/hermes_cli/subcommands/hooks.py +77 -0
  670. package/agent/hermes_cli/subcommands/import_cmd.py +31 -0
  671. package/agent/hermes_cli/subcommands/insights.py +25 -0
  672. package/agent/hermes_cli/subcommands/login.py +78 -0
  673. package/agent/hermes_cli/subcommands/logout.py +28 -0
  674. package/agent/hermes_cli/subcommands/logs.py +78 -0
  675. package/agent/hermes_cli/subcommands/mcp.py +108 -0
  676. package/agent/hermes_cli/subcommands/memory.py +53 -0
  677. package/agent/hermes_cli/subcommands/model.py +72 -0
  678. package/agent/hermes_cli/subcommands/pairing.py +36 -0
  679. package/agent/hermes_cli/subcommands/plugins.py +94 -0
  680. package/agent/hermes_cli/subcommands/postinstall.py +23 -0
  681. package/agent/hermes_cli/subcommands/profile.py +203 -0
  682. package/agent/hermes_cli/subcommands/prompt_size.py +36 -0
  683. package/agent/hermes_cli/subcommands/security.py +62 -0
  684. package/agent/hermes_cli/subcommands/setup.py +58 -0
  685. package/agent/hermes_cli/subcommands/skills.py +298 -0
  686. package/agent/hermes_cli/subcommands/slack.py +60 -0
  687. package/agent/hermes_cli/subcommands/status.py +28 -0
  688. package/agent/hermes_cli/subcommands/tools.py +95 -0
  689. package/agent/hermes_cli/subcommands/uninstall.py +41 -0
  690. package/agent/hermes_cli/subcommands/update.py +70 -0
  691. package/agent/hermes_cli/subcommands/version.py +18 -0
  692. package/agent/hermes_cli/subcommands/webhook.py +76 -0
  693. package/agent/hermes_cli/subcommands/whatsapp.py +22 -0
  694. package/agent/hermes_cli/suggestions_cmd.py +153 -0
  695. package/agent/hermes_cli/telegram_managed_bot.py +358 -0
  696. package/agent/hermes_cli/tips.py +3 -4
  697. package/agent/hermes_cli/tools_config.py +155 -28
  698. package/agent/hermes_cli/uninstall.py +231 -35
  699. package/agent/hermes_cli/web_server.py +6190 -973
  700. package/agent/hermes_cli/win_pty_bridge.py +179 -0
  701. package/agent/hermes_cli/write_approval_commands.py +209 -0
  702. package/agent/hermes_constants.py +164 -33
  703. package/agent/hermes_logging.py +74 -2
  704. package/agent/hermes_state.py +919 -106
  705. package/agent/hermes_time.py +20 -0
  706. package/agent/locales/af.yaml +23 -0
  707. package/agent/locales/de.yaml +23 -0
  708. package/agent/locales/en.yaml +20 -0
  709. package/agent/locales/es.yaml +23 -0
  710. package/agent/locales/fr.yaml +23 -0
  711. package/agent/locales/ga.yaml +23 -0
  712. package/agent/locales/hu.yaml +23 -0
  713. package/agent/locales/it.yaml +23 -0
  714. package/agent/locales/ja.yaml +23 -0
  715. package/agent/locales/ko.yaml +23 -0
  716. package/agent/locales/pt.yaml +23 -0
  717. package/agent/locales/ru.yaml +23 -0
  718. package/agent/locales/tr.yaml +23 -0
  719. package/agent/locales/uk.yaml +23 -0
  720. package/agent/locales/zh-hant.yaml +23 -0
  721. package/agent/locales/zh.yaml +23 -0
  722. package/agent/model_tools.py +204 -40
  723. package/agent/optional-mcps/clawpump/manifest.yaml +4 -2
  724. package/agent/optional-mcps/clawpump-stdio/manifest.yaml +2 -0
  725. package/agent/optional-mcps/unreal-engine/manifest.yaml +54 -0
  726. package/agent/optional-skills/blockchain/hyperliquid/SKILL.md +2 -2
  727. package/agent/optional-skills/blockchain/hyperliquid/scripts/hyperliquid_client.py +1 -1
  728. package/agent/optional-skills/creative/kanban-video-orchestrator/SKILL.md +1 -1
  729. package/agent/optional-skills/creative/kanban-video-orchestrator/assets/setup.sh.tmpl +4 -3
  730. package/agent/optional-skills/creative/kanban-video-orchestrator/references/kanban-setup.md +6 -4
  731. package/agent/optional-skills/creative/kanban-video-orchestrator/references/tool-matrix.md +2 -2
  732. package/agent/{skills/software-development → optional-skills/devops}/hermes-s6-container-supervision/SKILL.md +2 -0
  733. package/agent/optional-skills/devops/watchers/SKILL.md +1 -1
  734. package/agent/optional-skills/devops/watchers/scripts/watch_github.py +2 -1
  735. package/agent/optional-skills/payments/mpp-agent/SKILL.md +124 -0
  736. package/agent/optional-skills/payments/stripe-link-cli/SKILL.md +184 -0
  737. package/agent/optional-skills/payments/stripe-projects/SKILL.md +120 -0
  738. package/agent/optional-skills/productivity/canvas/SKILL.md +1 -1
  739. package/agent/optional-skills/productivity/canvas/scripts/canvas_api.py +4 -1
  740. package/agent/optional-skills/productivity/shop/SKILL.md +224 -0
  741. package/agent/optional-skills/productivity/shop/references/catalog-mcp.md +236 -0
  742. package/agent/optional-skills/productivity/shop/references/direct-api.md +278 -0
  743. package/agent/optional-skills/productivity/shop/references/legal.md +3 -0
  744. package/agent/optional-skills/productivity/shop/references/safety.md +36 -0
  745. package/agent/optional-skills/productivity/shopify/SKILL.md +1 -1
  746. package/agent/optional-skills/productivity/siyuan/SKILL.md +1 -1
  747. package/agent/optional-skills/productivity/telephony/SKILL.md +4 -4
  748. package/agent/optional-skills/productivity/telephony/scripts/telephony.py +15 -15
  749. package/agent/optional-skills/security/1password/SKILL.md +1 -1
  750. package/agent/{skills/red-teaming → optional-skills/security}/godmode/SKILL.md +3 -4
  751. package/agent/{skills/red-teaming → optional-skills/security}/godmode/scripts/auto_jailbreak.py +3 -1
  752. package/agent/optional-skills/software-development/rest-graphql-debug/SKILL.md +1 -1
  753. package/agent/{skills → optional-skills}/software-development/subagent-driven-development/SKILL.md +5 -5
  754. package/agent/package-lock.json +4082 -7907
  755. package/agent/package.json +18 -3
  756. package/agent/plugins/browser/firecrawl/provider.py +4 -1
  757. package/agent/plugins/cron/__init__.py +344 -0
  758. package/agent/plugins/cron/chronos/__init__.py +241 -0
  759. package/agent/plugins/cron/chronos/_nas_client.py +123 -0
  760. package/agent/plugins/cron/chronos/plugin.yaml +9 -0
  761. package/agent/plugins/cron/chronos/verify.py +103 -0
  762. package/agent/plugins/dashboard_auth/basic/__init__.py +491 -0
  763. package/agent/plugins/dashboard_auth/basic/plugin.yaml +7 -0
  764. package/agent/plugins/dashboard_auth/nous/__init__.py +12 -14
  765. package/agent/plugins/dashboard_auth/self_hosted/__init__.py +736 -0
  766. package/agent/plugins/dashboard_auth/self_hosted/plugin.yaml +8 -0
  767. package/agent/plugins/disk-cleanup/disk_cleanup.py +100 -20
  768. package/agent/plugins/google_meet/audio_bridge.py +4 -0
  769. package/agent/plugins/google_meet/meet_bot.py +7 -1
  770. package/agent/plugins/hermes-achievements/dashboard/dist/index.js +9 -15
  771. package/agent/plugins/image_gen/fal/__init__.py +35 -6
  772. package/agent/plugins/image_gen/krea/__init__.py +56 -13
  773. package/agent/plugins/image_gen/openai/__init__.py +122 -24
  774. package/agent/plugins/image_gen/openai-codex/__init__.py +28 -2
  775. package/agent/plugins/image_gen/xai/__init__.py +92 -12
  776. package/agent/plugins/kanban/dashboard/dist/index.js +63 -48
  777. package/agent/plugins/kanban/dashboard/plugin_api.py +39 -35
  778. package/agent/plugins/memory/__init__.py +48 -5
  779. package/agent/plugins/memory/byterover/__init__.py +1 -0
  780. package/agent/plugins/memory/hindsight/README.md +1 -1
  781. package/agent/plugins/memory/hindsight/__init__.py +138 -24
  782. package/agent/plugins/memory/hindsight/plugin.yaml +1 -1
  783. package/agent/plugins/memory/honcho/README.md +13 -10
  784. package/agent/plugins/memory/honcho/cli.py +247 -122
  785. package/agent/plugins/memory/honcho/client.py +112 -102
  786. package/agent/plugins/memory/openviking/README.md +12 -1
  787. package/agent/plugins/memory/openviking/__init__.py +2281 -107
  788. package/agent/plugins/memory/openviking/plugin.yaml +1 -2
  789. package/agent/plugins/memory/supermemory/README.md +22 -10
  790. package/agent/plugins/memory/supermemory/__init__.py +142 -37
  791. package/agent/plugins/memory/supermemory/plugin.yaml +1 -1
  792. package/agent/plugins/model-providers/anthropic/__init__.py +1 -0
  793. package/agent/plugins/model-providers/bedrock/__init__.py +1 -0
  794. package/agent/plugins/model-providers/copilot-acp/__init__.py +1 -0
  795. package/agent/plugins/model-providers/custom/__init__.py +8 -2
  796. package/agent/plugins/model-providers/kimi-coding/__init__.py +16 -7
  797. package/agent/plugins/model-providers/minimax/__init__.py +60 -8
  798. package/agent/plugins/model-providers/opencode-zen/__init__.py +12 -3
  799. package/agent/plugins/model-providers/openrouter/__init__.py +75 -4
  800. package/agent/plugins/model-providers/xiaomi/__init__.py +2 -0
  801. package/agent/plugins/model-providers/zai/__init__.py +1 -0
  802. package/agent/plugins/observability/langfuse/__init__.py +147 -14
  803. package/agent/plugins/observability/nemo_relay/README.md +559 -0
  804. package/agent/plugins/observability/nemo_relay/__init__.py +962 -0
  805. package/agent/plugins/observability/nemo_relay/plugin.yaml +20 -0
  806. package/agent/plugins/platforms/discord/adapter.py +932 -61
  807. package/agent/plugins/platforms/discord/voice_mixer.py +379 -0
  808. package/agent/plugins/platforms/google_chat/adapter.py +9 -3
  809. package/agent/plugins/platforms/google_chat/oauth.py +1 -1
  810. package/agent/plugins/platforms/homeassistant/__init__.py +3 -0
  811. package/agent/{gateway/platforms/homeassistant.py → plugins/platforms/homeassistant/adapter.py} +128 -0
  812. package/agent/plugins/platforms/homeassistant/plugin.yaml +22 -0
  813. package/agent/plugins/platforms/irc/adapter.py +4 -1
  814. package/agent/plugins/platforms/line/adapter.py +16 -1
  815. package/agent/plugins/platforms/mattermost/adapter.py +100 -24
  816. package/agent/plugins/platforms/photon/README.md +179 -0
  817. package/agent/plugins/platforms/photon/__init__.py +4 -0
  818. package/agent/plugins/platforms/photon/adapter.py +1586 -0
  819. package/agent/plugins/platforms/photon/auth.py +1046 -0
  820. package/agent/plugins/platforms/photon/cli.py +439 -0
  821. package/agent/plugins/platforms/photon/plugin.yaml +88 -0
  822. package/agent/plugins/platforms/photon/sidecar/README.md +52 -0
  823. package/agent/plugins/platforms/photon/sidecar/index.mjs +720 -0
  824. package/agent/plugins/platforms/photon/sidecar/package-lock.json +1730 -0
  825. package/agent/plugins/platforms/photon/sidecar/package.json +25 -0
  826. package/agent/plugins/platforms/photon/sidecar/patch-spectrum-mixed-attachments.mjs +155 -0
  827. package/agent/plugins/platforms/raft/__init__.py +3 -0
  828. package/agent/plugins/platforms/raft/adapter.py +774 -0
  829. package/agent/plugins/platforms/raft/plugin.yaml +19 -0
  830. package/agent/plugins/platforms/simplex/adapter.py +777 -220
  831. package/agent/plugins/platforms/simplex/plugin.yaml +21 -2
  832. package/agent/plugins/platforms/teams/adapter.py +175 -5
  833. package/agent/plugins/plugin_utils.py +135 -0
  834. package/agent/plugins/video_gen/fal/__init__.py +10 -3
  835. package/agent/plugins/web/searxng/provider.py +15 -2
  836. package/agent/plugins/web/xai/provider.py +2 -2
  837. package/agent/providers/base.py +22 -3
  838. package/agent/pyproject.toml +115 -21
  839. package/agent/run_agent.py +733 -39
  840. package/agent/scripts/build_skills_index.py +51 -19
  841. package/agent/scripts/check_subprocess_stdin.py +177 -0
  842. package/agent/scripts/contributor_audit.py +2 -0
  843. package/agent/scripts/docker_config_migrate.py +67 -0
  844. package/agent/scripts/install.cmd +3 -3
  845. package/agent/scripts/install.ps1 +580 -154
  846. package/agent/scripts/install.sh +402 -185
  847. package/agent/scripts/lib/node-bootstrap.sh +39 -4
  848. package/agent/scripts/release.py +183 -0
  849. package/agent/scripts/run_tests.sh +1 -0
  850. package/agent/scripts/run_tests_parallel.py +18 -23
  851. package/agent/scripts/whatsapp-bridge/bridge.js +25 -4
  852. package/agent/setup.py +59 -0
  853. package/agent/skills/autonomous-ai-agents/codex/SKILL.md +19 -0
  854. package/agent/skills/autonomous-ai-agents/hermes-agent/SKILL.md +10 -3
  855. package/agent/skills/{mcp/native-mcp/SKILL.md → autonomous-ai-agents/hermes-agent/references/native-mcp.md} +0 -13
  856. package/agent/skills/{devops/webhook-subscriptions/SKILL.md → autonomous-ai-agents/hermes-agent/references/webhooks.md} +1 -11
  857. package/agent/skills/clawpump/SKILL.md +4 -1
  858. package/agent/skills/devops/kanban-orchestrator/SKILL.md +1 -0
  859. package/agent/skills/devops/kanban-worker/SKILL.md +1 -0
  860. package/agent/skills/github/github-auth/SKILL.md +2 -2
  861. package/agent/skills/github/github-auth/scripts/gh-env.sh +2 -2
  862. package/agent/skills/github/github-code-review/SKILL.md +2 -2
  863. package/agent/skills/github/github-issues/SKILL.md +2 -2
  864. package/agent/skills/github/github-pr-workflow/SKILL.md +2 -2
  865. package/agent/skills/github/github-repo-management/SKILL.md +2 -2
  866. package/agent/skills/media/gif-search/SKILL.md +1 -1
  867. package/agent/skills/media/youtube-content/SKILL.md +10 -7
  868. package/agent/skills/media/youtube-content/scripts/fetch_transcript.py +3 -3
  869. package/agent/skills/note-taking/obsidian/SKILL.md +1 -1
  870. package/agent/skills/productivity/airtable/SKILL.md +2 -2
  871. package/agent/skills/productivity/google-workspace/scripts/setup.py +33 -7
  872. package/agent/skills/productivity/notion/SKILL.md +2 -2
  873. package/agent/skills/productivity/teams-meeting-pipeline/SKILL.md +1 -1
  874. package/agent/skills/research/llm-wiki/SKILL.md +1 -1
  875. package/agent/skills/social-media/xurl/SKILL.md +9 -0
  876. package/agent/skills/software-development/hermes-agent-skill-authoring/SKILL.md +1 -1
  877. package/agent/skills/software-development/plan/SKILL.md +285 -5
  878. package/agent/skills/software-development/requesting-code-review/SKILL.md +2 -2
  879. package/agent/skills/software-development/simplify-code/SKILL.md +212 -0
  880. package/agent/skills/software-development/spike/SKILL.md +2 -2
  881. package/agent/skills/software-development/systematic-debugging/SKILL.md +1 -1
  882. package/agent/skills/software-development/test-driven-development/SKILL.md +1 -1
  883. package/agent/tools/approval.py +302 -4
  884. package/agent/tools/async_delegation.py +386 -0
  885. package/agent/tools/blueprints.py +325 -0
  886. package/agent/tools/browser_cdp_tool.py +3 -3
  887. package/agent/tools/browser_tool.py +34 -6
  888. package/agent/tools/checkpoint_manager.py +31 -1
  889. package/agent/tools/clarify_tool.py +55 -5
  890. package/agent/tools/code_execution_tool.py +31 -14
  891. package/agent/tools/computer_use/cua_backend.py +81 -3
  892. package/agent/tools/computer_use/tool.py +79 -5
  893. package/agent/tools/computer_use/vision_routing.py +55 -3
  894. package/agent/tools/credential_files.py +31 -12
  895. package/agent/tools/cronjob_tools.py +30 -20
  896. package/agent/tools/delegate_tool.py +356 -31
  897. package/agent/tools/env_probe.py +1 -0
  898. package/agent/tools/environments/docker.py +163 -8
  899. package/agent/tools/environments/file_sync.py +2 -1
  900. package/agent/tools/environments/local.py +74 -23
  901. package/agent/tools/environments/singularity.py +4 -1
  902. package/agent/tools/environments/ssh.py +78 -11
  903. package/agent/tools/file_operations.py +277 -41
  904. package/agent/tools/file_tools.py +166 -28
  905. package/agent/tools/image_generation_tool.py +515 -29
  906. package/agent/tools/kanban_tools.py +99 -0
  907. package/agent/tools/lazy_deps.py +33 -2
  908. package/agent/tools/mcp_oauth.py +5 -5
  909. package/agent/tools/mcp_oauth_manager.py +7 -5
  910. package/agent/tools/mcp_tool.py +840 -33
  911. package/agent/tools/memory_tool.py +335 -38
  912. package/agent/tools/osv_check.py +15 -1
  913. package/agent/tools/process_registry.py +155 -11
  914. package/agent/tools/read_extract.py +248 -0
  915. package/agent/tools/read_terminal_tool.py +93 -0
  916. package/agent/tools/schema_sanitizer.py +38 -0
  917. package/agent/tools/send_message_tool.py +163 -49
  918. package/agent/tools/session_search_tool.py +189 -7
  919. package/agent/tools/skill_manager_tool.py +202 -3
  920. package/agent/tools/skill_usage.py +52 -4
  921. package/agent/tools/skills_hub.py +184 -44
  922. package/agent/tools/skills_sync.py +232 -5
  923. package/agent/tools/skills_tool.py +125 -11
  924. package/agent/tools/terminal_tool.py +148 -26
  925. package/agent/tools/tirith_security.py +2 -0
  926. package/agent/tools/todo_tool.py +32 -1
  927. package/agent/tools/transcription_tools.py +13 -5
  928. package/agent/tools/tts_tool.py +332 -38
  929. package/agent/tools/url_safety.py +52 -1
  930. package/agent/tools/vision_tools.py +124 -39
  931. package/agent/tools/voice_mode.py +4 -3
  932. package/agent/tools/web_tools.py +45 -15
  933. package/agent/tools/write_approval.py +493 -0
  934. package/agent/toolsets.py +34 -10
  935. package/agent/trajectory_compressor.py +81 -10
  936. package/agent/tui_gateway/entry.py +43 -6
  937. package/agent/tui_gateway/server.py +3335 -330
  938. package/agent/tui_gateway/slash_worker.py +61 -0
  939. package/agent/tui_gateway/ws.py +67 -9
  940. package/agent/ui-tui/eslint.config.mjs +0 -4
  941. package/agent/ui-tui/package.json +6 -6
  942. package/agent/ui-tui/packages/hermes-ink/package.json +1 -1
  943. package/agent/ui-tui/packages/hermes-ink/src/ink/app-mouse.test.ts +34 -1
  944. package/agent/ui-tui/packages/hermes-ink/src/ink/app-rawmode-mouse.test.ts +91 -0
  945. package/agent/ui-tui/packages/hermes-ink/src/ink/components/App.tsx +35 -2
  946. package/agent/ui-tui/packages/hermes-ink/src/ink/events/input-event.ts +4 -11
  947. package/agent/ui-tui/packages/hermes-ink/src/ink/parse-keypress.test.ts +23 -57
  948. package/agent/ui-tui/packages/hermes-ink/src/ink/parse-keypress.ts +11 -135
  949. package/agent/ui-tui/packages/hermes-ink/src/ink/termio/tokenize.test.ts +185 -0
  950. package/agent/ui-tui/packages/hermes-ink/src/ink/termio/tokenize.ts +37 -3
  951. package/agent/ui-tui/packages/hermes-ink/src/utils/execFileNoThrow.ts +5 -5
  952. package/agent/ui-tui/src/__tests__/appChromeStatusRule.test.tsx +217 -0
  953. package/agent/ui-tui/src/__tests__/appChromeStatusRuleDevCredits.test.tsx +73 -0
  954. package/agent/ui-tui/src/__tests__/approvalAction.test.ts +11 -0
  955. package/agent/ui-tui/src/__tests__/billingCommand.test.ts +301 -0
  956. package/agent/ui-tui/src/__tests__/blockLayout.test.ts +122 -0
  957. package/agent/ui-tui/src/__tests__/brandingMcpCount.test.ts +111 -0
  958. package/agent/ui-tui/src/__tests__/completionApply.test.ts +51 -0
  959. package/agent/ui-tui/src/__tests__/createGatewayEventHandler.test.ts +487 -2
  960. package/agent/ui-tui/src/__tests__/createSlashHandler.test.ts +54 -0
  961. package/agent/ui-tui/src/__tests__/creditsCommand.test.ts +144 -0
  962. package/agent/ui-tui/src/__tests__/gatewayClient.test.ts +120 -99
  963. package/agent/ui-tui/src/__tests__/gracefulExit.test.ts +11 -0
  964. package/agent/ui-tui/src/__tests__/memoryMonitor.test.ts +102 -0
  965. package/agent/ui-tui/src/__tests__/paths.test.ts +41 -1
  966. package/agent/ui-tui/src/__tests__/terminalModes.test.ts +22 -0
  967. package/agent/ui-tui/src/__tests__/text.test.ts +23 -0
  968. package/agent/ui-tui/src/__tests__/textInputFastEcho.test.ts +37 -0
  969. package/agent/ui-tui/src/__tests__/turnControllerNotice.test.ts +43 -0
  970. package/agent/ui-tui/src/__tests__/useInputHandlers.test.ts +38 -1
  971. package/agent/ui-tui/src/__tests__/virtualHeights.test.ts +8 -0
  972. package/agent/ui-tui/src/app/createGatewayEventHandler.ts +102 -7
  973. package/agent/ui-tui/src/app/interfaces.ts +64 -1
  974. package/agent/ui-tui/src/app/overlayStore.ts +18 -2
  975. package/agent/ui-tui/src/app/slash/commands/billing.ts +332 -0
  976. package/agent/ui-tui/src/app/slash/commands/core.ts +31 -2
  977. package/agent/ui-tui/src/app/slash/commands/credits.ts +57 -0
  978. package/agent/ui-tui/src/app/slash/commands/ops.ts +28 -0
  979. package/agent/ui-tui/src/app/slash/commands/session.ts +32 -4
  980. package/agent/ui-tui/src/app/slash/registry.ts +4 -0
  981. package/agent/ui-tui/src/app/turnController.ts +145 -2
  982. package/agent/ui-tui/src/app/uiStore.ts +2 -0
  983. package/agent/ui-tui/src/app/useInputHandlers.ts +42 -4
  984. package/agent/ui-tui/src/app/useMainApp.ts +54 -8
  985. package/agent/ui-tui/src/app/useSessionLifecycle.ts +40 -31
  986. package/agent/ui-tui/src/app/useSubmission.ts +23 -31
  987. package/agent/ui-tui/src/components/appChrome.tsx +112 -5
  988. package/agent/ui-tui/src/components/appLayout.tsx +9 -0
  989. package/agent/ui-tui/src/components/appOverlays.tsx +25 -1
  990. package/agent/ui-tui/src/components/billingOverlay.tsx +684 -0
  991. package/agent/ui-tui/src/components/branding.tsx +15 -3
  992. package/agent/ui-tui/src/components/messageLine.tsx +25 -3
  993. package/agent/ui-tui/src/components/pluginsHub.tsx +238 -0
  994. package/agent/ui-tui/src/components/prompts.tsx +31 -17
  995. package/agent/ui-tui/src/components/streamingAssistant.tsx +63 -55
  996. package/agent/ui-tui/src/components/textInput.tsx +16 -0
  997. package/agent/ui-tui/src/config/env.ts +12 -0
  998. package/agent/ui-tui/src/config/limits.ts +13 -0
  999. package/agent/ui-tui/src/domain/blockLayout.ts +146 -0
  1000. package/agent/ui-tui/src/domain/paths.ts +24 -0
  1001. package/agent/ui-tui/src/domain/slash.ts +40 -0
  1002. package/agent/ui-tui/src/entry.tsx +35 -4
  1003. package/agent/ui-tui/src/gatewayClient.ts +22 -10
  1004. package/agent/ui-tui/src/gatewayTypes.ts +130 -1
  1005. package/agent/ui-tui/src/lib/gracefulExit.ts +24 -4
  1006. package/agent/ui-tui/src/lib/memory.test.ts +162 -0
  1007. package/agent/ui-tui/src/lib/memory.ts +60 -1
  1008. package/agent/ui-tui/src/lib/memoryMonitor.ts +79 -4
  1009. package/agent/ui-tui/src/lib/osc52.ts +1 -1
  1010. package/agent/ui-tui/src/lib/text.test.ts +32 -1
  1011. package/agent/ui-tui/src/lib/text.ts +29 -2
  1012. package/agent/ui-tui/src/lib/virtualHeights.ts +13 -0
  1013. package/agent/ui-tui/src/types.ts +5 -0
  1014. package/agent/ui-tui/tsconfig.build.json +0 -1
  1015. package/agent/ui-tui/tsconfig.json +2 -1
  1016. package/agent/utils.py +66 -2
  1017. package/agent/uv.lock +300 -684
  1018. package/agent/web/index.html +2 -2
  1019. package/agent/web/package.json +11 -6
  1020. package/agent/web/public/claw-bg.webp +0 -0
  1021. package/agent/web/public/claw-logo.webp +0 -0
  1022. package/agent/web/src/App.tsx +138 -48
  1023. package/agent/web/src/components/AutomationBlueprints.tsx +225 -0
  1024. package/agent/web/src/components/Backdrop.tsx +15 -0
  1025. package/agent/web/src/components/ChatSessionList.tsx +260 -0
  1026. package/agent/web/src/components/ChatSidebar.tsx +262 -78
  1027. package/agent/web/src/components/ConfirmDialog.tsx +122 -0
  1028. package/agent/web/src/components/ModelPickerDialog.tsx +111 -16
  1029. package/agent/web/src/components/ModelReloadConfirm.tsx +40 -0
  1030. package/agent/web/src/components/ProfileScopeBanner.tsx +30 -0
  1031. package/agent/web/src/components/ProfileSwitcher.tsx +67 -0
  1032. package/agent/web/src/components/ReasoningPicker.tsx +167 -0
  1033. package/agent/web/src/components/SkillEditorDialog.tsx +215 -0
  1034. package/agent/web/src/components/ThemeSwitcher.tsx +119 -4
  1035. package/agent/web/src/components/ToolsetConfigDrawer.tsx +457 -0
  1036. package/agent/web/src/contexts/PageHeaderProvider.tsx +7 -4
  1037. package/agent/web/src/contexts/ProfileProvider.tsx +137 -0
  1038. package/agent/web/src/contexts/SystemActions.tsx +6 -8
  1039. package/agent/web/src/contexts/profile-context.ts +19 -0
  1040. package/agent/web/src/contexts/useProfileScope.ts +6 -0
  1041. package/agent/web/src/i18n/af.ts +5 -4
  1042. package/agent/web/src/i18n/de.ts +5 -4
  1043. package/agent/web/src/i18n/en.ts +58 -4
  1044. package/agent/web/src/i18n/es.ts +5 -3
  1045. package/agent/web/src/i18n/fr.ts +5 -3
  1046. package/agent/web/src/i18n/ga.ts +5 -4
  1047. package/agent/web/src/i18n/hu.ts +5 -4
  1048. package/agent/web/src/i18n/it.ts +5 -4
  1049. package/agent/web/src/i18n/ja.ts +5 -4
  1050. package/agent/web/src/i18n/ko.ts +5 -4
  1051. package/agent/web/src/i18n/pt.ts +5 -3
  1052. package/agent/web/src/i18n/ru.ts +5 -4
  1053. package/agent/web/src/i18n/tr.ts +5 -4
  1054. package/agent/web/src/i18n/types.ts +59 -1
  1055. package/agent/web/src/i18n/uk.ts +5 -3
  1056. package/agent/web/src/i18n/zh-hant.ts +5 -4
  1057. package/agent/web/src/i18n/zh.ts +5 -4
  1058. package/agent/web/src/index.css +2 -2
  1059. package/agent/web/src/lib/api.ts +819 -52
  1060. package/agent/web/src/lib/dashboard-flags.ts +16 -7
  1061. package/agent/web/src/lib/reasoning-effort.test.ts +48 -0
  1062. package/agent/web/src/lib/reasoning-effort.ts +36 -0
  1063. package/agent/web/src/lib/session-refresh.test.ts +21 -0
  1064. package/agent/web/src/lib/session-refresh.ts +26 -0
  1065. package/agent/web/src/pages/ChannelsPage.tsx +529 -68
  1066. package/agent/web/src/pages/ChatPage.tsx +249 -56
  1067. package/agent/web/src/pages/ConfigPage.tsx +11 -1
  1068. package/agent/web/src/pages/CronPage.tsx +219 -31
  1069. package/agent/web/src/pages/EnvPage.tsx +25 -6
  1070. package/agent/web/src/pages/FilesPage.tsx +525 -0
  1071. package/agent/web/src/pages/McpPage.tsx +80 -3
  1072. package/agent/web/src/pages/ModelsPage.tsx +97 -12
  1073. package/agent/web/src/pages/PluginsPage.tsx +1 -1
  1074. package/agent/web/src/pages/ProfileBuilderPage.tsx +611 -0
  1075. package/agent/web/src/pages/ProfilesPage.tsx +1038 -172
  1076. package/agent/web/src/pages/SessionsPage.tsx +144 -13
  1077. package/agent/web/src/pages/SkillsPage.tsx +851 -70
  1078. package/agent/web/src/pages/SystemPage.tsx +340 -4
  1079. package/agent/web/src/pages/WalletPage.tsx +401 -0
  1080. package/agent/web/src/pages/WebhooksPage.tsx +145 -15
  1081. package/agent/web/src/pages/X402Page.tsx +207 -0
  1082. package/agent/web/src/plugins/registry.ts +28 -11
  1083. package/agent/web/src/plugins/sdk.d.ts +160 -0
  1084. package/agent/web/src/themes/context.tsx +112 -5
  1085. package/agent/web/src/themes/fonts.ts +167 -0
  1086. package/agent/web/src/themes/index.ts +7 -0
  1087. package/agent/web/tsconfig.app.json +0 -1
  1088. package/agent/web/vite.config.ts +1 -8
  1089. package/agent/web/vitest.config.ts +16 -0
  1090. package/package.json +1 -1
  1091. package/agent/apps/desktop/package-lock.json +0 -18363
  1092. package/agent/apps/desktop/src/app/chat/composer/skin-slash-popover.tsx +0 -56
  1093. package/agent/apps/desktop/src/components/assistant-ui/thread-virtualizer.tsx +0 -382
  1094. package/agent/apps/desktop/src/components/assistant-ui/todo-tool.tsx +0 -109
  1095. package/agent/apps/desktop/src/components/chat/generated-image-context.tsx +0 -19
  1096. package/agent/optional-skills/productivity/shop-app/SKILL.md +0 -340
  1097. package/agent/skills/autonomous-ai-agents/kanban-codex-lane/SKILL.md +0 -277
  1098. package/agent/skills/autonomous-ai-agents/kanban-codex-lane/templates/pmb-codex-lane-prompt.md +0 -57
  1099. package/agent/skills/diagramming/DESCRIPTION.md +0 -3
  1100. package/agent/skills/domain/DESCRIPTION.md +0 -24
  1101. package/agent/skills/gifs/DESCRIPTION.md +0 -3
  1102. package/agent/skills/inference-sh/DESCRIPTION.md +0 -19
  1103. package/agent/skills/mcp/DESCRIPTION.md +0 -3
  1104. package/agent/skills/media/spotify/SKILL.md +0 -135
  1105. package/agent/skills/mlops/training/DESCRIPTION.md +0 -3
  1106. package/agent/skills/mlops/vector-databases/DESCRIPTION.md +0 -3
  1107. package/agent/skills/productivity/linear/SKILL.md +0 -380
  1108. package/agent/skills/productivity/linear/scripts/linear_api.py +0 -445
  1109. package/agent/skills/software-development/debugging-hermes-tui-commands/SKILL.md +0 -152
  1110. package/agent/skills/software-development/writing-plans/SKILL.md +0 -297
  1111. package/agent/ui-tui/package-lock.json +0 -7449
  1112. package/agent/ui-tui/packages/hermes-ink/package-lock.json +0 -1289
  1113. package/agent/web/package-lock.json +0 -8887
  1114. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/PORT_NOTES.md +0 -0
  1115. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/SKILL.md +0 -0
  1116. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/prompts/system.md +0 -0
  1117. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/palettes/macaron.md +0 -0
  1118. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/palettes/mono-ink.md +0 -0
  1119. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/palettes/neon.md +0 -0
  1120. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/palettes/warm.md +0 -0
  1121. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/prompt-construction.md +0 -0
  1122. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/style-presets.md +0 -0
  1123. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/blueprint.md +0 -0
  1124. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/chalkboard.md +0 -0
  1125. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/editorial.md +0 -0
  1126. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/elegant.md +0 -0
  1127. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/fantasy-animation.md +0 -0
  1128. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/flat-doodle.md +0 -0
  1129. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/flat.md +0 -0
  1130. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/ink-notes.md +0 -0
  1131. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/intuition-machine.md +0 -0
  1132. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/minimal.md +0 -0
  1133. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/nature.md +0 -0
  1134. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/notion.md +0 -0
  1135. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/pixel-art.md +0 -0
  1136. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/playful.md +0 -0
  1137. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/retro.md +0 -0
  1138. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/scientific.md +0 -0
  1139. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/screen-print.md +0 -0
  1140. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/sketch-notes.md +0 -0
  1141. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/sketch.md +0 -0
  1142. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/vector-illustration.md +0 -0
  1143. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/vintage.md +0 -0
  1144. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/warm.md +0 -0
  1145. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/watercolor.md +0 -0
  1146. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles.md +0 -0
  1147. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/usage.md +0 -0
  1148. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/workflow.md +0 -0
  1149. /package/agent/{skills → optional-skills}/creative/baoyu-comic/PORT_NOTES.md +0 -0
  1150. /package/agent/{skills → optional-skills}/creative/baoyu-comic/SKILL.md +0 -0
  1151. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/analysis-framework.md +0 -0
  1152. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/art-styles/chalk.md +0 -0
  1153. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/art-styles/ink-brush.md +0 -0
  1154. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/art-styles/ligne-claire.md +0 -0
  1155. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/art-styles/manga.md +0 -0
  1156. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/art-styles/minimalist.md +0 -0
  1157. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/art-styles/realistic.md +0 -0
  1158. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/auto-selection.md +0 -0
  1159. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/base-prompt.md +0 -0
  1160. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/character-template.md +0 -0
  1161. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/layouts/cinematic.md +0 -0
  1162. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/layouts/dense.md +0 -0
  1163. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/layouts/four-panel.md +0 -0
  1164. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/layouts/mixed.md +0 -0
  1165. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/layouts/splash.md +0 -0
  1166. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/layouts/standard.md +0 -0
  1167. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/layouts/webtoon.md +0 -0
  1168. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/ohmsha-guide.md +0 -0
  1169. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/partial-workflows.md +0 -0
  1170. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/presets/concept-story.md +0 -0
  1171. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/presets/four-panel.md +0 -0
  1172. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/presets/ohmsha.md +0 -0
  1173. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/presets/shoujo.md +0 -0
  1174. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/presets/wuxia.md +0 -0
  1175. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/storyboard-template.md +0 -0
  1176. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/tones/action.md +0 -0
  1177. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/tones/dramatic.md +0 -0
  1178. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/tones/energetic.md +0 -0
  1179. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/tones/neutral.md +0 -0
  1180. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/tones/romantic.md +0 -0
  1181. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/tones/vintage.md +0 -0
  1182. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/tones/warm.md +0 -0
  1183. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/workflow.md +0 -0
  1184. /package/agent/{skills → optional-skills}/creative/creative-ideation/SKILL.md +0 -0
  1185. /package/agent/{skills → optional-skills}/creative/creative-ideation/references/full-prompt-library.md +0 -0
  1186. /package/agent/{skills → optional-skills}/creative/pixel-art/ATTRIBUTION.md +0 -0
  1187. /package/agent/{skills → optional-skills}/creative/pixel-art/SKILL.md +0 -0
  1188. /package/agent/{skills → optional-skills}/creative/pixel-art/references/palettes.md +0 -0
  1189. /package/agent/{skills → optional-skills}/creative/pixel-art/scripts/__init__.py +0 -0
  1190. /package/agent/{skills → optional-skills}/creative/pixel-art/scripts/palettes.py +0 -0
  1191. /package/agent/{skills → optional-skills}/creative/pixel-art/scripts/pixel_art.py +0 -0
  1192. /package/agent/{skills → optional-skills}/creative/pixel-art/scripts/pixel_art_video.py +0 -0
  1193. /package/agent/{skills/mlops/inference → optional-skills/mlops}/obliteratus/SKILL.md +0 -0
  1194. /package/agent/{skills/mlops/inference → optional-skills/mlops}/obliteratus/references/analysis-modules.md +0 -0
  1195. /package/agent/{skills/mlops/inference → optional-skills/mlops}/obliteratus/references/methods-guide.md +0 -0
  1196. /package/agent/{skills/mlops/inference → optional-skills/mlops}/obliteratus/templates/abliteration-config.yaml +0 -0
  1197. /package/agent/{skills/mlops/inference → optional-skills/mlops}/obliteratus/templates/analysis-study.yaml +0 -0
  1198. /package/agent/{skills/mlops/inference → optional-skills/mlops}/obliteratus/templates/batch-abliteration.yaml +0 -0
  1199. /package/agent/{skills → optional-skills}/mlops/research/DESCRIPTION.md +0 -0
  1200. /package/agent/{skills → optional-skills}/mlops/research/dspy/SKILL.md +0 -0
  1201. /package/agent/{skills → optional-skills}/mlops/research/dspy/references/examples.md +0 -0
  1202. /package/agent/{skills → optional-skills}/mlops/research/dspy/references/modules.md +0 -0
  1203. /package/agent/{skills → optional-skills}/mlops/research/dspy/references/optimizers.md +0 -0
  1204. /package/agent/{skills/red-teaming → optional-skills/security}/godmode/references/jailbreak-templates.md +0 -0
  1205. /package/agent/{skills/red-teaming → optional-skills/security}/godmode/references/refusal-detection.md +0 -0
  1206. /package/agent/{skills/red-teaming → optional-skills/security}/godmode/scripts/godmode_race.py +0 -0
  1207. /package/agent/{skills/red-teaming → optional-skills/security}/godmode/scripts/load_godmode.py +0 -0
  1208. /package/agent/{skills/red-teaming → optional-skills/security}/godmode/scripts/parseltongue.py +0 -0
  1209. /package/agent/{skills/red-teaming → optional-skills/security}/godmode/templates/prefill-subtle.json +0 -0
  1210. /package/agent/{skills/red-teaming → optional-skills/security}/godmode/templates/prefill.json +0 -0
  1211. /package/agent/{skills → optional-skills}/software-development/subagent-driven-development/references/context-budget-discipline.md +0 -0
  1212. /package/agent/{skills → optional-skills}/software-development/subagent-driven-development/references/gates-taxonomy.md +0 -0
@@ -0,0 +1,2759 @@
1
+ """Per-provider model-selection wizard flows for ``hermes setup`` / ``hermes model``.
2
+
3
+ Extracted from ``hermes_cli/main.py`` as part of the god-file decomposition
4
+ campaign (``~/.hermes/plans/god-file-decomposition.md``, Phase 2 — splitting
5
+ main.py handler/flow bodies out of the module). These 18 ``_model_flow_*``
6
+ functions are the interactive provider-setup branches dispatched by
7
+ ``select_provider_and_model`` (which stays in main.py).
8
+
9
+ Behavior-neutral: each function is lifted verbatim. ``select_provider_and_model``
10
+ in main.py re-imports them (``from hermes_cli.model_setup_flows import *``-style
11
+ explicit import) so existing call sites — and test monkeypatches that target
12
+ ``hermes_cli.main._model_flow_*`` — keep resolving against main.py's namespace.
13
+
14
+ main.py-internal helpers the flows call (``_prompt_api_key``, ``_save_custom_provider``,
15
+ the reasoning-effort/stepfun/qwen helpers, ``_run_anthropic_oauth_flow``, …) are
16
+ imported lazily inside the flows (``from hermes_cli.main import ...`` resolves at
17
+ call time, when main.py is fully loaded) so this module never imports
18
+ ``hermes_cli.main`` at import time -> no import cycle.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import argparse
24
+ import os
25
+ import subprocess
26
+
27
+
28
+ def _prompt_auth_credentials_choice(title: str) -> str:
29
+ """Prompt for reuse / reauthenticate / cancel with the standard radio UI.
30
+
31
+ Returns one of ``"use"``, ``"reauth"``, ``"cancel"``. Falls back to a
32
+ numbered prompt when curses is unavailable (piped stdin, non-TTY).
33
+ """
34
+ choices = [
35
+ "Use existing credentials",
36
+ "Reauthenticate (new OAuth login)",
37
+ "Cancel",
38
+ ]
39
+ try:
40
+ from hermes_cli.setup import _curses_prompt_choice
41
+
42
+ idx = _curses_prompt_choice(title, choices, 0)
43
+ if idx >= 0:
44
+ print()
45
+ return ("use", "reauth", "cancel")[idx]
46
+ except Exception:
47
+ pass
48
+
49
+ print(title)
50
+ for i, label in enumerate(choices, 1):
51
+ marker = "→" if i == 1 else " "
52
+ print(f" {marker} {i}. {label}")
53
+ print()
54
+ try:
55
+ choice = input(" Choice [1/2/3]: ").strip()
56
+ except (KeyboardInterrupt, EOFError):
57
+ choice = "1"
58
+
59
+ if choice == "2":
60
+ return "reauth"
61
+ if choice == "3":
62
+ return "cancel"
63
+ return "use"
64
+
65
+
66
+ def _model_flow_openrouter(config, current_model=""):
67
+ """OpenRouter provider: ensure API key, then pick model."""
68
+ from hermes_cli.main import _prompt_api_key
69
+ from hermes_constants import OPENROUTER_BASE_URL
70
+ from hermes_cli.auth import (
71
+ ProviderConfig,
72
+ _prompt_model_selection,
73
+ _save_model_choice,
74
+ deactivate_provider,
75
+ )
76
+ from hermes_cli.config import get_env_value
77
+
78
+ # Route through _prompt_api_key so users can replace a stale/broken key
79
+ # in-flow (K/R/C) instead of having to edit ~/.hermes/.env by hand. The
80
+ # previous bypass-when-key-exists branch left no way to recover from a
81
+ # bad paste short of re-running `hermes setup` from scratch. OpenRouter
82
+ # isn't in PROVIDER_REGISTRY so we synthesize a minimal pconfig.
83
+ pconfig = ProviderConfig(
84
+ id="openrouter",
85
+ name="OpenRouter",
86
+ auth_type="api_key",
87
+ api_key_env_vars=("OPENROUTER_API_KEY",),
88
+ )
89
+ existing_key = get_env_value("OPENROUTER_API_KEY") or ""
90
+ if not existing_key:
91
+ print("Get one at: https://openrouter.ai/keys")
92
+ print()
93
+ _resolved, abort = _prompt_api_key(pconfig, existing_key, provider_id="openrouter")
94
+ if abort:
95
+ return
96
+
97
+ from hermes_cli.models import model_ids, get_pricing_for_provider
98
+
99
+ openrouter_models = model_ids(force_refresh=True)
100
+
101
+ # Fetch live pricing (non-blocking — returns empty dict on failure)
102
+ pricing = get_pricing_for_provider("openrouter", force_refresh=True)
103
+
104
+ selected = _prompt_model_selection(
105
+ openrouter_models,
106
+ current_model=current_model,
107
+ pricing=pricing,
108
+ confirm_provider="openrouter",
109
+ confirm_base_url=OPENROUTER_BASE_URL,
110
+ confirm_api_key=_resolved or existing_key,
111
+ )
112
+ if selected:
113
+ _save_model_choice(selected)
114
+
115
+ # Update config provider and deactivate any OAuth provider
116
+ from hermes_cli.config import load_config, save_config
117
+
118
+ cfg = load_config()
119
+ model = cfg.get("model")
120
+ if not isinstance(model, dict):
121
+ model = {"default": model} if model else {}
122
+ cfg["model"] = model
123
+ model["provider"] = "openrouter"
124
+ model["base_url"] = OPENROUTER_BASE_URL
125
+ model["api_mode"] = "chat_completions"
126
+ save_config(cfg)
127
+ deactivate_provider()
128
+ print(f"Default model set to: {selected} (via OpenRouter)")
129
+ else:
130
+ print("No change.")
131
+
132
+ def _model_flow_nous(config, current_model="", args=None):
133
+ """Nous Portal provider: ensure logged in, then pick model."""
134
+ from hermes_cli.auth import (
135
+ get_provider_auth_state,
136
+ _prompt_model_selection,
137
+ _save_model_choice,
138
+ _update_config_for_provider,
139
+ resolve_nous_runtime_credentials,
140
+ AuthError,
141
+ format_auth_error,
142
+ _login_nous,
143
+ PROVIDER_REGISTRY,
144
+ )
145
+ from hermes_cli.config import (
146
+ get_env_value,
147
+ load_config,
148
+ save_config,
149
+ save_env_value,
150
+ )
151
+ from hermes_cli.nous_subscription import prompt_enable_tool_gateway
152
+
153
+ state = get_provider_auth_state("nous")
154
+ if not state or not state.get("access_token"):
155
+ print("Not logged into Nous Portal. Starting login...")
156
+ print()
157
+ try:
158
+ mock_args = argparse.Namespace(
159
+ portal_url=getattr(args, "portal_url", None),
160
+ inference_url=getattr(args, "inference_url", None),
161
+ client_id=getattr(args, "client_id", None),
162
+ scope=getattr(args, "scope", None),
163
+ no_browser=bool(getattr(args, "no_browser", False)),
164
+ timeout=getattr(args, "timeout", None) or 15.0,
165
+ ca_bundle=getattr(args, "ca_bundle", None),
166
+ insecure=bool(getattr(args, "insecure", False)),
167
+ )
168
+ _login_nous(mock_args, PROVIDER_REGISTRY["nous"])
169
+ # Offer Tool Gateway enablement for paid subscribers
170
+ try:
171
+ _refreshed = load_config() or {}
172
+ prompt_enable_tool_gateway(_refreshed)
173
+ except Exception:
174
+ pass
175
+ except SystemExit:
176
+ print("Login cancelled or failed.")
177
+ return
178
+ except Exception as exc:
179
+ print(f"Login failed: {exc}")
180
+ return
181
+ # login_nous already handles model selection + config update
182
+ return
183
+
184
+ # Already logged in — use curated model list (same as OpenRouter defaults).
185
+ # The live /models endpoint returns hundreds of models; the curated list
186
+ # shows only agentic models users recognize from OpenRouter.
187
+ from hermes_cli.models import (
188
+ get_curated_nous_model_ids,
189
+ get_pricing_for_provider,
190
+ check_nous_free_tier,
191
+ partition_nous_models_by_tier,
192
+ union_with_portal_free_recommendations,
193
+ union_with_portal_paid_recommendations,
194
+ )
195
+
196
+ model_ids = get_curated_nous_model_ids()
197
+ if not model_ids:
198
+ print("No curated models available for Nous Portal.")
199
+ return
200
+
201
+ # Verify credentials are still valid (catches expired sessions early)
202
+ try:
203
+ creds = resolve_nous_runtime_credentials()
204
+ except Exception as exc:
205
+ relogin = isinstance(exc, AuthError) and exc.relogin_required
206
+ msg = format_auth_error(exc) if isinstance(exc, AuthError) else str(exc)
207
+ if relogin:
208
+ print(f"Session expired: {msg}")
209
+ print("Re-authenticating with Nous Portal...\n")
210
+ try:
211
+ mock_args = argparse.Namespace(
212
+ portal_url=None,
213
+ inference_url=None,
214
+ client_id=None,
215
+ scope=None,
216
+ no_browser=False,
217
+ timeout=15.0,
218
+ ca_bundle=None,
219
+ insecure=False,
220
+ )
221
+ _login_nous(mock_args, PROVIDER_REGISTRY["nous"])
222
+ except Exception as login_exc:
223
+ print(f"Re-login failed: {login_exc}")
224
+ return
225
+ print(f"Could not verify credentials: {msg}")
226
+ return
227
+
228
+ # Fetch live pricing (non-blocking — returns empty dict on failure)
229
+ pricing = get_pricing_for_provider("nous")
230
+
231
+ # Force fresh account data for model selection so recent credit purchases
232
+ # are reflected immediately.
233
+ free_tier = check_nous_free_tier(force_fresh=True)
234
+ if not free_tier:
235
+ try:
236
+ refreshed_creds = resolve_nous_runtime_credentials(
237
+ force_refresh=True,
238
+ )
239
+ if refreshed_creds:
240
+ creds = refreshed_creds
241
+ except Exception:
242
+ # Runtime inference has its own paid-entitlement recovery path; do
243
+ # not block model selection if this opportunistic refresh fails.
244
+ pass
245
+
246
+ # Resolve portal URL early — needed both for upgrade links and for the
247
+ # freeRecommendedModels endpoint below.
248
+ _nous_portal_url = ""
249
+ try:
250
+ _nous_state = get_provider_auth_state("nous")
251
+ if _nous_state:
252
+ _nous_portal_url = _nous_state.get("portal_base_url", "")
253
+ except Exception:
254
+ pass
255
+
256
+ # For free users: partition models into selectable/unavailable based on
257
+ # whether they are free per the Portal-reported pricing. First augment
258
+ # with the Portal's freeRecommendedModels list so newly-launched free
259
+ # models show up even if this CLI build's hardcoded curated list and
260
+ # docs-hosted manifest haven't caught up yet.
261
+ #
262
+ # For paid users: mirror the same idea with paidRecommendedModels so
263
+ # newly-launched paid models surface in the picker too — independent
264
+ # of CLI release cadence.
265
+ unavailable_models: list[str] = []
266
+ unavailable_message = ""
267
+ if free_tier:
268
+ try:
269
+ from hermes_cli.nous_account import (
270
+ format_nous_portal_entitlement_message,
271
+ get_nous_portal_account_info,
272
+ )
273
+
274
+ _account_info = get_nous_portal_account_info(force_fresh=True)
275
+ unavailable_message = (
276
+ format_nous_portal_entitlement_message(
277
+ _account_info,
278
+ capability="paid Nous models",
279
+ )
280
+ or ""
281
+ )
282
+ except Exception:
283
+ unavailable_message = ""
284
+ model_ids, pricing = union_with_portal_free_recommendations(
285
+ model_ids, pricing, _nous_portal_url,
286
+ )
287
+ model_ids, unavailable_models = partition_nous_models_by_tier(
288
+ model_ids, pricing, free_tier=True
289
+ )
290
+ else:
291
+ model_ids, pricing = union_with_portal_paid_recommendations(
292
+ model_ids, pricing, _nous_portal_url,
293
+ )
294
+
295
+ if not model_ids and not unavailable_models:
296
+ print("No models available for Nous Portal after filtering.")
297
+ return
298
+
299
+ if free_tier and not model_ids:
300
+ print("No free models currently available.")
301
+ if unavailable_models:
302
+ from hermes_cli.auth import DEFAULT_NOUS_PORTAL_URL
303
+
304
+ _url = (_nous_portal_url or DEFAULT_NOUS_PORTAL_URL).rstrip("/")
305
+ print(unavailable_message or f"Upgrade at {_url} to access paid models.")
306
+ return
307
+
308
+ print(
309
+ f'Showing {len(model_ids)} curated models — use "Enter custom model name" for others.'
310
+ )
311
+
312
+ selected = _prompt_model_selection(
313
+ model_ids,
314
+ current_model=current_model,
315
+ pricing=pricing,
316
+ unavailable_models=unavailable_models,
317
+ portal_url=_nous_portal_url,
318
+ unavailable_message=unavailable_message,
319
+ confirm_provider="nous",
320
+ confirm_base_url=creds.get("base_url", ""),
321
+ confirm_api_key=creds.get("api_key", ""),
322
+ )
323
+ if selected:
324
+ _save_model_choice(selected)
325
+ # Reactivate Nous as the provider and update config
326
+ inference_url = creds.get("base_url", "")
327
+ _update_config_for_provider("nous", inference_url)
328
+ current_model_cfg = config.get("model")
329
+ if isinstance(current_model_cfg, dict):
330
+ model_cfg = dict(current_model_cfg)
331
+ elif isinstance(current_model_cfg, str) and current_model_cfg.strip():
332
+ model_cfg = {"default": current_model_cfg.strip()}
333
+ else:
334
+ model_cfg = {}
335
+ model_cfg["provider"] = "nous"
336
+ model_cfg["default"] = selected
337
+ if inference_url and inference_url.strip():
338
+ model_cfg["base_url"] = inference_url.rstrip("/")
339
+ else:
340
+ model_cfg.pop("base_url", None)
341
+ config["model"] = model_cfg
342
+ # Clear any custom endpoint that might conflict
343
+ if get_env_value("OPENAI_BASE_URL"):
344
+ save_env_value("OPENAI_BASE_URL", "")
345
+ save_env_value("OPENAI_API_KEY", "")
346
+ save_config(config)
347
+ print(f"Default model set to: {selected} (via Nous Portal)")
348
+ # Offer Tool Gateway enablement for paid subscribers
349
+ prompt_enable_tool_gateway(config)
350
+ else:
351
+ print("No change.")
352
+
353
+ def _model_flow_openai_codex(config, current_model=""):
354
+ """OpenAI Codex provider: ensure logged in, then pick model."""
355
+ from hermes_cli.auth import (
356
+ get_codex_auth_status,
357
+ _prompt_model_selection,
358
+ _save_model_choice,
359
+ _update_config_for_provider,
360
+ _login_openai_codex,
361
+ PROVIDER_REGISTRY,
362
+ DEFAULT_CODEX_BASE_URL,
363
+ )
364
+ from hermes_cli.codex_models import get_codex_model_ids
365
+
366
+ status = get_codex_auth_status()
367
+ if status.get("logged_in"):
368
+ print(" OpenAI Codex credentials: ✓")
369
+ print()
370
+ choice = _prompt_auth_credentials_choice("OpenAI Codex credentials:")
371
+
372
+ if choice == "reauth":
373
+ print("Starting a fresh OpenAI Codex login...")
374
+ print()
375
+ try:
376
+ mock_args = argparse.Namespace()
377
+ _login_openai_codex(
378
+ mock_args,
379
+ PROVIDER_REGISTRY["openai-codex"],
380
+ force_new_login=True,
381
+ )
382
+ except SystemExit:
383
+ print("Login cancelled or failed.")
384
+ return
385
+ except Exception as exc:
386
+ print(f"Login failed: {exc}")
387
+ return
388
+ status = get_codex_auth_status()
389
+ if not status.get("logged_in"):
390
+ print("Login failed.")
391
+ return
392
+ elif choice == "cancel":
393
+ return
394
+ else:
395
+ print("Not logged into OpenAI Codex. Starting login...")
396
+ print()
397
+ try:
398
+ mock_args = argparse.Namespace()
399
+ _login_openai_codex(mock_args, PROVIDER_REGISTRY["openai-codex"])
400
+ except SystemExit:
401
+ print("Login cancelled or failed.")
402
+ return
403
+ except Exception as exc:
404
+ print(f"Login failed: {exc}")
405
+ return
406
+
407
+ _codex_token = None
408
+ # Prefer credential pool (where `hermes auth` stores device_code tokens),
409
+ # fall back to legacy provider state.
410
+ try:
411
+ _codex_status = get_codex_auth_status()
412
+ if _codex_status.get("logged_in"):
413
+ _codex_token = _codex_status.get("api_key")
414
+ except Exception:
415
+ pass
416
+ if not _codex_token:
417
+ try:
418
+ from hermes_cli.auth import resolve_codex_runtime_credentials
419
+
420
+ _codex_creds = resolve_codex_runtime_credentials()
421
+ _codex_token = _codex_creds.get("api_key")
422
+ except Exception:
423
+ pass
424
+
425
+ codex_models = get_codex_model_ids(access_token=_codex_token)
426
+
427
+ selected = _prompt_model_selection(
428
+ codex_models,
429
+ current_model=current_model,
430
+ confirm_provider="openai-codex",
431
+ confirm_base_url=DEFAULT_CODEX_BASE_URL,
432
+ confirm_api_key=_codex_token or "",
433
+ )
434
+ if selected:
435
+ _save_model_choice(selected)
436
+ _update_config_for_provider("openai-codex", DEFAULT_CODEX_BASE_URL)
437
+ print(f"Default model set to: {selected} (via OpenAI Codex)")
438
+ else:
439
+ print("No change.")
440
+
441
+ def _model_flow_xai_oauth(_config, current_model="", *, args=None):
442
+ """xAI Grok OAuth (SuperGrok / Premium+) provider: ensure logged in, then pick model."""
443
+ from hermes_cli.auth import (
444
+ get_xai_oauth_auth_status,
445
+ _prompt_model_selection,
446
+ _save_model_choice,
447
+ _update_config_for_provider,
448
+ resolve_xai_oauth_runtime_credentials,
449
+ _login_xai_oauth,
450
+ DEFAULT_XAI_OAUTH_BASE_URL,
451
+ PROVIDER_REGISTRY,
452
+ )
453
+ from hermes_cli.models import _PROVIDER_MODELS
454
+
455
+ status = get_xai_oauth_auth_status()
456
+ if status.get("logged_in"):
457
+ print(" xAI Grok OAuth (SuperGrok / Premium+) credentials: ✓")
458
+ print()
459
+ choice = _prompt_auth_credentials_choice(
460
+ "xAI Grok OAuth (SuperGrok / Premium+) credentials:"
461
+ )
462
+
463
+ if choice == "reauth":
464
+ print("Starting a fresh xAI OAuth login...")
465
+ print()
466
+ try:
467
+ # Forward CLI flags from ``hermes model --manual-paste``
468
+ # / ``--no-browser`` / ``--timeout`` into the loopback
469
+ # login. Without this, browser-only remotes (#26923)
470
+ # can't reach the manual-paste path via ``hermes model``.
471
+ mock_args = argparse.Namespace(
472
+ manual_paste=bool(getattr(args, "manual_paste", False)),
473
+ no_browser=bool(getattr(args, "no_browser", False)),
474
+ timeout=getattr(args, "timeout", None),
475
+ )
476
+ _login_xai_oauth(
477
+ mock_args,
478
+ PROVIDER_REGISTRY["xai-oauth"],
479
+ force_new_login=True,
480
+ )
481
+ except SystemExit:
482
+ print("Login cancelled or failed.")
483
+ return
484
+ except Exception as exc:
485
+ print(f"Login failed: {exc}")
486
+ return
487
+ elif choice == "cancel":
488
+ return
489
+ else:
490
+ print("Not logged into xAI Grok OAuth (SuperGrok / Premium+). Starting login...")
491
+ print()
492
+ try:
493
+ mock_args = argparse.Namespace(
494
+ manual_paste=bool(getattr(args, "manual_paste", False)),
495
+ no_browser=bool(getattr(args, "no_browser", False)),
496
+ timeout=getattr(args, "timeout", None),
497
+ )
498
+ _login_xai_oauth(mock_args, PROVIDER_REGISTRY["xai-oauth"])
499
+ except SystemExit:
500
+ print("Login cancelled or failed.")
501
+ return
502
+ except Exception as exc:
503
+ print(f"Login failed: {exc}")
504
+ return
505
+
506
+ # Resolve a usable base URL. ``resolve_xai_oauth_runtime_credentials``
507
+ # only reads from the auth.json singleton — but credentials may legitimately
508
+ # live only in the pool (e.g. after ``hermes auth add xai-oauth``). Fall
509
+ # back to the default base URL in that case so the model picker still
510
+ # completes successfully instead of bailing out with
511
+ # ``Could not resolve xAI OAuth credentials``.
512
+ base_url = DEFAULT_XAI_OAUTH_BASE_URL
513
+ try:
514
+ creds = resolve_xai_oauth_runtime_credentials()
515
+ base_url = (creds.get("base_url") or "").strip().rstrip("/") or base_url
516
+ except Exception:
517
+ pass
518
+
519
+ models = list(_PROVIDER_MODELS.get("xai-oauth") or _PROVIDER_MODELS.get("xai") or [])
520
+ selected = _prompt_model_selection(models, current_model=current_model or (models[0] if models else "grok-build-0.1"))
521
+ if selected:
522
+ _save_model_choice(selected)
523
+ _update_config_for_provider("xai-oauth", base_url)
524
+ print(f"Default model set to: {selected} (via xAI Grok OAuth — SuperGrok / Premium+)")
525
+ else:
526
+ print("No change.")
527
+
528
+ def _model_flow_qwen_oauth(_config, current_model=""):
529
+ """Qwen OAuth provider: reuse local Qwen CLI login, then pick model."""
530
+ from hermes_cli.main import _DEFAULT_QWEN_PORTAL_MODELS
531
+ from hermes_cli.auth import (
532
+ get_qwen_auth_status,
533
+ resolve_qwen_runtime_credentials,
534
+ _prompt_model_selection,
535
+ _save_model_choice,
536
+ _update_config_for_provider,
537
+ DEFAULT_QWEN_BASE_URL,
538
+ )
539
+ from hermes_cli.models import fetch_api_models
540
+
541
+ status = get_qwen_auth_status()
542
+ if not status.get("logged_in"):
543
+ print("Not logged into Qwen CLI OAuth.")
544
+ print("Run: qwen auth qwen-oauth")
545
+ auth_file = status.get("auth_file")
546
+ if auth_file:
547
+ print(f"Expected credentials file: {auth_file}")
548
+ if status.get("error"):
549
+ print(f"Error: {status.get('error')}")
550
+ return
551
+
552
+ # Try live model discovery, fall back to curated list.
553
+ models = None
554
+ try:
555
+ creds = resolve_qwen_runtime_credentials(refresh_if_expiring=True)
556
+ models = fetch_api_models(creds["api_key"], creds["base_url"])
557
+ except Exception:
558
+ pass
559
+ if not models:
560
+ models = list(_DEFAULT_QWEN_PORTAL_MODELS)
561
+
562
+ default = current_model or (models[0] if models else "qwen3-coder-plus")
563
+ selected = _prompt_model_selection(
564
+ models,
565
+ current_model=default,
566
+ confirm_provider="qwen-oauth",
567
+ confirm_base_url=DEFAULT_QWEN_BASE_URL,
568
+ )
569
+ if selected:
570
+ _save_model_choice(selected)
571
+ _update_config_for_provider("qwen-oauth", DEFAULT_QWEN_BASE_URL)
572
+ print(f"Default model set to: {selected} (via Qwen OAuth)")
573
+ else:
574
+ print("No change.")
575
+
576
+ def _model_flow_minimax_oauth(config, current_model="", args=None):
577
+ """MiniMax OAuth provider: ensure logged in, then pick model."""
578
+ from hermes_cli.auth import (
579
+ get_provider_auth_state,
580
+ _prompt_model_selection,
581
+ _save_model_choice,
582
+ _update_config_for_provider,
583
+ resolve_minimax_oauth_runtime_credentials,
584
+ AuthError,
585
+ format_auth_error,
586
+ _login_minimax_oauth,
587
+ PROVIDER_REGISTRY,
588
+ )
589
+
590
+ state = get_provider_auth_state("minimax-oauth")
591
+ if not state or not state.get("access_token"):
592
+ print("Not logged into MiniMax. Starting OAuth login...")
593
+ print()
594
+ try:
595
+ mock_args = argparse.Namespace(
596
+ region=getattr(args, "region", None) or "global",
597
+ no_browser=bool(getattr(args, "no_browser", False)),
598
+ timeout=getattr(args, "timeout", None) or 15.0,
599
+ )
600
+ _login_minimax_oauth(mock_args, PROVIDER_REGISTRY["minimax-oauth"])
601
+ except SystemExit:
602
+ print("Login cancelled or failed.")
603
+ return
604
+ except Exception as exc:
605
+ print(f"Login failed: {exc}")
606
+ return
607
+
608
+ try:
609
+ creds = resolve_minimax_oauth_runtime_credentials()
610
+ except AuthError as exc:
611
+ print(format_auth_error(exc))
612
+ return
613
+
614
+ from hermes_cli.models import _PROVIDER_MODELS
615
+
616
+ model_ids = _PROVIDER_MODELS.get("minimax-oauth", [])
617
+ selected = _prompt_model_selection(
618
+ model_ids,
619
+ current_model,
620
+ confirm_provider="minimax-oauth",
621
+ confirm_base_url=creds["base_url"],
622
+ )
623
+ if not selected:
624
+ return
625
+ _save_model_choice(selected)
626
+ _update_config_for_provider("minimax-oauth", creds["base_url"])
627
+ print(f"\u2713 Using MiniMax model: {selected}")
628
+
629
+ def _model_flow_google_gemini_cli(_config, current_model=""):
630
+ """Google Gemini OAuth (PKCE) via Cloud Code Assist — supports free AND paid tiers.
631
+
632
+ Flow:
633
+ 1. Show upfront warning about Google's ToS stance (per opencode-gemini-auth).
634
+ 2. If creds missing, run PKCE browser OAuth via agent.google_oauth.
635
+ 3. Resolve project context (env -> config -> auto-discover -> free tier).
636
+ 4. Prompt user to pick a model.
637
+ 5. Save to ~/.hermes/config.yaml.
638
+ """
639
+ from hermes_cli.auth import (
640
+ DEFAULT_GEMINI_CLOUDCODE_BASE_URL,
641
+ get_gemini_oauth_auth_status,
642
+ resolve_gemini_oauth_runtime_credentials,
643
+ _prompt_model_selection,
644
+ _save_model_choice,
645
+ _update_config_for_provider,
646
+ )
647
+ from hermes_cli.models import _PROVIDER_MODELS
648
+
649
+ print()
650
+ print("⚠ Google considers using the Gemini CLI OAuth client with third-party")
651
+ print(" software a policy violation. Some users have reported account")
652
+ print(" restrictions. You can use your own API key via 'gemini' provider")
653
+ print(" for the lowest-risk experience.")
654
+ print()
655
+ try:
656
+ proceed = input("Continue with OAuth login? [y/N]: ").strip().lower()
657
+ except (EOFError, KeyboardInterrupt):
658
+ print("Cancelled.")
659
+ return
660
+ if proceed not in {"y", "yes"}:
661
+ print("Cancelled.")
662
+ return
663
+
664
+ status = get_gemini_oauth_auth_status()
665
+ if not status.get("logged_in"):
666
+ try:
667
+ from agent.google_oauth import resolve_project_id_from_env, start_oauth_flow
668
+
669
+ env_project = resolve_project_id_from_env()
670
+ start_oauth_flow(force_relogin=True, project_id=env_project)
671
+ except Exception as exc:
672
+ print(f"OAuth login failed: {exc}")
673
+ return
674
+
675
+ # Verify creds resolve + trigger project discovery
676
+ try:
677
+ creds = resolve_gemini_oauth_runtime_credentials(force_refresh=False)
678
+ project_id = creds.get("project_id", "")
679
+ if project_id:
680
+ print(f" Using GCP project: {project_id}")
681
+ else:
682
+ print(
683
+ " No GCP project configured — free tier will be auto-provisioned on first request."
684
+ )
685
+ except Exception as exc:
686
+ print(f"Failed to resolve Gemini credentials: {exc}")
687
+ return
688
+
689
+ models = list(_PROVIDER_MODELS.get("google-gemini-cli") or [])
690
+ default = current_model or (models[0] if models else "gemini-3-flash-preview")
691
+ selected = _prompt_model_selection(
692
+ models,
693
+ current_model=default,
694
+ confirm_provider="google-gemini-cli",
695
+ confirm_base_url=DEFAULT_GEMINI_CLOUDCODE_BASE_URL,
696
+ )
697
+ if selected:
698
+ _save_model_choice(selected)
699
+ _update_config_for_provider(
700
+ "google-gemini-cli", DEFAULT_GEMINI_CLOUDCODE_BASE_URL
701
+ )
702
+ print(
703
+ f"Default model set to: {selected} (via Google Gemini OAuth / Code Assist)"
704
+ )
705
+ else:
706
+ print("No change.")
707
+
708
+ def _model_flow_custom(config):
709
+ """Custom endpoint: collect URL, API key, and model name.
710
+
711
+ Automatically saves the endpoint to ``custom_providers`` in config.yaml
712
+ so it appears in the provider menu on subsequent runs.
713
+ """
714
+ from hermes_cli.main import _auto_provider_name, _prompt_custom_api_mode_selection, _save_custom_provider
715
+ from hermes_cli.auth import _save_model_choice, deactivate_provider
716
+ from hermes_cli.config import get_env_value, load_config, save_config
717
+ from hermes_cli.secret_prompt import masked_secret_prompt
718
+
719
+ current_url = get_env_value("OPENAI_BASE_URL") or ""
720
+ current_key = get_env_value("OPENAI_API_KEY") or ""
721
+
722
+ print("Custom OpenAI-compatible endpoint configuration:")
723
+ if current_url:
724
+ print(f" Current URL: {current_url}")
725
+ if current_key:
726
+ print(f" Current key: {current_key[:8]}...")
727
+ print()
728
+
729
+ try:
730
+ base_url = input(
731
+ f"API base URL [{current_url or 'e.g. https://api.example.com/v1'}]: "
732
+ ).strip()
733
+ api_key = masked_secret_prompt(
734
+ f"API key [{current_key[:8] + '...' if current_key else 'optional'}]: "
735
+ ).strip()
736
+ except (KeyboardInterrupt, EOFError):
737
+ print("\nCancelled.")
738
+ return
739
+
740
+ if not base_url and not current_url:
741
+ print("No URL provided. Cancelled.")
742
+ return
743
+
744
+ # Validate URL format
745
+ effective_url = base_url or current_url
746
+ if not effective_url.startswith(("http://", "https://")):
747
+ print(f"Invalid URL: {effective_url} (must start with http:// or https://)")
748
+ return
749
+
750
+ effective_key = api_key or current_key
751
+
752
+ # Hint: most local model servers (Ollama, vLLM, llama.cpp) require /v1
753
+ # in the base URL for OpenAI-compatible chat completions. Prompt the
754
+ # user if the URL looks like a local server without /v1.
755
+ _url_lower = effective_url.rstrip("/").lower()
756
+ _looks_local = any(
757
+ h in _url_lower
758
+ for h in ("localhost", "127.0.0.1", "0.0.0.0", ":11434", ":8080", ":5000")
759
+ )
760
+ if _looks_local and not _url_lower.endswith("/v1"):
761
+ print()
762
+ print(f" Hint: Did you mean to add /v1 at the end?")
763
+ print(f" Most local model servers (Ollama, vLLM, llama.cpp) require it.")
764
+ print(f" e.g. {effective_url.rstrip('/')}/v1")
765
+ try:
766
+ _add_v1 = input(" Add /v1? [Y/n]: ").strip().lower()
767
+ except (KeyboardInterrupt, EOFError):
768
+ _add_v1 = "n"
769
+ if _add_v1 in {"", "y", "yes"}:
770
+ effective_url = effective_url.rstrip("/") + "/v1"
771
+ if base_url:
772
+ base_url = effective_url
773
+ print(f" Updated URL: {effective_url}")
774
+ print()
775
+
776
+ from hermes_cli.models import probe_api_models
777
+
778
+ probe = probe_api_models(effective_key, effective_url)
779
+ if probe.get("used_fallback") and probe.get("resolved_base_url"):
780
+ print(
781
+ f"Warning: endpoint verification worked at {probe['resolved_base_url']}/models, "
782
+ f"not the exact URL you entered. Saving the working base URL instead."
783
+ )
784
+ effective_url = probe["resolved_base_url"]
785
+ if base_url:
786
+ base_url = effective_url
787
+ elif probe.get("models") is not None:
788
+ print(
789
+ f"Verified endpoint via {probe.get('probed_url')} "
790
+ f"({len(probe.get('models') or [])} model(s) visible)"
791
+ )
792
+ else:
793
+ print(
794
+ f"Warning: could not verify this endpoint via {probe.get('probed_url')}. "
795
+ f"Hermes will still save it."
796
+ )
797
+ if probe.get("suggested_base_url"):
798
+ suggested = probe["suggested_base_url"]
799
+ if suggested.endswith("/v1"):
800
+ print(
801
+ f" If this server expects /v1 in the path, try base URL: {suggested}"
802
+ )
803
+ else:
804
+ print(f" If /v1 should not be in the base URL, try: {suggested}")
805
+
806
+ # Prompt for API compatibility mode explicitly so codex-compatible custom
807
+ # providers don't silently fall back to chat_completions.
808
+ current_model_cfg = config.get("model")
809
+ current_api_mode = ""
810
+ if isinstance(current_model_cfg, dict):
811
+ current_api_mode = str(current_model_cfg.get("api_mode") or "").strip()
812
+ api_mode = _prompt_custom_api_mode_selection(
813
+ effective_url,
814
+ current_api_mode=current_api_mode,
815
+ )
816
+ if api_mode:
817
+ print(f" API mode: {api_mode}")
818
+ else:
819
+ print(" API mode: auto-detect")
820
+
821
+ # Select model — use probe results when available, fall back to manual input
822
+ model_name = ""
823
+ detected_models = probe.get("models") or []
824
+ try:
825
+ if len(detected_models) == 1:
826
+ print(f" Detected model: {detected_models[0]}")
827
+ confirm = input(" Use this model? [Y/n]: ").strip().lower()
828
+ if confirm in {"", "y", "yes"}:
829
+ model_name = detected_models[0]
830
+ else:
831
+ model_name = input("Model name (e.g. gpt-4, llama-3-70b): ").strip()
832
+ elif len(detected_models) > 1:
833
+ print(" Available models:")
834
+ for i, m in enumerate(detected_models, 1):
835
+ print(f" {i}. {m}")
836
+ pick = input(
837
+ f" Select model [1-{len(detected_models)}] or type name: "
838
+ ).strip()
839
+ if pick.isdigit() and 1 <= int(pick) <= len(detected_models):
840
+ model_name = detected_models[int(pick) - 1]
841
+ elif pick:
842
+ model_name = pick
843
+ else:
844
+ model_name = input("Model name (e.g. gpt-4, llama-3-70b): ").strip()
845
+
846
+ context_length_str = input(
847
+ "Context length in tokens [leave blank for auto-detect]: "
848
+ ).strip()
849
+
850
+ # Prompt for a display name — shown in the provider menu on future runs
851
+ default_name = _auto_provider_name(effective_url)
852
+ display_name = input(f"Display name [{default_name}]: ").strip() or default_name
853
+ except (KeyboardInterrupt, EOFError):
854
+ print("\nCancelled.")
855
+ return
856
+
857
+ context_length = None
858
+ if context_length_str:
859
+ try:
860
+ context_length = int(
861
+ context_length_str.replace(",", "")
862
+ .replace("k", "000")
863
+ .replace("K", "000")
864
+ )
865
+ if context_length <= 0:
866
+ context_length = None
867
+ except ValueError:
868
+ print(f"Invalid context length: {context_length_str} — will auto-detect.")
869
+ context_length = None
870
+
871
+ if model_name:
872
+ _save_model_choice(model_name)
873
+
874
+ # Update config and deactivate any OAuth provider
875
+ cfg = load_config()
876
+ model = cfg.get("model")
877
+ if not isinstance(model, dict):
878
+ model = {"default": model} if model else {}
879
+ cfg["model"] = model
880
+ model["provider"] = "custom"
881
+ model["base_url"] = effective_url
882
+ if effective_key:
883
+ model["api_key"] = effective_key
884
+ if api_mode:
885
+ model["api_mode"] = api_mode
886
+ else:
887
+ model.pop("api_mode", None)
888
+ save_config(cfg)
889
+ deactivate_provider()
890
+
891
+ # Sync the caller's config dict so the setup wizard's final
892
+ # save_config(config) preserves our model settings. Without
893
+ # this, the wizard overwrites model.provider/base_url with
894
+ # the stale values from its own config dict (#4172).
895
+ config["model"] = dict(model)
896
+
897
+ print(f"Default model set to: {model_name} (via {effective_url})")
898
+ else:
899
+ if base_url or api_key:
900
+ deactivate_provider()
901
+ # Even without a model name, persist the custom endpoint on the
902
+ # caller's config dict so the setup wizard doesn't lose it.
903
+ _caller_model = config.get("model")
904
+ if not isinstance(_caller_model, dict):
905
+ _caller_model = {"default": _caller_model} if _caller_model else {}
906
+ _caller_model["provider"] = "custom"
907
+ _caller_model["base_url"] = effective_url
908
+ if effective_key:
909
+ _caller_model["api_key"] = effective_key
910
+ if api_mode:
911
+ _caller_model["api_mode"] = api_mode
912
+ else:
913
+ _caller_model.pop("api_mode", None)
914
+ config["model"] = _caller_model
915
+ print("Endpoint saved. Use `/model` in chat or `hermes model` to set a model.")
916
+
917
+ # Auto-save to custom_providers so it appears in the menu next time
918
+ _save_custom_provider(
919
+ effective_url,
920
+ effective_key,
921
+ model_name or "",
922
+ context_length=context_length,
923
+ name=display_name,
924
+ api_mode=api_mode,
925
+ )
926
+
927
+ def _model_flow_azure_foundry(config, current_model=""):
928
+ """Azure Foundry provider: configure endpoint, auth mode, API mode, and model.
929
+
930
+ Azure Foundry supports both OpenAI-style (``/v1/chat/completions``) and
931
+ Anthropic-style (``/v1/messages``) endpoints, and two authentication
932
+ modes:
933
+
934
+ * **API key** (default) — uses ``AZURE_FOUNDRY_API_KEY`` from .env.
935
+ * **Microsoft Entra ID** — keyless, RBAC-based auth via the
936
+ ``azure-identity`` SDK (Managed Identity / Workload Identity / az
937
+ login / VS Code / azd / service principal env vars). Works on both
938
+ OpenAI-style and Anthropic-style endpoints — Microsoft RBAC is
939
+ per-resource and the same ``Azure AI User`` role grants
940
+ both. For OpenAI-style the OpenAI SDK's native callable
941
+ ``api_key=`` contract is used; for Anthropic-style an
942
+ ``httpx.Client`` with a request event hook (built by
943
+ :func:`agent.azure_identity_adapter.build_bearer_http_client`)
944
+ mints a fresh JWT per request because the Anthropic SDK does not
945
+ accept a callable ``auth_token`` natively.
946
+
947
+ The wizard auto-detects the transport and available models when
948
+ possible:
949
+
950
+ * URLs ending in ``/anthropic`` → Anthropic Messages API.
951
+ * Successful ``GET <base>/models`` probe → OpenAI-style + populates
952
+ a picker with the returned deployment / model IDs.
953
+ * Anthropic Messages probe fallback when ``/models`` fails.
954
+ * Manual entry when every probe fails (private endpoints, etc.).
955
+
956
+ Context lengths for the chosen model are resolved via the standard
957
+ :func:`agent.model_metadata.get_model_context_length` chain
958
+ (models.dev, provider metadata, hardcoded family fallbacks).
959
+ """
960
+ from hermes_cli.auth import _save_model_choice, deactivate_provider # noqa: F401
961
+ from hermes_cli.config import (
962
+ get_env_value,
963
+ save_env_value,
964
+ load_config,
965
+ save_config,
966
+ )
967
+ from hermes_cli import azure_detect
968
+
969
+ # ── Load current Azure Foundry configuration ─────────────────────
970
+ model_cfg = config.get("model", {})
971
+ if isinstance(model_cfg, dict) and model_cfg.get("provider") == "azure-foundry":
972
+ current_base_url = str(model_cfg.get("base_url", "") or "")
973
+ current_api_mode = str(model_cfg.get("api_mode", "") or "")
974
+ current_auth_mode = str(model_cfg.get("auth_mode") or "api_key").strip().lower() or "api_key"
975
+ _cur_entra = model_cfg.get("entra") or {}
976
+ current_entra = _cur_entra if isinstance(_cur_entra, dict) else {}
977
+ else:
978
+ current_base_url = ""
979
+ current_api_mode = ""
980
+ current_auth_mode = "api_key"
981
+ current_entra = {}
982
+
983
+ current_api_key = get_env_value("AZURE_FOUNDRY_API_KEY") or ""
984
+
985
+ print()
986
+ print("Azure Foundry Configuration")
987
+ print("=" * 50)
988
+ print()
989
+ print("Azure Foundry can host models with either OpenAI-style or")
990
+ print("Anthropic-style API endpoints. Hermes will probe your")
991
+ print("endpoint to auto-detect the transport and the deployed")
992
+ print("models when possible.")
993
+ print()
994
+
995
+ if current_base_url:
996
+ print(f" Current endpoint: {current_base_url}")
997
+ if current_api_mode:
998
+ _lbl = (
999
+ "OpenAI-style"
1000
+ if current_api_mode == "chat_completions"
1001
+ else "Anthropic-style"
1002
+ )
1003
+ print(f" Current API mode: {_lbl}")
1004
+ if current_auth_mode == "entra_id":
1005
+ print(f" Current auth mode: Microsoft Entra ID (keyless)")
1006
+ elif current_api_key:
1007
+ print(f" Current auth mode: API key ({current_api_key[:8]}...)")
1008
+ print()
1009
+
1010
+ # ── Step 1: endpoint URL ─────────────────────────────────────────
1011
+ try:
1012
+ _placeholder = (
1013
+ current_base_url
1014
+ or "e.g. https://<resource>.openai.azure.com/openai/v1 "
1015
+ "or https://<resource>.services.ai.azure.com/anthropic"
1016
+ )
1017
+ base_url = input(
1018
+ f"API endpoint URL [{_placeholder}]: "
1019
+ ).strip()
1020
+ except (KeyboardInterrupt, EOFError):
1021
+ print("\nCancelled.")
1022
+ return
1023
+
1024
+ effective_url = (base_url or current_base_url).rstrip("/")
1025
+ if not effective_url:
1026
+ print("No endpoint URL provided. Cancelled.")
1027
+ return
1028
+ if not effective_url.startswith(("http://", "https://")):
1029
+ print(f"Invalid URL: {effective_url} (must start with http:// or https://)")
1030
+ return
1031
+
1032
+ # ── Step 2: authentication mode ──────────────────────────────────
1033
+ print()
1034
+ print("Authentication:")
1035
+ print(" 1. API key (AZURE_FOUNDRY_API_KEY in .env)")
1036
+ print(" 2. Microsoft Entra ID (managed identity / workload identity / az login)")
1037
+ print(" Recommended by Microsoft. Works for both OpenAI-style and Anthropic-style endpoints.")
1038
+ print(" Requires the 'Azure AI User' role on the Foundry resource.")
1039
+ try:
1040
+ _auth_default = "2" if current_auth_mode == "entra_id" else "1"
1041
+ auth_choice = (
1042
+ input(f"Authentication mode [1/2] ({_auth_default}): ").strip()
1043
+ or _auth_default
1044
+ )
1045
+ except (KeyboardInterrupt, EOFError):
1046
+ print("\nCancelled.")
1047
+ return
1048
+ use_entra = auth_choice == "2"
1049
+ auth_mode_label = "entra_id" if use_entra else "api_key"
1050
+
1051
+ # ── Step 3: credentials (key OR Entra preflight) ─────────────────
1052
+ effective_key: str = ""
1053
+ entra_overrides: dict = {}
1054
+ token_provider = None # callable when entra
1055
+ entra_scope = ""
1056
+
1057
+ if use_entra:
1058
+ try:
1059
+ from agent.azure_identity_adapter import (
1060
+ EntraIdentityConfig,
1061
+ SCOPE_AI_AZURE_DEFAULT,
1062
+ build_token_provider,
1063
+ describe_active_credential,
1064
+ has_azure_identity_installed,
1065
+ )
1066
+ except ImportError as exc:
1067
+ print()
1068
+ print(f"⚠ Could not import azure-identity adapter: {exc}")
1069
+ print(" Falling back to API key auth.")
1070
+ use_entra = False
1071
+ auth_mode_label = "api_key"
1072
+
1073
+ if use_entra:
1074
+ print()
1075
+ if not has_azure_identity_installed():
1076
+ print("◐ The 'azure-identity' package is not installed yet.")
1077
+ print(
1078
+ " Hermes will install it now (the preflight below "
1079
+ "triggers the lazy-install). To skip lazy installs, "
1080
+ "run: pip install azure-identity"
1081
+ )
1082
+
1083
+ # Preserve only the optional scope override. Identity selection
1084
+ # (tenant, user-assigned MI, workload identity, service principal)
1085
+ # stays in Azure SDK env vars such as AZURE_CLIENT_ID.
1086
+ _persisted_scope_override = str(current_entra.get("scope") or "").strip()
1087
+ entra_scope = _persisted_scope_override or SCOPE_AI_AZURE_DEFAULT
1088
+
1089
+ entra_overrides = {}
1090
+ if _persisted_scope_override:
1091
+ entra_overrides["scope"] = _persisted_scope_override
1092
+
1093
+ print()
1094
+ print("◐ Probing Microsoft Entra ID credential chain (up to 10s)...")
1095
+ _config = EntraIdentityConfig(
1096
+ scope=entra_scope,
1097
+ )
1098
+ info = describe_active_credential(config=_config, timeout_seconds=10.0)
1099
+ if info.get("ok"):
1100
+ env_sources = info.get("env_sources") or []
1101
+ tag = ", ".join(env_sources) if env_sources else "default chain"
1102
+ print(f"✓ Entra ID token acquired ({tag}, scope={entra_scope})")
1103
+ else:
1104
+ err = info.get("error") or "credential chain exhausted"
1105
+ hint = info.get("hint") or (
1106
+ "Run `az login`, attach a managed identity to this VM, or "
1107
+ "set AZURE_TENANT_ID/AZURE_CLIENT_ID/AZURE_CLIENT_SECRET."
1108
+ )
1109
+ print(f"⚠ {err}")
1110
+ print(f" Hint: {hint}")
1111
+ try:
1112
+ ans = input("Save Entra config anyway and validate later? [Y/n]: ").strip().lower()
1113
+ except (KeyboardInterrupt, EOFError):
1114
+ print("\nCancelled.")
1115
+ return
1116
+ if ans and ans not in ("y", "yes"):
1117
+ print("Cancelled.")
1118
+ return
1119
+
1120
+ # Build the token provider for the detection probe (best-effort —
1121
+ # if the credential chain failed above, this will silently return
1122
+ # None inside azure_detect and the probe falls back to manual).
1123
+ try:
1124
+ token_provider = build_token_provider(config=_config)
1125
+ except Exception as exc:
1126
+ print(f"⚠ Could not build token provider for probing: {exc}")
1127
+ token_provider = None
1128
+ else:
1129
+ print()
1130
+ from hermes_cli.secret_prompt import masked_secret_prompt
1131
+
1132
+ try:
1133
+ api_key = masked_secret_prompt(
1134
+ f"API key [{current_api_key[:8] + '...' if current_api_key else 'required'}]: "
1135
+ ).strip()
1136
+ except (KeyboardInterrupt, EOFError):
1137
+ print("\nCancelled.")
1138
+ return
1139
+
1140
+ effective_key = api_key or current_api_key
1141
+ if not effective_key:
1142
+ print("No API key provided. Cancelled.")
1143
+ return
1144
+
1145
+ # ── Step 4: auto-detect transport + models ───────────────────────
1146
+ print()
1147
+ print("◐ Probing endpoint to auto-detect transport and models...")
1148
+ detection = azure_detect.detect(
1149
+ effective_url,
1150
+ api_key=effective_key,
1151
+ token_provider=token_provider,
1152
+ )
1153
+
1154
+ discovered_models: list[str] = list(detection.models)
1155
+ api_mode: str = detection.api_mode or ""
1156
+
1157
+ if api_mode:
1158
+ mode_label = (
1159
+ "OpenAI-style" if api_mode == "chat_completions" else "Anthropic-style"
1160
+ )
1161
+ print(f"✓ Detected API transport: {mode_label}")
1162
+ if detection.reason:
1163
+ print(f" ({detection.reason})")
1164
+ if discovered_models:
1165
+ print(
1166
+ f"✓ Found {len(discovered_models)} deployed model(s) on this endpoint"
1167
+ )
1168
+ else:
1169
+ print(f"⚠ Auto-detection incomplete: {detection.reason}")
1170
+ print()
1171
+ print("Select the API format your Azure Foundry endpoint uses:")
1172
+ print(" 1. OpenAI-style (POST /v1/chat/completions)")
1173
+ print(" For: GPT models, Llama, Mistral, and most open models")
1174
+ print(" 2. Anthropic-style (POST /v1/messages)")
1175
+ print(" For: Claude models deployed via Anthropic API format")
1176
+ try:
1177
+ default_choice = "2" if current_api_mode == "anthropic_messages" else "1"
1178
+ mode_choice = (
1179
+ input(f"API format [1/2] ({default_choice}): ").strip()
1180
+ or default_choice
1181
+ )
1182
+ except (KeyboardInterrupt, EOFError):
1183
+ print("\nCancelled.")
1184
+ return
1185
+ api_mode = "anthropic_messages" if mode_choice == "2" else "chat_completions"
1186
+
1187
+ # ── Step 5: model name ───────────────────────────────────────────
1188
+ print()
1189
+ effective_model = ""
1190
+ if discovered_models:
1191
+ print("Available models on this endpoint:")
1192
+ for i, mid in enumerate(discovered_models[:30], start=1):
1193
+ print(f" {i:>2}. {mid}")
1194
+ if len(discovered_models) > 30:
1195
+ print(
1196
+ f" ... and {len(discovered_models) - 30} more (type name manually if not shown)"
1197
+ )
1198
+ print()
1199
+ try:
1200
+ pick = input(
1201
+ f"Pick by number, or type a deployment name [{current_model or discovered_models[0]}]: "
1202
+ ).strip()
1203
+ except (KeyboardInterrupt, EOFError):
1204
+ print("\nCancelled.")
1205
+ return
1206
+ if not pick:
1207
+ effective_model = current_model or discovered_models[0]
1208
+ elif pick.isdigit() and 1 <= int(pick) <= min(len(discovered_models), 30):
1209
+ effective_model = discovered_models[int(pick) - 1]
1210
+ else:
1211
+ effective_model = pick
1212
+ else:
1213
+ try:
1214
+ model_name = input(
1215
+ f"Model / deployment name [{current_model or 'e.g. gpt-5.4, claude-sonnet-4-6'}]: "
1216
+ ).strip()
1217
+ except (KeyboardInterrupt, EOFError):
1218
+ print("\nCancelled.")
1219
+ return
1220
+ effective_model = model_name or current_model
1221
+
1222
+ if not effective_model:
1223
+ print("No model name provided. Cancelled.")
1224
+ return
1225
+
1226
+ # ── Step 6: context-length lookup ────────────────────────────────
1227
+ ctx_len = azure_detect.lookup_context_length(
1228
+ effective_model,
1229
+ effective_url,
1230
+ api_key=effective_key,
1231
+ token_provider=token_provider,
1232
+ )
1233
+
1234
+ # ── Step 7: persist ──────────────────────────────────────────────
1235
+ if not use_entra:
1236
+ save_env_value("AZURE_FOUNDRY_API_KEY", effective_key)
1237
+
1238
+ cfg = load_config()
1239
+ model = cfg.get("model")
1240
+ if not isinstance(model, dict):
1241
+ model = {"default": model} if model else {}
1242
+ cfg["model"] = model
1243
+
1244
+ model["provider"] = "azure-foundry"
1245
+ model["base_url"] = effective_url
1246
+ model["api_mode"] = api_mode
1247
+ model["default"] = effective_model
1248
+ model["auth_mode"] = auth_mode_label
1249
+ if use_entra:
1250
+ # Persist only the non-default Entra scope so config.yaml stays tidy.
1251
+ # Azure identity selection stays in standard AZURE_* env vars.
1252
+ clean_entra: dict = {}
1253
+ for key in ("scope",):
1254
+ val = entra_overrides.get(key)
1255
+ if val:
1256
+ clean_entra[key] = val
1257
+ if clean_entra:
1258
+ model["entra"] = clean_entra
1259
+ elif "entra" in model:
1260
+ del model["entra"]
1261
+ else:
1262
+ if "entra" in model:
1263
+ del model["entra"]
1264
+ if ctx_len:
1265
+ model["context_length"] = ctx_len
1266
+
1267
+ save_config(cfg)
1268
+ deactivate_provider()
1269
+ config["model"] = dict(model)
1270
+
1271
+ # Clear any conflicting env vars so auxiliary clients don't poison
1272
+ # themselves with a stale OpenAI base URL / key.
1273
+ if get_env_value("OPENAI_BASE_URL"):
1274
+ save_env_value("OPENAI_BASE_URL", "")
1275
+ if get_env_value("OPENAI_API_KEY"):
1276
+ save_env_value("OPENAI_API_KEY", "")
1277
+
1278
+ mode_label = "OpenAI-style" if api_mode == "chat_completions" else "Anthropic-style"
1279
+ auth_label = (
1280
+ "Microsoft Entra ID (keyless)" if use_entra else "API key"
1281
+ )
1282
+ print()
1283
+ print("✓ Azure Foundry configured:")
1284
+ print(f" Endpoint: {effective_url}")
1285
+ print(f" API mode: {mode_label}")
1286
+ print(f" Auth: {auth_label}")
1287
+ print(f" Model: {effective_model}")
1288
+ if ctx_len:
1289
+ print(f" Context length: {ctx_len:,} tokens")
1290
+ else:
1291
+ print(" Context length: not auto-detected (will fall back at runtime)")
1292
+ print()
1293
+
1294
+ def _model_flow_named_custom(config, provider_info):
1295
+ """Handle a named custom provider from config.yaml custom_providers list.
1296
+
1297
+ Always probes the endpoint's /models API to let the user pick a model.
1298
+ If a model was previously saved, it is pre-selected in the menu.
1299
+ Falls back to the saved model if probing fails.
1300
+ """
1301
+ from hermes_cli.main import _custom_provider_api_key_config_value, _custom_provider_base_url_config_value, _save_custom_provider
1302
+ from hermes_cli.auth import _save_model_choice, deactivate_provider
1303
+ from hermes_cli.config import load_config, save_config
1304
+ from hermes_cli.models import fetch_api_models
1305
+
1306
+ name = provider_info["name"]
1307
+ base_url = provider_info["base_url"]
1308
+ api_mode = provider_info.get("api_mode", "")
1309
+ api_key = provider_info.get("api_key", "")
1310
+ key_env = provider_info.get("key_env", "")
1311
+ saved_model = provider_info.get("model", "")
1312
+ provider_key = (provider_info.get("provider_key") or "").strip()
1313
+
1314
+ # Resolve key from env var if api_key not set directly
1315
+ if not api_key and key_env:
1316
+ api_key = os.environ.get(key_env, "")
1317
+ config_api_key = _custom_provider_api_key_config_value(provider_info, api_key)
1318
+
1319
+ # Honor ``discover_models: false`` (default True) — when discovery is
1320
+ # disabled, use the configured ``models:`` list verbatim and skip the
1321
+ # live /models probe. This lets operators restrict the picker to the
1322
+ # subset their plan actually serves instead of the endpoint's full
1323
+ # catalog (#18726: Baidu Qianfan returns 100+ models for a 2-3 model
1324
+ # plan). Same semantics as the slash-command picker (model_switch.py
1325
+ # sections 3 & 4): default discovers, false keeps the explicit list.
1326
+ discover = provider_info.get("discover_models", True)
1327
+ if isinstance(discover, str):
1328
+ discover = discover.lower() not in {"false", "no", "0"}
1329
+ configured_models: list[str] = []
1330
+ cfg_models = provider_info.get("models", {})
1331
+ if isinstance(cfg_models, dict):
1332
+ configured_models = [str(m) for m in cfg_models if str(m).strip()]
1333
+ elif isinstance(cfg_models, list):
1334
+ configured_models = [
1335
+ str(m) for m in cfg_models if isinstance(m, str) and m.strip()
1336
+ ]
1337
+
1338
+ print(f" Provider: {name}")
1339
+ print(f" URL: {base_url}")
1340
+ if saved_model:
1341
+ print(f" Current: {saved_model}")
1342
+ print()
1343
+
1344
+ if not discover and configured_models:
1345
+ # Discovery disabled with an explicit list — use it verbatim, no probe.
1346
+ print(f"Using configured models (discover_models: false): {len(configured_models)}")
1347
+ models = configured_models
1348
+ else:
1349
+ print("Fetching available models...")
1350
+ fetch_kwargs = {"timeout": 8.0}
1351
+ if api_mode:
1352
+ fetch_kwargs["api_mode"] = api_mode
1353
+ models = fetch_api_models(api_key, base_url, **fetch_kwargs)
1354
+ # If the probe came back empty but the operator configured an explicit
1355
+ # list, fall back to it rather than forcing manual entry.
1356
+ if not models and configured_models:
1357
+ models = configured_models
1358
+
1359
+ if models:
1360
+ default_idx = 0
1361
+ if saved_model and saved_model in models:
1362
+ default_idx = models.index(saved_model)
1363
+
1364
+ print(f"Found {len(models)} model(s):\n")
1365
+ try:
1366
+ from hermes_cli.curses_ui import curses_radiolist
1367
+
1368
+ menu_items = [
1369
+ f"{m} (current)" if m == saved_model else m for m in models
1370
+ ] + ["Cancel"]
1371
+ idx = curses_radiolist(
1372
+ f"Select model from {name}:",
1373
+ menu_items,
1374
+ selected=default_idx,
1375
+ cancel_returns=-1,
1376
+ searchable=True,
1377
+ )
1378
+ print()
1379
+ if idx < 0 or idx >= len(models):
1380
+ print("Cancelled.")
1381
+ return
1382
+ model_name = models[idx]
1383
+ except (ImportError, NotImplementedError, OSError, subprocess.SubprocessError):
1384
+ for i, m in enumerate(models, 1):
1385
+ suffix = " (current)" if m == saved_model else ""
1386
+ print(f" {i}. {m}{suffix}")
1387
+ print(f" {len(models) + 1}. Cancel")
1388
+ print()
1389
+ try:
1390
+ val = input(f"Choice [1-{len(models) + 1}]: ").strip()
1391
+ if not val:
1392
+ print("Cancelled.")
1393
+ return
1394
+ idx = int(val) - 1
1395
+ if idx < 0 or idx >= len(models):
1396
+ print("Cancelled.")
1397
+ return
1398
+ model_name = models[idx]
1399
+ except (ValueError, KeyboardInterrupt, EOFError):
1400
+ print("\nCancelled.")
1401
+ return
1402
+ elif saved_model:
1403
+ print("Could not fetch models from endpoint.")
1404
+ try:
1405
+ model_name = input(f"Model name [{saved_model}]: ").strip() or saved_model
1406
+ except (KeyboardInterrupt, EOFError):
1407
+ print("\nCancelled.")
1408
+ return
1409
+ else:
1410
+ print("Could not fetch models from endpoint. Enter model name manually.")
1411
+ try:
1412
+ model_name = input("Model name: ").strip()
1413
+ except (KeyboardInterrupt, EOFError):
1414
+ print("\nCancelled.")
1415
+ return
1416
+ if not model_name:
1417
+ print("No model specified. Cancelled.")
1418
+ return
1419
+
1420
+ # Activate and save the model to the custom_providers entry
1421
+ _save_model_choice(model_name)
1422
+
1423
+ cfg = load_config()
1424
+ model = cfg.get("model")
1425
+ if not isinstance(model, dict):
1426
+ model = {"default": model} if model else {}
1427
+ cfg["model"] = model
1428
+ if provider_key:
1429
+ model["provider"] = provider_key
1430
+ model.pop("base_url", None)
1431
+ model.pop("api_key", None)
1432
+ else:
1433
+ model["provider"] = "custom"
1434
+ model["base_url"] = _custom_provider_base_url_config_value(
1435
+ provider_info, base_url
1436
+ )
1437
+ if config_api_key:
1438
+ model["api_key"] = config_api_key
1439
+ # Apply api_mode from custom_providers entry, or clear stale value
1440
+ custom_api_mode = provider_info.get("api_mode", "")
1441
+ if custom_api_mode:
1442
+ model["api_mode"] = custom_api_mode
1443
+ else:
1444
+ model.pop("api_mode", None) # let runtime auto-detect from URL
1445
+ save_config(cfg)
1446
+ deactivate_provider()
1447
+
1448
+ # Persist the selected model back to whichever schema owns this endpoint.
1449
+ if provider_key:
1450
+ cfg = load_config()
1451
+ providers_cfg = cfg.get("providers")
1452
+ if isinstance(providers_cfg, dict):
1453
+ provider_entry = providers_cfg.get(provider_key)
1454
+ if isinstance(provider_entry, dict):
1455
+ provider_entry["default_model"] = model_name
1456
+ # Only persist an inline api_key when the user originally had
1457
+ # one (either a literal secret or a ``${VAR}`` template). When
1458
+ # the entry relies on ``key_env``, do not synthesize a
1459
+ # ``${key_env}`` api_key — the runtime already resolves the
1460
+ # key from ``key_env`` directly, and writing the resolved
1461
+ # secret (or even a synthesized template) would silently
1462
+ # downgrade credential hygiene on entries that intentionally
1463
+ # keep plaintext out of ``config.yaml``. See issue #15803.
1464
+ original_api_key_ref = str(
1465
+ provider_info.get("api_key_ref", "") or ""
1466
+ ).strip()
1467
+ original_api_key = str(provider_info.get("api_key", "") or "").strip()
1468
+ had_inline_api_key = bool(original_api_key_ref or original_api_key)
1469
+ if (
1470
+ had_inline_api_key
1471
+ and config_api_key
1472
+ and not str(provider_entry.get("api_key", "") or "").strip()
1473
+ ):
1474
+ provider_entry["api_key"] = config_api_key
1475
+ if key_env and not str(provider_entry.get("key_env", "") or "").strip():
1476
+ provider_entry["key_env"] = key_env
1477
+ cfg["providers"] = providers_cfg
1478
+ save_config(cfg)
1479
+ else:
1480
+ # Save model name to the custom_providers entry for next time
1481
+ _save_custom_provider(base_url, config_api_key, model_name, api_mode=api_mode)
1482
+
1483
+ print(f"\n✅ Model set to: {model_name}")
1484
+ print(f" Provider: {name} ({base_url})")
1485
+
1486
+ def _model_flow_copilot(config, current_model=""):
1487
+ """GitHub Copilot flow using env vars, gh CLI, or OAuth device code."""
1488
+ from hermes_cli.main import _current_reasoning_effort, _prompt_reasoning_effort_selection, _set_reasoning_effort
1489
+ from hermes_cli.auth import (
1490
+ PROVIDER_REGISTRY,
1491
+ _prompt_model_selection,
1492
+ _save_model_choice,
1493
+ deactivate_provider,
1494
+ resolve_api_key_provider_credentials,
1495
+ )
1496
+ from hermes_cli.config import save_env_value, load_config, save_config
1497
+ from hermes_cli.models import (
1498
+ _PROVIDER_MODELS,
1499
+ fetch_api_models,
1500
+ fetch_github_model_catalog,
1501
+ github_model_reasoning_efforts,
1502
+ copilot_model_api_mode,
1503
+ normalize_copilot_model_id,
1504
+ )
1505
+
1506
+ provider_id = "copilot"
1507
+ pconfig = PROVIDER_REGISTRY[provider_id]
1508
+
1509
+ creds = resolve_api_key_provider_credentials(provider_id)
1510
+ api_key = creds.get("api_key", "")
1511
+ source = creds.get("source", "")
1512
+
1513
+ if not api_key:
1514
+ print("No GitHub token configured for GitHub Copilot.")
1515
+ print()
1516
+ print(" Supported token types:")
1517
+ print(
1518
+ " → OAuth token (gho_*) via `copilot login` or device code flow"
1519
+ )
1520
+ print(" → Fine-grained PAT (github_pat_*) with Copilot Requests permission")
1521
+ print(" → GitHub App token (ghu_*) via environment variable")
1522
+ print(" ✗ Classic PAT (ghp_*) NOT supported by Copilot API")
1523
+ print()
1524
+ print(" Options:")
1525
+ print(" 1. Login with GitHub (OAuth device code flow)")
1526
+ print(" 2. Enter a token manually")
1527
+ print(" 3. Cancel")
1528
+ print()
1529
+ try:
1530
+ choice = input(" Choice [1-3]: ").strip()
1531
+ except (KeyboardInterrupt, EOFError):
1532
+ print()
1533
+ return
1534
+
1535
+ if choice == "1":
1536
+ try:
1537
+ from hermes_cli.copilot_auth import copilot_device_code_login
1538
+
1539
+ token = copilot_device_code_login()
1540
+ if token:
1541
+ save_env_value("COPILOT_GITHUB_TOKEN", token)
1542
+ print(" Copilot token saved.")
1543
+ print()
1544
+ else:
1545
+ print(" Login cancelled or failed.")
1546
+ return
1547
+ except Exception as exc:
1548
+ print(f" Login failed: {exc}")
1549
+ return
1550
+ elif choice == "2":
1551
+ from hermes_cli.secret_prompt import masked_secret_prompt
1552
+
1553
+ try:
1554
+ new_key = masked_secret_prompt(" Token (COPILOT_GITHUB_TOKEN): ").strip()
1555
+ except (KeyboardInterrupt, EOFError):
1556
+ print()
1557
+ return
1558
+ if not new_key:
1559
+ print(" Cancelled.")
1560
+ return
1561
+ # Validate token type
1562
+ try:
1563
+ from hermes_cli.copilot_auth import validate_copilot_token
1564
+
1565
+ valid, msg = validate_copilot_token(new_key)
1566
+ if not valid:
1567
+ print(f" ✗ {msg}")
1568
+ return
1569
+ except ImportError:
1570
+ pass
1571
+ save_env_value("COPILOT_GITHUB_TOKEN", new_key)
1572
+ print(" Token saved.")
1573
+ print()
1574
+ else:
1575
+ print(" Cancelled.")
1576
+ return
1577
+
1578
+ creds = resolve_api_key_provider_credentials(provider_id)
1579
+ api_key = creds.get("api_key", "")
1580
+ source = creds.get("source", "")
1581
+ else:
1582
+ if source in {"GITHUB_TOKEN", "GH_TOKEN"}:
1583
+ from hermes_cli.env_loader import format_secret_source_suffix
1584
+ bw_suffix = format_secret_source_suffix(source)
1585
+ print(f" GitHub token: {api_key[:8]}... ✓ ({source}{bw_suffix})")
1586
+ elif source == "gh auth token":
1587
+ print(" GitHub token: ✓ (from `gh auth token`)")
1588
+ else:
1589
+ print(" GitHub token: ✓")
1590
+ print()
1591
+
1592
+ effective_base = pconfig.inference_base_url
1593
+
1594
+ catalog = fetch_github_model_catalog(api_key)
1595
+ live_models = (
1596
+ [item.get("id", "") for item in catalog if item.get("id")]
1597
+ if catalog
1598
+ else fetch_api_models(api_key, effective_base)
1599
+ )
1600
+ normalized_current_model = (
1601
+ normalize_copilot_model_id(
1602
+ current_model,
1603
+ catalog=catalog,
1604
+ api_key=api_key,
1605
+ )
1606
+ or current_model
1607
+ )
1608
+ if live_models:
1609
+ model_list = [model_id for model_id in live_models if model_id]
1610
+ print(f" Found {len(model_list)} model(s) from GitHub Copilot")
1611
+ else:
1612
+ model_list = _PROVIDER_MODELS.get(provider_id, [])
1613
+ if model_list:
1614
+ print(
1615
+ " ⚠ Could not auto-detect models from GitHub Copilot — showing defaults."
1616
+ )
1617
+ print(' Use "Enter custom model name" if you do not see your model.')
1618
+
1619
+ if model_list:
1620
+ selected = _prompt_model_selection(
1621
+ model_list,
1622
+ current_model=normalized_current_model,
1623
+ confirm_provider=provider_id,
1624
+ confirm_base_url=effective_base,
1625
+ confirm_api_key=api_key,
1626
+ )
1627
+ else:
1628
+ try:
1629
+ selected = input("Model name: ").strip()
1630
+ except (KeyboardInterrupt, EOFError):
1631
+ selected = None
1632
+
1633
+ if selected:
1634
+ selected = (
1635
+ normalize_copilot_model_id(
1636
+ selected,
1637
+ catalog=catalog,
1638
+ api_key=api_key,
1639
+ )
1640
+ or selected
1641
+ )
1642
+ initial_cfg = load_config()
1643
+ current_effort = _current_reasoning_effort(initial_cfg)
1644
+ reasoning_efforts = github_model_reasoning_efforts(
1645
+ selected,
1646
+ catalog=catalog,
1647
+ api_key=api_key,
1648
+ )
1649
+ selected_effort = None
1650
+ if reasoning_efforts:
1651
+ print(f" {selected} supports reasoning controls.")
1652
+ selected_effort = _prompt_reasoning_effort_selection(
1653
+ reasoning_efforts, current_effort=current_effort
1654
+ )
1655
+
1656
+ _save_model_choice(selected)
1657
+
1658
+ cfg = load_config()
1659
+ model = cfg.get("model")
1660
+ if not isinstance(model, dict):
1661
+ model = {"default": model} if model else {}
1662
+ cfg["model"] = model
1663
+ model["provider"] = provider_id
1664
+ model["base_url"] = effective_base
1665
+ model["api_mode"] = copilot_model_api_mode(
1666
+ selected,
1667
+ catalog=catalog,
1668
+ api_key=api_key,
1669
+ )
1670
+ if selected_effort is not None:
1671
+ _set_reasoning_effort(cfg, selected_effort)
1672
+ save_config(cfg)
1673
+ deactivate_provider()
1674
+
1675
+ print(f"Default model set to: {selected} (via {pconfig.name})")
1676
+ if reasoning_efforts:
1677
+ if selected_effort == "none":
1678
+ print("Reasoning disabled for this model.")
1679
+ elif selected_effort:
1680
+ print(f"Reasoning effort set to: {selected_effort}")
1681
+ else:
1682
+ print("No change.")
1683
+
1684
+ def _model_flow_copilot_acp(config, current_model=""):
1685
+ """GitHub Copilot ACP flow using the local Copilot CLI."""
1686
+ from hermes_cli.auth import (
1687
+ PROVIDER_REGISTRY,
1688
+ _prompt_model_selection,
1689
+ _save_model_choice,
1690
+ deactivate_provider,
1691
+ get_external_process_provider_status,
1692
+ resolve_api_key_provider_credentials,
1693
+ resolve_external_process_provider_credentials,
1694
+ )
1695
+ from hermes_cli.models import (
1696
+ _PROVIDER_MODELS,
1697
+ fetch_github_model_catalog,
1698
+ normalize_copilot_model_id,
1699
+ )
1700
+ from hermes_cli.config import load_config, save_config
1701
+
1702
+ del config
1703
+
1704
+ provider_id = "copilot-acp"
1705
+ pconfig = PROVIDER_REGISTRY[provider_id]
1706
+
1707
+ status = get_external_process_provider_status(provider_id)
1708
+ resolved_command = (
1709
+ status.get("resolved_command") or status.get("command") or "copilot"
1710
+ )
1711
+ effective_base = status.get("base_url") or pconfig.inference_base_url
1712
+
1713
+ print(" GitHub Copilot ACP delegates Hermes turns to `copilot --acp`.")
1714
+ print(" Hermes currently starts its own ACP subprocess for each request.")
1715
+ print(" Hermes uses your selected model as a hint for the Copilot ACP session.")
1716
+ print(f" Command: {resolved_command}")
1717
+ print(f" Backend marker: {effective_base}")
1718
+ print()
1719
+
1720
+ try:
1721
+ creds = resolve_external_process_provider_credentials(provider_id)
1722
+ except Exception as exc:
1723
+ print(f" ⚠ {exc}")
1724
+ print(
1725
+ " Set HERMES_COPILOT_ACP_COMMAND or COPILOT_CLI_PATH if Copilot CLI is installed elsewhere."
1726
+ )
1727
+ return
1728
+
1729
+ effective_base = creds.get("base_url") or effective_base
1730
+
1731
+ catalog_api_key = ""
1732
+ try:
1733
+ catalog_creds = resolve_api_key_provider_credentials("copilot")
1734
+ catalog_api_key = catalog_creds.get("api_key", "")
1735
+ except Exception:
1736
+ pass
1737
+
1738
+ catalog = fetch_github_model_catalog(catalog_api_key)
1739
+ normalized_current_model = (
1740
+ normalize_copilot_model_id(
1741
+ current_model,
1742
+ catalog=catalog,
1743
+ api_key=catalog_api_key,
1744
+ )
1745
+ or current_model
1746
+ )
1747
+
1748
+ if catalog:
1749
+ model_list = [item.get("id", "") for item in catalog if item.get("id")]
1750
+ print(f" Found {len(model_list)} model(s) from GitHub Copilot")
1751
+ else:
1752
+ model_list = _PROVIDER_MODELS.get("copilot", [])
1753
+ if model_list:
1754
+ print(
1755
+ " ⚠ Could not auto-detect models from GitHub Copilot — showing defaults."
1756
+ )
1757
+ print(' Use "Enter custom model name" if you do not see your model.')
1758
+
1759
+ if model_list:
1760
+ selected = _prompt_model_selection(
1761
+ model_list,
1762
+ current_model=normalized_current_model,
1763
+ confirm_provider=provider_id,
1764
+ confirm_base_url=effective_base,
1765
+ confirm_api_key=catalog_api_key,
1766
+ )
1767
+ else:
1768
+ try:
1769
+ selected = input("Model name: ").strip()
1770
+ except (KeyboardInterrupt, EOFError):
1771
+ selected = None
1772
+
1773
+ if not selected:
1774
+ print("No change.")
1775
+ return
1776
+
1777
+ selected = (
1778
+ normalize_copilot_model_id(
1779
+ selected,
1780
+ catalog=catalog,
1781
+ api_key=catalog_api_key,
1782
+ )
1783
+ or selected
1784
+ )
1785
+ _save_model_choice(selected)
1786
+
1787
+ cfg = load_config()
1788
+ model = cfg.get("model")
1789
+ if not isinstance(model, dict):
1790
+ model = {"default": model} if model else {}
1791
+ cfg["model"] = model
1792
+ model["provider"] = provider_id
1793
+ model["base_url"] = effective_base
1794
+ model["api_mode"] = "chat_completions"
1795
+ save_config(cfg)
1796
+ deactivate_provider()
1797
+
1798
+ print(f"Default model set to: {selected} (via {pconfig.name})")
1799
+
1800
+ def _model_flow_kimi(config, current_model=""):
1801
+ """Kimi / Moonshot model selection with automatic endpoint routing.
1802
+
1803
+ - sk-kimi-* keys → api.kimi.com/coding/v1 (Kimi Coding Plan)
1804
+ - Other keys → api.moonshot.ai/v1 (legacy Moonshot)
1805
+
1806
+ No manual base URL prompt — endpoint is determined by key prefix.
1807
+ """
1808
+ from hermes_cli.main import _prompt_api_key
1809
+ from hermes_cli.auth import (
1810
+ PROVIDER_REGISTRY,
1811
+ KIMI_CODE_BASE_URL,
1812
+ _prompt_model_selection,
1813
+ _save_model_choice,
1814
+ deactivate_provider,
1815
+ )
1816
+ from hermes_cli.config import (
1817
+ get_env_value,
1818
+ save_env_value,
1819
+ load_config,
1820
+ save_config,
1821
+ )
1822
+ from hermes_cli.models import _PROVIDER_MODELS
1823
+
1824
+ provider_id = "kimi-coding"
1825
+ pconfig = PROVIDER_REGISTRY[provider_id]
1826
+ key_env = pconfig.api_key_env_vars[0] if pconfig.api_key_env_vars else ""
1827
+ base_url_env = pconfig.base_url_env_var or ""
1828
+
1829
+ # Step 1: Check / prompt for API key
1830
+ existing_key = ""
1831
+ for ev in pconfig.api_key_env_vars:
1832
+ existing_key = get_env_value(ev) or os.getenv(ev, "")
1833
+ if existing_key:
1834
+ break
1835
+
1836
+ existing_key, abort = _prompt_api_key(
1837
+ pconfig, existing_key, provider_id=provider_id
1838
+ )
1839
+ if abort:
1840
+ return
1841
+
1842
+ # Step 2: Auto-detect endpoint from key prefix
1843
+ is_coding_plan = existing_key.startswith("sk-kimi-")
1844
+ if is_coding_plan:
1845
+ effective_base = KIMI_CODE_BASE_URL
1846
+ print(f" Detected Kimi Coding Plan key → {effective_base}")
1847
+ else:
1848
+ effective_base = pconfig.inference_base_url
1849
+ print(f" Using Moonshot endpoint → {effective_base}")
1850
+ # Clear any manual base URL override so auto-detection works at runtime
1851
+ if base_url_env and get_env_value(base_url_env):
1852
+ save_env_value(base_url_env, "")
1853
+ print()
1854
+
1855
+ # Step 3: Model selection — show appropriate models for the endpoint
1856
+ model_list = _PROVIDER_MODELS.get("kimi-coding" if is_coding_plan else "moonshot", [])
1857
+
1858
+ if model_list:
1859
+ selected = _prompt_model_selection(
1860
+ model_list,
1861
+ current_model=current_model,
1862
+ confirm_provider=provider_id,
1863
+ confirm_base_url=effective_base,
1864
+ confirm_api_key=existing_key,
1865
+ )
1866
+ else:
1867
+ try:
1868
+ selected = input("Enter model name: ").strip()
1869
+ except (KeyboardInterrupt, EOFError):
1870
+ selected = None
1871
+
1872
+ if selected:
1873
+ _save_model_choice(selected)
1874
+
1875
+ # Update config with provider and base URL
1876
+ cfg = load_config()
1877
+ model = cfg.get("model")
1878
+ if not isinstance(model, dict):
1879
+ model = {"default": model} if model else {}
1880
+ cfg["model"] = model
1881
+ model["provider"] = provider_id
1882
+ model["base_url"] = effective_base
1883
+ model.pop("api_mode", None) # let runtime auto-detect from URL
1884
+ save_config(cfg)
1885
+ deactivate_provider()
1886
+
1887
+ endpoint_label = "Kimi Coding" if is_coding_plan else "Moonshot"
1888
+ print(f"Default model set to: {selected} (via {endpoint_label})")
1889
+ else:
1890
+ print("No change.")
1891
+
1892
+ def _model_flow_stepfun(config, current_model=""):
1893
+ """StepFun Step Plan flow with region-specific endpoints."""
1894
+ from hermes_cli.main import _infer_stepfun_region, _prompt_api_key, _prompt_provider_choice, _stepfun_base_url_for_region
1895
+ from hermes_cli.auth import (
1896
+ PROVIDER_REGISTRY,
1897
+ _prompt_model_selection,
1898
+ _save_model_choice,
1899
+ deactivate_provider,
1900
+ )
1901
+ from hermes_cli.config import (
1902
+ get_env_value,
1903
+ save_env_value,
1904
+ load_config,
1905
+ save_config,
1906
+ )
1907
+ from hermes_cli.models import _PROVIDER_MODELS, fetch_api_models
1908
+
1909
+ provider_id = "stepfun"
1910
+ pconfig = PROVIDER_REGISTRY[provider_id]
1911
+ key_env = pconfig.api_key_env_vars[0] if pconfig.api_key_env_vars else ""
1912
+ base_url_env = pconfig.base_url_env_var or ""
1913
+
1914
+ existing_key = ""
1915
+ for ev in pconfig.api_key_env_vars:
1916
+ existing_key = get_env_value(ev) or os.getenv(ev, "")
1917
+ if existing_key:
1918
+ break
1919
+
1920
+ existing_key, abort = _prompt_api_key(
1921
+ pconfig, existing_key, provider_id=provider_id
1922
+ )
1923
+ if abort:
1924
+ return
1925
+
1926
+ current_base = ""
1927
+ if base_url_env:
1928
+ current_base = get_env_value(base_url_env) or os.getenv(base_url_env, "")
1929
+ if not current_base:
1930
+ model_cfg = config.get("model")
1931
+ if isinstance(model_cfg, dict):
1932
+ current_base = str(model_cfg.get("base_url") or "").strip()
1933
+ current_region = _infer_stepfun_region(current_base or pconfig.inference_base_url)
1934
+
1935
+ region_choices = [
1936
+ (
1937
+ "international",
1938
+ f"International ({_stepfun_base_url_for_region('international')})",
1939
+ ),
1940
+ ("china", f"China ({_stepfun_base_url_for_region('china')})"),
1941
+ ]
1942
+ ordered_regions = []
1943
+ for region_key, label in region_choices:
1944
+ if region_key == current_region:
1945
+ ordered_regions.insert(0, (region_key, f"{label} ← currently active"))
1946
+ else:
1947
+ ordered_regions.append((region_key, label))
1948
+ ordered_regions.append(("cancel", "Cancel"))
1949
+
1950
+ region_idx = _prompt_provider_choice([label for _, label in ordered_regions])
1951
+ if region_idx is None or ordered_regions[region_idx][0] == "cancel":
1952
+ print("No change.")
1953
+ return
1954
+
1955
+ selected_region = ordered_regions[region_idx][0]
1956
+ effective_base = _stepfun_base_url_for_region(selected_region)
1957
+ if base_url_env:
1958
+ save_env_value(base_url_env, effective_base)
1959
+
1960
+ live_models = fetch_api_models(existing_key, effective_base)
1961
+ if live_models:
1962
+ model_list = live_models
1963
+ print(f" Found {len(model_list)} model(s) from {pconfig.name} API")
1964
+ else:
1965
+ model_list = _PROVIDER_MODELS.get(provider_id, [])
1966
+ if model_list:
1967
+ print(
1968
+ f" Could not auto-detect models from {pconfig.name} API — "
1969
+ "showing Step Plan fallback catalog."
1970
+ )
1971
+
1972
+ if model_list:
1973
+ selected = _prompt_model_selection(
1974
+ model_list,
1975
+ current_model=current_model,
1976
+ confirm_provider=provider_id,
1977
+ confirm_base_url=effective_base,
1978
+ confirm_api_key=existing_key,
1979
+ )
1980
+ else:
1981
+ try:
1982
+ selected = input("Model name: ").strip()
1983
+ except (KeyboardInterrupt, EOFError):
1984
+ selected = None
1985
+
1986
+ if selected:
1987
+ _save_model_choice(selected)
1988
+
1989
+ cfg = load_config()
1990
+ model = cfg.get("model")
1991
+ if not isinstance(model, dict):
1992
+ model = {"default": model} if model else {}
1993
+ cfg["model"] = model
1994
+ model["provider"] = provider_id
1995
+ model["base_url"] = effective_base
1996
+ model.pop("api_mode", None)
1997
+ save_config(cfg)
1998
+ deactivate_provider()
1999
+
2000
+ config["model"] = dict(model)
2001
+ print(f"Default model set to: {selected} (via {pconfig.name})")
2002
+ else:
2003
+ print("No change.")
2004
+
2005
+ def _model_flow_bedrock_api_key(config, region, current_model=""):
2006
+ """Bedrock API Key mode — uses the OpenAI-compatible bedrock-mantle endpoint.
2007
+
2008
+ For developers who don't have an AWS account but received a Bedrock API Key
2009
+ from their AWS admin. Works like any OpenAI-compatible endpoint.
2010
+ """
2011
+ from hermes_cli.auth import (
2012
+ _prompt_model_selection,
2013
+ _save_model_choice,
2014
+ deactivate_provider,
2015
+ )
2016
+ from hermes_cli.config import (
2017
+ load_config,
2018
+ save_config,
2019
+ get_env_value,
2020
+ save_env_value,
2021
+ )
2022
+ from hermes_cli.models import _PROVIDER_MODELS
2023
+
2024
+ mantle_base_url = f"https://bedrock-mantle.{region}.api.aws/v1"
2025
+
2026
+ # Prompt for API key
2027
+ existing_key = get_env_value("AWS_BEARER_TOKEN_BEDROCK") or ""
2028
+ if existing_key:
2029
+ from hermes_cli.env_loader import format_secret_source_suffix
2030
+ source_suffix = format_secret_source_suffix("AWS_BEARER_TOKEN_BEDROCK")
2031
+ print(f" Bedrock API Key: {existing_key[:12]}... ✓{source_suffix}")
2032
+ else:
2033
+ print(f" Endpoint: {mantle_base_url}")
2034
+ print()
2035
+ from hermes_cli.secret_prompt import masked_secret_prompt
2036
+
2037
+ try:
2038
+ api_key = masked_secret_prompt(" Bedrock API Key: ").strip()
2039
+ except (KeyboardInterrupt, EOFError):
2040
+ print()
2041
+ return
2042
+ if not api_key:
2043
+ print(" Cancelled.")
2044
+ return
2045
+ save_env_value("AWS_BEARER_TOKEN_BEDROCK", api_key)
2046
+ existing_key = api_key
2047
+ print(" ✓ API key saved.")
2048
+ print()
2049
+
2050
+ # Model selection — use static list (mantle doesn't need boto3 for discovery)
2051
+ model_list = _PROVIDER_MODELS.get("bedrock", [])
2052
+ print(f" Showing {len(model_list)} curated models")
2053
+
2054
+ if model_list:
2055
+ selected = _prompt_model_selection(
2056
+ model_list,
2057
+ current_model=current_model,
2058
+ confirm_provider="custom",
2059
+ confirm_base_url=mantle_base_url,
2060
+ confirm_api_key=existing_key,
2061
+ )
2062
+ else:
2063
+ try:
2064
+ selected = input(" Model ID: ").strip()
2065
+ except (KeyboardInterrupt, EOFError):
2066
+ selected = None
2067
+
2068
+ if selected:
2069
+ _save_model_choice(selected)
2070
+
2071
+ # Save as custom provider pointing to bedrock-mantle
2072
+ cfg = load_config()
2073
+ model = cfg.get("model")
2074
+ if not isinstance(model, dict):
2075
+ model = {"default": model} if model else {}
2076
+ cfg["model"] = model
2077
+ model["provider"] = "custom"
2078
+ model["base_url"] = mantle_base_url
2079
+ model.pop("api_mode", None) # chat_completions is the default
2080
+
2081
+ # Also save region in bedrock config for reference
2082
+ bedrock_cfg = cfg.get("bedrock", {})
2083
+ if not isinstance(bedrock_cfg, dict):
2084
+ bedrock_cfg = {}
2085
+ bedrock_cfg["region"] = region
2086
+ cfg["bedrock"] = bedrock_cfg
2087
+
2088
+ # Save the API key env var name so hermes knows where to find it
2089
+ save_env_value("OPENAI_API_KEY", existing_key)
2090
+ save_env_value("OPENAI_BASE_URL", mantle_base_url)
2091
+
2092
+ save_config(cfg)
2093
+ deactivate_provider()
2094
+
2095
+ print(f" Default model set to: {selected} (via Bedrock API Key, {region})")
2096
+ print(f" Endpoint: {mantle_base_url}")
2097
+ else:
2098
+ print(" No change.")
2099
+
2100
+ def _model_flow_bedrock(config, current_model=""):
2101
+ """AWS Bedrock provider: verify credentials, pick region, discover models.
2102
+
2103
+ Uses the native Converse API via boto3 — not the OpenAI-compatible endpoint.
2104
+ Auth is handled by the AWS SDK default credential chain (env vars, profile,
2105
+ instance role), so no API key prompt is needed.
2106
+ """
2107
+ from hermes_cli.auth import (
2108
+ _prompt_model_selection,
2109
+ _save_model_choice,
2110
+ deactivate_provider,
2111
+ )
2112
+ from hermes_cli.config import load_config, save_config
2113
+ from hermes_cli.models import _PROVIDER_MODELS
2114
+
2115
+ # 1. Check for AWS credentials
2116
+ try:
2117
+ from agent.bedrock_adapter import (
2118
+ has_aws_credentials,
2119
+ resolve_aws_auth_env_var,
2120
+ resolve_bedrock_region,
2121
+ discover_bedrock_models,
2122
+ )
2123
+ except ImportError:
2124
+ print(" ✗ boto3 is not installed. Install it with:")
2125
+ print(" pip install boto3")
2126
+ print()
2127
+ return
2128
+
2129
+ if not has_aws_credentials():
2130
+ print(" ⚠ No AWS credentials detected via environment variables.")
2131
+ print(" Bedrock will use boto3's default credential chain (IMDS, SSO, etc.)")
2132
+ print()
2133
+
2134
+ auth_var = resolve_aws_auth_env_var()
2135
+ if auth_var:
2136
+ print(f" AWS credentials: {auth_var} ✓")
2137
+ else:
2138
+ print(" AWS credentials: boto3 default chain (instance role / SSO)")
2139
+ print()
2140
+
2141
+ # 2. Region selection
2142
+ current_region = resolve_bedrock_region()
2143
+ try:
2144
+ region_input = input(f" AWS Region [{current_region}]: ").strip()
2145
+ except (KeyboardInterrupt, EOFError):
2146
+ print()
2147
+ return
2148
+ region = region_input or current_region
2149
+
2150
+ # 2b. Authentication mode
2151
+ print(" Choose authentication method:")
2152
+ print()
2153
+ print(" 1. IAM credential chain (recommended)")
2154
+ print(" Works with EC2 instance roles, SSO, env vars, aws configure")
2155
+ print(" 2. Bedrock API Key")
2156
+ print(" Enter your Bedrock API Key directly — also supports")
2157
+ print(" team scenarios where an admin distributes keys")
2158
+ print()
2159
+ try:
2160
+ auth_choice = input(" Choice [1]: ").strip()
2161
+ except (KeyboardInterrupt, EOFError):
2162
+ print()
2163
+ return
2164
+
2165
+ if auth_choice == "2":
2166
+ _model_flow_bedrock_api_key(config, region, current_model)
2167
+ return
2168
+
2169
+ # 3. Model discovery — try live API first, fall back to static list
2170
+ print(f" Discovering models in {region}...")
2171
+ live_models = discover_bedrock_models(region)
2172
+
2173
+ if live_models:
2174
+ _EXCLUDE_PREFIXES = (
2175
+ "stability.",
2176
+ "cohere.embed",
2177
+ "twelvelabs.",
2178
+ "us.stability.",
2179
+ "us.cohere.embed",
2180
+ "us.twelvelabs.",
2181
+ "global.cohere.embed",
2182
+ "global.twelvelabs.",
2183
+ )
2184
+ _EXCLUDE_SUBSTRINGS = ("safeguard", "voxtral", "palmyra-vision")
2185
+ filtered = []
2186
+ for m in live_models:
2187
+ mid = m["id"]
2188
+ if any(mid.startswith(p) for p in _EXCLUDE_PREFIXES):
2189
+ continue
2190
+ if any(s in mid.lower() for s in _EXCLUDE_SUBSTRINGS):
2191
+ continue
2192
+ filtered.append(m)
2193
+
2194
+ # Deduplicate: prefer inference profiles (us.*, global.*) over bare
2195
+ # foundation model IDs.
2196
+ profile_base_ids = set()
2197
+ for m in filtered:
2198
+ mid = m["id"]
2199
+ if mid.startswith(("us.", "global.")):
2200
+ base = mid.split(".", 1)[1] if "." in mid[3:] else mid
2201
+ profile_base_ids.add(base)
2202
+
2203
+ deduped = []
2204
+ for m in filtered:
2205
+ mid = m["id"]
2206
+ if not mid.startswith(("us.", "global.")) and mid in profile_base_ids:
2207
+ continue
2208
+ deduped.append(m)
2209
+
2210
+ _RECOMMENDED = [
2211
+ "us.anthropic.claude-sonnet-4-6",
2212
+ "us.anthropic.claude-opus-4-6",
2213
+ "us.anthropic.claude-haiku-4-5",
2214
+ "us.amazon.nova-pro",
2215
+ "us.amazon.nova-lite",
2216
+ "us.amazon.nova-micro",
2217
+ "deepseek.v3",
2218
+ "us.meta.llama4-maverick",
2219
+ "us.meta.llama4-scout",
2220
+ ]
2221
+
2222
+ def _sort_key(m):
2223
+ mid = m["id"]
2224
+ for i, rec in enumerate(_RECOMMENDED):
2225
+ if mid.startswith(rec):
2226
+ return (0, i, mid)
2227
+ if mid.startswith("global."):
2228
+ return (1, 0, mid)
2229
+ return (2, 0, mid)
2230
+
2231
+ deduped.sort(key=_sort_key)
2232
+ model_list = [m["id"] for m in deduped]
2233
+ print(
2234
+ f" Found {len(model_list)} text model(s) (filtered from {len(live_models)} total)"
2235
+ )
2236
+ else:
2237
+ model_list = _PROVIDER_MODELS.get("bedrock", [])
2238
+ if model_list:
2239
+ print(
2240
+ f" Using {len(model_list)} curated models (live discovery unavailable)"
2241
+ )
2242
+ else:
2243
+ print(
2244
+ " No models found. Check IAM permissions for bedrock:ListFoundationModels."
2245
+ )
2246
+ return
2247
+
2248
+ # 4. Model selection
2249
+ if model_list:
2250
+ selected = _prompt_model_selection(
2251
+ model_list,
2252
+ current_model=current_model,
2253
+ confirm_provider="bedrock",
2254
+ confirm_base_url=f"https://bedrock-runtime.{region}.amazonaws.com",
2255
+ )
2256
+ else:
2257
+ try:
2258
+ selected = input(" Model ID: ").strip()
2259
+ except (KeyboardInterrupt, EOFError):
2260
+ selected = None
2261
+
2262
+ if selected:
2263
+ _save_model_choice(selected)
2264
+
2265
+ cfg = load_config()
2266
+ model = cfg.get("model")
2267
+ if not isinstance(model, dict):
2268
+ model = {"default": model} if model else {}
2269
+ cfg["model"] = model
2270
+ model["provider"] = "bedrock"
2271
+ model["base_url"] = f"https://bedrock-runtime.{region}.amazonaws.com"
2272
+ model.pop("api_mode", None) # bedrock_converse is auto-detected
2273
+
2274
+ bedrock_cfg = cfg.get("bedrock", {})
2275
+ if not isinstance(bedrock_cfg, dict):
2276
+ bedrock_cfg = {}
2277
+ bedrock_cfg["region"] = region
2278
+ cfg["bedrock"] = bedrock_cfg
2279
+
2280
+ save_config(cfg)
2281
+ deactivate_provider()
2282
+
2283
+ print(f" Default model set to: {selected} (via AWS Bedrock, {region})")
2284
+ else:
2285
+ print(" No change.")
2286
+
2287
+ def _model_flow_api_key_provider(config, provider_id, current_model=""):
2288
+ """Generic flow for API-key providers (z.ai, MiniMax, OpenCode, etc.)."""
2289
+ from hermes_cli.main import _prompt_api_key
2290
+ from hermes_cli.auth import (
2291
+ PROVIDER_REGISTRY,
2292
+ _prompt_model_selection,
2293
+ _save_model_choice,
2294
+ deactivate_provider,
2295
+ )
2296
+ from hermes_cli.config import (
2297
+ get_env_value,
2298
+ save_env_value,
2299
+ load_config,
2300
+ save_config,
2301
+ )
2302
+ from hermes_cli.models import (
2303
+ _PROVIDER_MODELS,
2304
+ fetch_api_models,
2305
+ opencode_model_api_mode,
2306
+ normalize_opencode_model_id,
2307
+ )
2308
+
2309
+ pconfig = PROVIDER_REGISTRY[provider_id]
2310
+ key_env = pconfig.api_key_env_vars[0] if pconfig.api_key_env_vars else ""
2311
+ base_url_env = pconfig.base_url_env_var or ""
2312
+
2313
+ # Check / prompt for API key
2314
+ existing_key = ""
2315
+ for ev in pconfig.api_key_env_vars:
2316
+ existing_key = get_env_value(ev) or os.getenv(ev, "")
2317
+ if existing_key:
2318
+ break
2319
+
2320
+ existing_key, abort = _prompt_api_key(
2321
+ pconfig, existing_key, provider_id=provider_id
2322
+ )
2323
+ if abort:
2324
+ return
2325
+
2326
+ # Gemini free-tier gate: free-tier daily quotas (<= 250 RPD for Flash)
2327
+ # are exhausted in a handful of agent turns, so refuse to wire up the
2328
+ # provider with a free-tier key. Probe is best-effort; network or auth
2329
+ # errors fall through without blocking.
2330
+ if provider_id == "gemini" and existing_key:
2331
+ try:
2332
+ from agent.gemini_native_adapter import probe_gemini_tier
2333
+ except Exception:
2334
+ probe_gemini_tier = None
2335
+ if probe_gemini_tier is not None:
2336
+ print(" Checking Gemini API tier...")
2337
+ probe_base = (
2338
+ (get_env_value(base_url_env) if base_url_env else "")
2339
+ or os.getenv(base_url_env or "", "")
2340
+ or pconfig.inference_base_url
2341
+ )
2342
+ tier = probe_gemini_tier(existing_key, probe_base)
2343
+ if tier == "free":
2344
+ print()
2345
+ print(
2346
+ "❌ This Google API key is on the free tier "
2347
+ "(<= 250 requests/day for gemini-2.5-flash)."
2348
+ )
2349
+ print(
2350
+ " Hermes typically makes 3-10 API calls per user turn "
2351
+ "(tool iterations + auxiliary tasks),"
2352
+ )
2353
+ print(
2354
+ " so the free tier is exhausted after a handful of "
2355
+ "messages and cannot sustain"
2356
+ )
2357
+ print(" an agent session.")
2358
+ print()
2359
+ print(
2360
+ " To use Gemini with Hermes, enable billing on your "
2361
+ "Google Cloud project and regenerate"
2362
+ )
2363
+ print(
2364
+ " the key in a billing-enabled project: "
2365
+ "https://aistudio.google.com/apikey"
2366
+ )
2367
+ print()
2368
+ print(
2369
+ " Alternatives with workable free usage: DeepSeek, "
2370
+ "OpenRouter (free models), Groq, Nous."
2371
+ )
2372
+ print()
2373
+ print("Not saving Gemini as the default provider.")
2374
+ return
2375
+ if tier == "paid":
2376
+ print(" Tier check: paid ✓")
2377
+ else:
2378
+ # "unknown" -- network issue, auth problem, unexpected response.
2379
+ # Don't block; the runtime 429 handler will surface free-tier
2380
+ # guidance if the key turns out to be free tier.
2381
+ print(" Tier check: could not verify (proceeding anyway).")
2382
+ print()
2383
+
2384
+ # Optional base URL override.
2385
+ # Precedence: env var → config.yaml model.base_url → registry default.
2386
+ # Reading config.yaml prevents silently overwriting a saved remote URL
2387
+ # (e.g. a remote LM Studio endpoint) with localhost when the user just
2388
+ # presses Enter at the prompt below.
2389
+ current_base = ""
2390
+ if base_url_env:
2391
+ current_base = get_env_value(base_url_env) or os.getenv(base_url_env, "")
2392
+ if not current_base:
2393
+ try:
2394
+ _m = load_config().get("model") or {}
2395
+ if str(_m.get("provider") or "").strip().lower() == provider_id:
2396
+ current_base = str(_m.get("base_url") or "").strip()
2397
+ except Exception:
2398
+ pass
2399
+ effective_base = current_base or pconfig.inference_base_url
2400
+
2401
+ if provider_id == "usepod":
2402
+ # UsePod authenticates by the token embedded in the URL path, so there
2403
+ # is no base URL for the user to type — it is derived from the pasted
2404
+ # key. Skip the prompt; honour USEPOD_BASE_URL only for self-hosting.
2405
+ from hermes_cli.auth import _resolve_usepod_base_url
2406
+
2407
+ key_for_probe = existing_key or (get_env_value(key_env) if key_env else "")
2408
+ env_override = ""
2409
+ if base_url_env:
2410
+ env_override = get_env_value(base_url_env) or os.getenv(base_url_env, "")
2411
+ effective_base = _resolve_usepod_base_url(key_for_probe, env_override)
2412
+ else:
2413
+ try:
2414
+ override = input(f"Base URL [{effective_base}]: ").strip()
2415
+ except (KeyboardInterrupt, EOFError):
2416
+ print()
2417
+ override = ""
2418
+ if override and base_url_env:
2419
+ if not override.startswith(("http://", "https://")):
2420
+ print(
2421
+ " Invalid URL — must start with http:// or https://. Keeping current value."
2422
+ )
2423
+ else:
2424
+ save_env_value(base_url_env, override)
2425
+ effective_base = override
2426
+
2427
+ # Model selection — resolution order:
2428
+ # 1. models.dev registry (cached, filtered for agentic/tool-capable models)
2429
+ # 2. Curated static fallback list (offline insurance)
2430
+ # 3. Live /models endpoint probe (small providers without models.dev data)
2431
+ #
2432
+ # LM Studio: live /api/v1/models probe (no models.dev catalog).
2433
+ # Ollama Cloud: merged discovery (live API + models.dev + disk cache).
2434
+ if provider_id == "lmstudio":
2435
+ from hermes_cli.auth import AuthError
2436
+ from hermes_cli.models import fetch_lmstudio_models
2437
+
2438
+ api_key_for_probe = existing_key or (get_env_value(key_env) if key_env else "")
2439
+ try:
2440
+ model_list = fetch_lmstudio_models(
2441
+ api_key=api_key_for_probe, base_url=effective_base
2442
+ )
2443
+ except AuthError as exc:
2444
+ print(f" LM Studio rejected the request: {exc}")
2445
+ print(" Set LM_API_KEY (or update it) to match the server's bearer token.")
2446
+ model_list = []
2447
+ if model_list:
2448
+ print(f" Found {len(model_list)} model(s) from LM Studio")
2449
+ elif provider_id == "ollama-cloud":
2450
+ from hermes_cli.models import fetch_ollama_cloud_models
2451
+
2452
+ api_key_for_probe = existing_key or (get_env_value(key_env) if key_env else "")
2453
+ # During setup, force a live refresh so the picker reflects newly
2454
+ # released models (e.g. deepseek v4 flash, kimi k2.6) the moment
2455
+ # the user enters their key — not an hour later when the disk
2456
+ # cache TTL expires.
2457
+ model_list = fetch_ollama_cloud_models(
2458
+ api_key=api_key_for_probe,
2459
+ base_url=effective_base,
2460
+ force_refresh=True,
2461
+ )
2462
+ if model_list:
2463
+ print(f" Found {len(model_list)} model(s) from Ollama Cloud")
2464
+ elif provider_id == "usepod":
2465
+ from hermes_cli.models import fetch_api_models
2466
+ from providers import get_provider_profile
2467
+
2468
+ api_key_for_probe = existing_key or (get_env_value(key_env) if key_env else "")
2469
+ live_models = fetch_api_models(api_key_for_probe, effective_base)
2470
+ if live_models:
2471
+ model_list = live_models
2472
+ print(f" Found {len(model_list)} model(s) from {pconfig.name} API")
2473
+ else:
2474
+ _pp = get_provider_profile("usepod")
2475
+ model_list = list(getattr(_pp, "fallback_models", ()) or [])
2476
+ if model_list:
2477
+ print(
2478
+ f' Showing {len(model_list)} curated models — use "Enter custom model name" for others.'
2479
+ )
2480
+ elif provider_id == "novita":
2481
+ from hermes_cli.models import fetch_api_models
2482
+
2483
+ api_key_for_probe = existing_key or (get_env_value(key_env) if key_env else "")
2484
+ curated = _PROVIDER_MODELS.get(provider_id, [])
2485
+ live_models = fetch_api_models(api_key_for_probe, effective_base)
2486
+ if live_models:
2487
+ model_list = live_models
2488
+ print(f" Found {len(model_list)} model(s) from {pconfig.name} API")
2489
+ else:
2490
+ mdev_models: list = []
2491
+ try:
2492
+ from agent.models_dev import list_agentic_models
2493
+
2494
+ mdev_models = list_agentic_models(provider_id)
2495
+ except Exception:
2496
+ pass
2497
+ if mdev_models:
2498
+ seen = {m.lower() for m in mdev_models}
2499
+ model_list = list(mdev_models)
2500
+ for m in curated:
2501
+ if m.lower() not in seen:
2502
+ model_list.append(m)
2503
+ seen.add(m.lower())
2504
+ print(f" Found {len(model_list)} model(s) from models.dev registry")
2505
+ else:
2506
+ model_list = curated
2507
+ if model_list:
2508
+ print(
2509
+ f' Showing {len(model_list)} curated models — use "Enter custom model name" for others.'
2510
+ )
2511
+ else:
2512
+ curated = _PROVIDER_MODELS.get(provider_id, [])
2513
+
2514
+ # Try models.dev first — returns tool-capable models, filtered for noise
2515
+ mdev_models: list = []
2516
+ try:
2517
+ from agent.models_dev import list_agentic_models
2518
+
2519
+ mdev_models = list_agentic_models(provider_id)
2520
+ except Exception:
2521
+ pass
2522
+
2523
+ if mdev_models:
2524
+ # Merge models.dev with curated list so newly added models
2525
+ # (not yet in models.dev) still appear in the picker.
2526
+ if curated:
2527
+ seen = {m.lower() for m in mdev_models}
2528
+ merged = list(mdev_models)
2529
+ for m in curated:
2530
+ if m.lower() not in seen:
2531
+ merged.append(m)
2532
+ seen.add(m.lower())
2533
+ model_list = merged
2534
+ else:
2535
+ model_list = mdev_models
2536
+ print(f" Found {len(model_list)} model(s) from models.dev registry")
2537
+ elif curated and len(curated) >= 8:
2538
+ # Curated list is substantial — use it directly, skip live probe
2539
+ model_list = curated
2540
+ print(
2541
+ f' Showing {len(model_list)} curated models — use "Enter custom model name" for others.'
2542
+ )
2543
+ else:
2544
+ api_key_for_probe = existing_key or (
2545
+ get_env_value(key_env) if key_env else ""
2546
+ )
2547
+ live_models = fetch_api_models(api_key_for_probe, effective_base)
2548
+ if live_models and len(live_models) >= len(curated):
2549
+ model_list = live_models
2550
+ print(f" Found {len(model_list)} model(s) from {pconfig.name} API")
2551
+ else:
2552
+ model_list = curated
2553
+ if model_list:
2554
+ print(
2555
+ f' Showing {len(model_list)} curated models — use "Enter custom model name" for others.'
2556
+ )
2557
+ # else: no defaults either, will fall through to raw input
2558
+
2559
+ if provider_id in {"opencode-zen", "opencode-go"}:
2560
+ model_list = [
2561
+ normalize_opencode_model_id(provider_id, mid) for mid in model_list
2562
+ ]
2563
+ current_model = normalize_opencode_model_id(provider_id, current_model)
2564
+ model_list = list(dict.fromkeys(mid for mid in model_list if mid))
2565
+
2566
+ if model_list:
2567
+ selected = _prompt_model_selection(
2568
+ model_list,
2569
+ current_model=current_model,
2570
+ confirm_provider=provider_id,
2571
+ confirm_base_url=effective_base,
2572
+ confirm_api_key=existing_key,
2573
+ )
2574
+ else:
2575
+ try:
2576
+ selected = input("Model name: ").strip()
2577
+ except (KeyboardInterrupt, EOFError):
2578
+ selected = None
2579
+
2580
+ if selected:
2581
+ if provider_id in {"opencode-zen", "opencode-go"}:
2582
+ selected = normalize_opencode_model_id(provider_id, selected)
2583
+
2584
+ _save_model_choice(selected)
2585
+
2586
+ # Update config with provider, base URL, and provider-specific API mode
2587
+ cfg = load_config()
2588
+ model = cfg.get("model")
2589
+ if not isinstance(model, dict):
2590
+ model = {"default": model} if model else {}
2591
+ cfg["model"] = model
2592
+ model["provider"] = provider_id
2593
+ if provider_id == "usepod":
2594
+ # The effective base URL embeds the token; persisting it would bake
2595
+ # the secret into config.yaml and go stale on key rotation. Runtime
2596
+ # always re-derives it from the current USEPOD_API_KEY.
2597
+ model.pop("base_url", None)
2598
+ else:
2599
+ model["base_url"] = effective_base
2600
+ if provider_id in {"opencode-zen", "opencode-go"}:
2601
+ model["api_mode"] = opencode_model_api_mode(provider_id, selected)
2602
+ else:
2603
+ model.pop("api_mode", None)
2604
+ save_config(cfg)
2605
+ deactivate_provider()
2606
+
2607
+ print(f"Default model set to: {selected} (via {pconfig.name})")
2608
+ else:
2609
+ print("No change.")
2610
+
2611
+ def _model_flow_anthropic(config, current_model=""):
2612
+ """Flow for Anthropic provider — OAuth subscription, API key, or Claude Code creds."""
2613
+ from hermes_cli.main import _run_anthropic_oauth_flow
2614
+ from hermes_cli.auth import (
2615
+ _prompt_model_selection,
2616
+ _save_model_choice,
2617
+ deactivate_provider,
2618
+ )
2619
+ from hermes_cli.config import (
2620
+ save_env_value,
2621
+ load_config,
2622
+ save_config,
2623
+ save_anthropic_api_key,
2624
+ )
2625
+ from hermes_cli.models import _PROVIDER_MODELS
2626
+
2627
+ # Check ALL credential sources
2628
+ from hermes_cli.auth import get_anthropic_key
2629
+
2630
+ existing_key = get_anthropic_key()
2631
+ cc_available = False
2632
+ try:
2633
+ from agent.anthropic_adapter import (
2634
+ read_claude_code_credentials,
2635
+ is_claude_code_token_valid,
2636
+ _is_oauth_token,
2637
+ )
2638
+
2639
+ cc_creds = read_claude_code_credentials()
2640
+ if cc_creds and is_claude_code_token_valid(cc_creds):
2641
+ cc_available = True
2642
+ except Exception:
2643
+ pass
2644
+
2645
+ # Stale-OAuth guard: if the only existing cred is an expired OAuth token
2646
+ # (no valid cc_creds to fall back on), treat it as missing so the re-auth
2647
+ # path is offered instead of silently accepting a broken token.
2648
+ existing_is_stale_oauth = False
2649
+ if existing_key and _is_oauth_token(existing_key) and not cc_available:
2650
+ existing_is_stale_oauth = True
2651
+
2652
+ has_creds = (bool(existing_key) and not existing_is_stale_oauth) or cc_available
2653
+ needs_auth = not has_creds
2654
+
2655
+ if has_creds:
2656
+ # Show what we found
2657
+ if existing_key:
2658
+ from hermes_cli.env_loader import format_secret_source_suffix
2659
+ from hermes_cli.auth import PROVIDER_REGISTRY
2660
+
2661
+ # Surface which env var supplied the key so users with
2662
+ # Bitwarden see "(from Bitwarden)" — without this, a detected
2663
+ # BSM key looks identical to a key in .env and users assume
2664
+ # nothing is wired up.
2665
+ source_suffix = ""
2666
+ for var in PROVIDER_REGISTRY["anthropic"].api_key_env_vars:
2667
+ if os.getenv(var, "").strip() == existing_key:
2668
+ source_suffix = format_secret_source_suffix(var)
2669
+ if source_suffix:
2670
+ break
2671
+ print(
2672
+ f" Anthropic credentials: {existing_key[:12]}... ✓{source_suffix}"
2673
+ )
2674
+ elif cc_available:
2675
+ print(" Claude Code credentials: ✓ (auto-detected)")
2676
+ print()
2677
+ choice = _prompt_auth_credentials_choice("Anthropic credentials:")
2678
+
2679
+ if choice == "reauth":
2680
+ needs_auth = True
2681
+ elif choice == "cancel":
2682
+ return
2683
+ # choice == "use" or default: use existing, proceed to model selection
2684
+
2685
+ if needs_auth:
2686
+ # Show auth method choice
2687
+ print()
2688
+ print(" Choose authentication method:")
2689
+ print()
2690
+ print(" 1. Claude Pro/Max subscription (OAuth login)")
2691
+ print(" 2. Anthropic API key (pay-per-token)")
2692
+ print(" 3. Cancel")
2693
+ print()
2694
+ try:
2695
+ choice = input(" Choice [1/2/3]: ").strip()
2696
+ except (KeyboardInterrupt, EOFError):
2697
+ print()
2698
+ return
2699
+
2700
+ if choice == "1":
2701
+ if not _run_anthropic_oauth_flow(save_env_value):
2702
+ return
2703
+
2704
+ elif choice == "2":
2705
+ print()
2706
+ print(" Get an API key at: https://platform.claude.com/settings/keys")
2707
+ print()
2708
+ from hermes_cli.secret_prompt import masked_secret_prompt
2709
+
2710
+ try:
2711
+ api_key = masked_secret_prompt(" API key (sk-ant-...): ").strip()
2712
+ except (KeyboardInterrupt, EOFError):
2713
+ print()
2714
+ return
2715
+ if not api_key:
2716
+ print(" Cancelled.")
2717
+ return
2718
+ save_anthropic_api_key(api_key, save_fn=save_env_value)
2719
+ print(" ✓ API key saved.")
2720
+
2721
+ else:
2722
+ print(" No change.")
2723
+ return
2724
+ print()
2725
+
2726
+ # Model selection
2727
+ model_list = _PROVIDER_MODELS.get("anthropic", [])
2728
+ if model_list:
2729
+ selected = _prompt_model_selection(
2730
+ model_list,
2731
+ current_model=current_model,
2732
+ confirm_provider="anthropic",
2733
+ )
2734
+ else:
2735
+ try:
2736
+ selected = input("Model name (e.g., claude-sonnet-4-20250514): ").strip()
2737
+ except (KeyboardInterrupt, EOFError):
2738
+ selected = None
2739
+
2740
+ if selected:
2741
+ _save_model_choice(selected)
2742
+
2743
+ # Update config with provider — clear base_url since
2744
+ # resolve_runtime_provider() always hardcodes Anthropic's URL.
2745
+ # Leaving a stale base_url in config can contaminate other
2746
+ # providers if the user switches without running 'hermes model'.
2747
+ cfg = load_config()
2748
+ model = cfg.get("model")
2749
+ if not isinstance(model, dict):
2750
+ model = {"default": model} if model else {}
2751
+ cfg["model"] = model
2752
+ model["provider"] = "anthropic"
2753
+ model.pop("base_url", None)
2754
+ save_config(cfg)
2755
+ deactivate_provider()
2756
+
2757
+ print(f"Default model set to: {selected} (via Anthropic)")
2758
+ else:
2759
+ print("No change.")