@clawpump/claw-agent 0.1.5 → 0.1.6
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.
- package/agent/.dockerignore +67 -0
- package/agent/.envrc +1 -1
- package/agent/.gitattributes +8 -0
- package/agent/AGENTS.md +216 -4
- package/agent/CONTRIBUTING.md +46 -8
- package/agent/Dockerfile +78 -35
- package/agent/MANIFEST.in +2 -0
- package/agent/README.md +12 -5
- package/agent/README.ur-pk.md +261 -0
- package/agent/README.zh-CN.md +11 -8
- package/agent/SECURITY.md +5 -4
- package/agent/acp_adapter/provenance.py +127 -0
- package/agent/acp_adapter/server.py +112 -5
- package/agent/acp_adapter/session.py +1 -6
- package/agent/acp_registry/agent.json +2 -2
- package/agent/agent/account_usage.py +313 -1
- package/agent/agent/agent_init.py +140 -37
- package/agent/agent/agent_runtime_helpers.py +342 -83
- package/agent/agent/anthropic_adapter.py +320 -33
- package/agent/agent/auxiliary_client.py +525 -105
- package/agent/agent/background_review.py +157 -19
- package/agent/agent/bedrock_adapter.py +71 -6
- package/agent/agent/billing_view.py +295 -0
- package/agent/agent/chat_completion_helpers.py +229 -4
- package/agent/agent/codex_responses_adapter.py +86 -10
- package/agent/agent/codex_runtime.py +153 -1
- package/agent/agent/coding_context.py +738 -0
- package/agent/agent/context_compressor.py +392 -44
- package/agent/agent/context_references.py +34 -1
- package/agent/agent/conversation_compression.py +159 -22
- package/agent/agent/conversation_loop.py +643 -908
- package/agent/agent/copilot_acp_client.py +4 -11
- package/agent/agent/credential_pool.py +5 -3
- package/agent/agent/credits_tracker.py +794 -0
- package/agent/agent/curator.py +91 -18
- package/agent/agent/curator_backup.py +26 -10
- package/agent/agent/display.py +42 -1
- package/agent/agent/error_classifier.py +52 -3
- package/agent/agent/errors.py +3 -0
- package/agent/agent/file_safety.py +0 -17
- package/agent/agent/gemini_native_adapter.py +31 -1
- package/agent/agent/i18n.py +48 -4
- package/agent/agent/image_gen_provider.py +74 -5
- package/agent/agent/image_routing.py +29 -0
- package/agent/agent/insights.py +8 -17
- package/agent/agent/lsp/install.py +3 -0
- package/agent/agent/memory_manager.py +326 -31
- package/agent/agent/message_content.py +50 -0
- package/agent/agent/model_metadata.py +214 -3
- package/agent/agent/moonshot_schema.py +8 -1
- package/agent/agent/onboarding.py +60 -0
- package/agent/agent/prompt_builder.py +327 -37
- package/agent/agent/redact.py +1 -0
- package/agent/agent/runtime_cwd.py +34 -5
- package/agent/agent/secret_scope.py +205 -0
- package/agent/agent/secret_sources/bitwarden.py +34 -2
- package/agent/agent/skill_commands.py +90 -1
- package/agent/agent/skill_preprocessing.py +1 -0
- package/agent/agent/skill_utils.py +209 -36
- package/agent/agent/ssl_guard.py +94 -0
- package/agent/agent/system_prompt.py +133 -5
- package/agent/agent/tool_executor.py +496 -70
- package/agent/agent/transports/anthropic.py +83 -21
- package/agent/agent/transports/chat_completions.py +94 -5
- package/agent/agent/transports/codex.py +67 -2
- package/agent/agent/transports/codex_app_server.py +1 -0
- package/agent/agent/transports/codex_app_server_session.py +30 -0
- package/agent/agent/transports/types.py +12 -0
- package/agent/agent/turn_context.py +408 -0
- package/agent/agent/turn_finalizer.py +428 -0
- package/agent/agent/turn_retry_state.py +68 -0
- package/agent/agent/usage_pricing.py +3 -0
- package/agent/apps/bootstrap-installer/package.json +6 -5
- package/agent/apps/bootstrap-installer/src/routes/failure.tsx +12 -5
- package/agent/apps/bootstrap-installer/src/routes/progress.tsx +1 -3
- package/agent/apps/bootstrap-installer/src/store.ts +3 -2
- package/agent/apps/bootstrap-installer/src-tauri/src/bootstrap.rs +172 -7
- package/agent/apps/bootstrap-installer/src-tauri/src/events.rs +14 -1
- package/agent/apps/bootstrap-installer/src-tauri/src/paths.rs +29 -0
- package/agent/apps/bootstrap-installer/src-tauri/src/powershell.rs +93 -3
- package/agent/apps/bootstrap-installer/src-tauri/src/update.rs +695 -39
- package/agent/apps/bootstrap-installer/tsconfig.json +3 -4
- package/agent/apps/desktop/DESIGN.md +167 -0
- package/agent/apps/desktop/README.md +20 -16
- package/agent/apps/desktop/assets/icon.icns +0 -0
- package/agent/apps/desktop/assets/icon.ico +0 -0
- package/agent/apps/desktop/assets/icon.png +0 -0
- package/agent/apps/desktop/electron/backend-env.cjs +112 -0
- package/agent/apps/desktop/electron/backend-env.test.cjs +111 -0
- package/agent/apps/desktop/electron/backend-probes.test.cjs +3 -1
- package/agent/apps/desktop/electron/backend-ready.cjs +66 -0
- package/agent/apps/desktop/electron/bootstrap-platform.cjs +52 -0
- package/agent/apps/desktop/electron/bootstrap-platform.test.cjs +59 -1
- package/agent/apps/desktop/electron/bootstrap-runner.cjs +176 -38
- package/agent/apps/desktop/electron/bootstrap-runner.test.cjs +112 -1
- package/agent/apps/desktop/electron/connection-config.cjs +288 -0
- package/agent/apps/desktop/electron/connection-config.test.cjs +396 -0
- package/agent/apps/desktop/electron/dashboard-token.cjs +99 -0
- package/agent/apps/desktop/electron/dashboard-token.test.cjs +142 -0
- package/agent/apps/desktop/electron/desktop-uninstall.cjs +232 -0
- package/agent/apps/desktop/electron/desktop-uninstall.test.cjs +246 -0
- package/agent/apps/desktop/electron/entitlements.mac.inherit.plist +2 -0
- package/agent/apps/desktop/electron/fs-read-dir.cjs +109 -0
- package/agent/apps/desktop/electron/fs-read-dir.test.cjs +364 -0
- package/agent/apps/desktop/electron/gateway-ws-probe.cjs +188 -0
- package/agent/apps/desktop/electron/gateway-ws-probe.test.cjs +122 -0
- package/agent/apps/desktop/electron/git-root.cjs +54 -0
- package/agent/apps/desktop/electron/git-root.test.cjs +40 -0
- package/agent/apps/desktop/electron/git-worktrees.cjs +174 -0
- package/agent/apps/desktop/electron/hardening.cjs +123 -28
- package/agent/apps/desktop/electron/hardening.test.cjs +163 -0
- package/agent/apps/desktop/electron/main.cjs +3121 -331
- package/agent/apps/desktop/electron/oauth-net-request.cjs +20 -0
- package/agent/apps/desktop/electron/oauth-net-request.test.cjs +34 -0
- package/agent/apps/desktop/electron/preload.cjs +52 -2
- package/agent/apps/desktop/electron/session-windows.cjs +124 -0
- package/agent/apps/desktop/electron/session-windows.test.cjs +199 -0
- package/agent/apps/desktop/electron/update-rebuild.cjs +29 -0
- package/agent/apps/desktop/electron/update-rebuild.test.cjs +55 -0
- package/agent/apps/desktop/electron/update-remote.cjs +56 -0
- package/agent/apps/desktop/electron/update-remote.test.cjs +78 -0
- package/agent/apps/desktop/electron/vscode-marketplace.cjs +331 -0
- package/agent/apps/desktop/electron/vscode-marketplace.test.cjs +113 -0
- package/agent/apps/desktop/electron/windows-child-process.test.cjs +57 -0
- package/agent/apps/desktop/electron/windows-user-env.cjs +76 -0
- package/agent/apps/desktop/electron/windows-user-env.test.cjs +90 -0
- package/agent/apps/desktop/electron/workspace-cwd.cjs +38 -0
- package/agent/apps/desktop/electron/workspace-cwd.test.cjs +45 -0
- package/agent/apps/desktop/eslint.config.mjs +0 -3
- package/agent/apps/desktop/index.html +27 -2
- package/agent/apps/desktop/package.json +31 -11
- package/agent/apps/desktop/pr-assets/session-source-folders.png +0 -0
- package/agent/apps/desktop/public/apple-touch-icon.png +0 -0
- package/agent/apps/desktop/public/nous-girl.jpg +0 -0
- package/agent/apps/desktop/scripts/assert-dist-built.cjs +70 -0
- package/agent/apps/desktop/scripts/assert-dist-built.test.cjs +84 -0
- package/agent/apps/desktop/scripts/before-pack.cjs +78 -0
- package/agent/apps/desktop/scripts/before-pack.test.cjs +53 -0
- package/agent/apps/desktop/scripts/diag-scroll-reset.mjs +229 -0
- package/agent/apps/desktop/scripts/patch-electron-builder-mac-binary.cjs +64 -0
- package/agent/apps/desktop/scripts/run-electron-builder.cjs +57 -0
- package/agent/apps/desktop/src/app/agents/index.tsx +53 -45
- package/agent/apps/desktop/src/app/artifacts/index.tsx +102 -83
- package/agent/apps/desktop/src/app/chat/chat-drop-overlay.tsx +29 -8
- package/agent/apps/desktop/src/app/chat/chat-swap-overlay.tsx +47 -0
- package/agent/apps/desktop/src/app/chat/composer/attachments.tsx +81 -45
- package/agent/apps/desktop/src/app/chat/composer/completion-drawer.tsx +13 -24
- package/agent/apps/desktop/src/app/chat/composer/context-menu.tsx +138 -88
- package/agent/apps/desktop/src/app/chat/composer/controls.tsx +138 -90
- package/agent/apps/desktop/src/app/chat/composer/enter-submit-dom-race.test.tsx +218 -0
- package/agent/apps/desktop/src/app/chat/composer/focus.ts +32 -0
- package/agent/apps/desktop/src/app/chat/composer/help-hint.tsx +38 -25
- package/agent/apps/desktop/src/app/chat/composer/hooks/use-live-completion-adapter.ts +7 -0
- package/agent/apps/desktop/src/app/chat/composer/hooks/use-mic-recorder.ts +22 -12
- package/agent/apps/desktop/src/app/chat/composer/hooks/use-slash-completions.ts +142 -14
- package/agent/apps/desktop/src/app/chat/composer/hooks/use-voice-conversation.ts +14 -11
- package/agent/apps/desktop/src/app/chat/composer/hooks/use-voice-recorder.ts +9 -6
- package/agent/apps/desktop/src/app/chat/composer/ime-composition-dom-repro.test.tsx +108 -0
- package/agent/apps/desktop/src/app/chat/composer/index.tsx +930 -180
- package/agent/apps/desktop/src/app/chat/composer/inline-refs.ts +136 -32
- package/agent/apps/desktop/src/app/chat/composer/model-pill.tsx +86 -0
- package/agent/apps/desktop/src/app/chat/composer/queue-panel.tsx +54 -75
- package/agent/apps/desktop/src/app/chat/composer/rich-editor.test.ts +117 -1
- package/agent/apps/desktop/src/app/chat/composer/rich-editor.ts +117 -6
- package/agent/apps/desktop/src/app/chat/composer/slash-nav-dom-repro.test.tsx +186 -0
- package/agent/apps/desktop/src/app/chat/composer/status-stack/index.tsx +202 -0
- package/agent/apps/desktop/src/app/chat/composer/status-stack/status-row.tsx +155 -0
- package/agent/apps/desktop/src/app/chat/composer/text-utils.test.ts +104 -0
- package/agent/apps/desktop/src/app/chat/composer/text-utils.ts +37 -9
- package/agent/apps/desktop/src/app/chat/composer/trigger-popover.test.tsx +50 -0
- package/agent/apps/desktop/src/app/chat/composer/trigger-popover.tsx +105 -40
- package/agent/apps/desktop/src/app/chat/composer/types.ts +5 -0
- package/agent/apps/desktop/src/app/chat/composer/url-dialog.tsx +11 -15
- package/agent/apps/desktop/src/app/chat/composer/voice-activity.tsx +8 -4
- package/agent/apps/desktop/src/app/chat/hooks/use-composer-actions.test.ts +57 -0
- package/agent/apps/desktop/src/app/chat/hooks/use-composer-actions.ts +70 -16
- package/agent/apps/desktop/src/app/chat/hooks/use-file-drop-zone.ts +52 -16
- package/agent/apps/desktop/src/app/chat/index.tsx +234 -81
- package/agent/apps/desktop/src/app/chat/perf-probe.tsx +69 -21
- package/agent/apps/desktop/src/app/chat/right-rail/preview-console.tsx +44 -40
- package/agent/apps/desktop/src/app/chat/right-rail/preview-file.tsx +71 -25
- package/agent/apps/desktop/src/app/chat/right-rail/preview-pane.test.tsx +40 -1
- package/agent/apps/desktop/src/app/chat/right-rail/preview-pane.tsx +55 -53
- package/agent/apps/desktop/src/app/chat/right-rail/preview.tsx +35 -17
- package/agent/apps/desktop/src/app/chat/scroll-to-bottom-button.test.tsx +67 -0
- package/agent/apps/desktop/src/app/chat/scroll-to-bottom-button.tsx +74 -0
- package/agent/apps/desktop/src/app/chat/sidebar/cron-jobs-section.tsx +356 -0
- package/agent/apps/desktop/src/app/chat/sidebar/index.tsx +1189 -364
- package/agent/apps/desktop/src/app/chat/sidebar/load-more-row.tsx +30 -0
- package/agent/apps/desktop/src/app/chat/sidebar/order.test.ts +21 -0
- package/agent/apps/desktop/src/app/chat/sidebar/order.ts +17 -0
- package/agent/apps/desktop/src/app/chat/sidebar/profile-switcher.tsx +524 -0
- package/agent/apps/desktop/src/app/chat/sidebar/session-actions-menu.tsx +80 -45
- package/agent/apps/desktop/src/app/chat/sidebar/session-row.tsx +120 -25
- package/agent/apps/desktop/src/app/chat/sidebar/virtual-session-list.tsx +7 -13
- package/agent/apps/desktop/src/app/chat/sidebar/workspace-groups.test.ts +149 -0
- package/agent/apps/desktop/src/app/chat/sidebar/workspace-groups.ts +326 -0
- package/agent/apps/desktop/src/app/chat/thread-loading.ts +7 -2
- package/agent/apps/desktop/src/app/command-center/index.tsx +320 -581
- package/agent/apps/desktop/src/app/command-palette/index.tsx +681 -0
- package/agent/apps/desktop/src/app/command-palette/marketplace-theme-page.tsx +157 -0
- package/agent/apps/desktop/src/app/cron/index.tsx +392 -324
- package/agent/apps/desktop/src/app/cron/job-state.ts +29 -0
- package/agent/apps/desktop/src/app/desktop-controller.tsx +618 -123
- package/agent/apps/desktop/src/app/floating-hud.ts +22 -0
- package/agent/apps/desktop/src/app/gateway/hooks/use-gateway-boot.test.tsx +265 -0
- package/agent/apps/desktop/src/app/gateway/hooks/use-gateway-boot.ts +260 -14
- package/agent/apps/desktop/src/app/gateway/hooks/use-gateway-request.ts +48 -4
- package/agent/apps/desktop/src/app/hooks/use-keybinds.ts +270 -0
- package/agent/apps/desktop/src/app/hooks/use-refresh-hotkey.ts +45 -0
- package/agent/apps/desktop/src/app/layout-constants.ts +19 -0
- package/agent/apps/desktop/src/app/messaging/index.tsx +136 -241
- package/agent/apps/desktop/src/app/messaging/platform-icon.tsx +95 -0
- package/agent/apps/desktop/src/app/model-visibility-overlay.tsx +31 -0
- package/agent/apps/desktop/src/app/overlays/overlay-search-input.tsx +18 -62
- package/agent/apps/desktop/src/app/overlays/overlay-split-layout.tsx +59 -7
- package/agent/apps/desktop/src/app/overlays/overlay-view.tsx +9 -5
- package/agent/apps/desktop/src/app/page-search-shell.tsx +42 -20
- package/agent/apps/desktop/src/app/profiles/create-profile-dialog.tsx +165 -0
- package/agent/apps/desktop/src/app/profiles/delete-profile-dialog.tsx +65 -0
- package/agent/apps/desktop/src/app/profiles/index.tsx +174 -199
- package/agent/apps/desktop/src/app/profiles/rename-profile-dialog.tsx +125 -0
- package/agent/apps/desktop/src/app/right-sidebar/files/dnd-manager.ts +27 -0
- package/agent/apps/desktop/src/app/right-sidebar/files/ipc.test.ts +100 -0
- package/agent/apps/desktop/src/app/right-sidebar/files/ipc.ts +12 -18
- package/agent/apps/desktop/src/app/right-sidebar/files/remote-picker.tsx +177 -0
- package/agent/apps/desktop/src/app/right-sidebar/files/tree.tsx +35 -21
- package/agent/apps/desktop/src/app/right-sidebar/files/use-project-tree.test.ts +75 -3
- package/agent/apps/desktop/src/app/right-sidebar/files/use-project-tree.ts +152 -5
- package/agent/apps/desktop/src/app/right-sidebar/index.test.tsx +75 -0
- package/agent/apps/desktop/src/app/right-sidebar/index.tsx +166 -129
- package/agent/apps/desktop/src/app/right-sidebar/store.ts +19 -4
- package/agent/apps/desktop/src/app/right-sidebar/terminal/buffer.ts +65 -0
- package/agent/apps/desktop/src/app/right-sidebar/terminal/index.tsx +29 -34
- package/agent/apps/desktop/src/app/right-sidebar/terminal/persistent.tsx +18 -6
- package/agent/apps/desktop/src/app/right-sidebar/terminal/selection.ts +93 -32
- package/agent/apps/desktop/src/app/right-sidebar/terminal/use-terminal-session.ts +381 -119
- package/agent/apps/desktop/src/app/routes.ts +9 -0
- package/agent/apps/desktop/src/app/session/hooks/use-cwd-actions.ts +17 -7
- package/agent/apps/desktop/src/app/session/hooks/use-message-stream.ts +365 -47
- package/agent/apps/desktop/src/app/session/hooks/use-model-controls.test.tsx +198 -0
- package/agent/apps/desktop/src/app/session/hooks/use-model-controls.ts +70 -34
- package/agent/apps/desktop/src/app/session/hooks/use-prompt-actions.test.tsx +1061 -0
- package/agent/apps/desktop/src/app/session/hooks/use-prompt-actions.ts +1143 -165
- package/agent/apps/desktop/src/app/session/hooks/use-route-resume.test.tsx +341 -2
- package/agent/apps/desktop/src/app/session/hooks/use-route-resume.ts +176 -5
- package/agent/apps/desktop/src/app/session/hooks/use-session-actions.test.tsx +259 -0
- package/agent/apps/desktop/src/app/session/hooks/use-session-actions.ts +452 -149
- package/agent/apps/desktop/src/app/session/hooks/use-session-state-cache.test.tsx +327 -0
- package/agent/apps/desktop/src/app/session/hooks/use-session-state-cache.ts +133 -4
- package/agent/apps/desktop/src/app/session-picker-overlay.tsx +32 -0
- package/agent/apps/desktop/src/app/session-switcher.tsx +107 -0
- package/agent/apps/desktop/src/app/settings/about-settings.tsx +45 -36
- package/agent/apps/desktop/src/app/settings/appearance-settings.tsx +243 -162
- package/agent/apps/desktop/src/app/settings/config-settings.tsx +86 -66
- package/agent/apps/desktop/src/app/settings/constants.ts +459 -122
- package/agent/apps/desktop/src/app/settings/credential-key-ui.tsx +373 -0
- package/agent/apps/desktop/src/app/settings/env-credentials.tsx +198 -0
- package/agent/apps/desktop/src/app/settings/env-var-actions-menu.tsx +136 -0
- package/agent/apps/desktop/src/app/settings/field-copy.ts +56 -0
- package/agent/apps/desktop/src/app/settings/gateway-settings.tsx +385 -72
- package/agent/apps/desktop/src/app/settings/helpers.test.ts +156 -1
- package/agent/apps/desktop/src/app/settings/helpers.ts +30 -2
- package/agent/apps/desktop/src/app/settings/index.tsx +118 -84
- package/agent/apps/desktop/src/app/settings/keys-settings.tsx +62 -419
- package/agent/apps/desktop/src/app/settings/mcp-settings.tsx +65 -60
- package/agent/apps/desktop/src/app/settings/model-settings.test.tsx +129 -5
- package/agent/apps/desktop/src/app/settings/model-settings.tsx +370 -65
- package/agent/apps/desktop/src/app/settings/notifications-settings.tsx +150 -0
- package/agent/apps/desktop/src/app/settings/primitives.tsx +5 -11
- package/agent/apps/desktop/src/app/settings/provider-config-panel.test.tsx +142 -0
- package/agent/apps/desktop/src/app/settings/provider-config-panel.tsx +182 -0
- package/agent/apps/desktop/src/app/settings/providers-settings.test.tsx +171 -0
- package/agent/apps/desktop/src/app/settings/providers-settings.tsx +471 -0
- package/agent/apps/desktop/src/app/settings/sessions-settings.tsx +183 -71
- package/agent/apps/desktop/src/app/settings/toolset-config-panel.test.tsx +135 -1
- package/agent/apps/desktop/src/app/settings/toolset-config-panel.tsx +180 -57
- package/agent/apps/desktop/src/app/settings/types.ts +9 -6
- package/agent/apps/desktop/src/app/settings/uninstall-section.tsx +185 -0
- package/agent/apps/desktop/src/app/settings/use-deep-link-highlight.ts +60 -0
- package/agent/apps/desktop/src/app/shell/app-shell.tsx +59 -13
- package/agent/apps/desktop/src/app/shell/gateway-menu-panel.tsx +37 -32
- package/agent/apps/desktop/src/app/shell/hooks/use-overlay-routing.ts +6 -3
- package/agent/apps/desktop/src/app/shell/hooks/use-statusbar-items.tsx +212 -53
- package/agent/apps/desktop/src/app/shell/keybind-panel.tsx +215 -0
- package/agent/apps/desktop/src/app/shell/model-edit-submenu.test.tsx +84 -0
- package/agent/apps/desktop/src/app/shell/model-edit-submenu.tsx +244 -0
- package/agent/apps/desktop/src/app/shell/model-menu-panel.tsx +392 -0
- package/agent/apps/desktop/src/app/shell/statusbar-controls.tsx +23 -33
- package/agent/apps/desktop/src/app/shell/titlebar-controls.tsx +79 -95
- package/agent/apps/desktop/src/app/shell/titlebar.ts +8 -2
- package/agent/apps/desktop/src/app/skills/index.test.tsx +11 -0
- package/agent/apps/desktop/src/app/skills/index.tsx +79 -64
- package/agent/apps/desktop/src/app/types.ts +85 -0
- package/agent/apps/desktop/src/app/updates-overlay.tsx +110 -105
- package/agent/apps/desktop/src/components/assistant-ui/ansi-text.tsx +34 -0
- package/agent/apps/desktop/src/components/assistant-ui/block-direction.test.tsx +129 -0
- package/agent/apps/desktop/src/components/assistant-ui/clarify-tool.tsx +102 -81
- package/agent/apps/desktop/src/components/assistant-ui/directive-text.tsx +92 -15
- package/agent/apps/desktop/src/components/assistant-ui/markdown-text.test.ts +38 -0
- package/agent/apps/desktop/src/components/assistant-ui/markdown-text.tsx +304 -45
- package/agent/apps/desktop/src/components/assistant-ui/message-render-boundary.test.tsx +80 -0
- package/agent/apps/desktop/src/components/assistant-ui/message-render-boundary.tsx +48 -0
- package/agent/apps/desktop/src/components/assistant-ui/streaming.test.tsx +142 -90
- package/agent/apps/desktop/src/components/assistant-ui/thread-list.tsx +337 -0
- package/agent/apps/desktop/src/components/assistant-ui/thread.tsx +667 -190
- package/agent/apps/desktop/src/components/assistant-ui/tool-approval-group.test.tsx +299 -0
- package/agent/apps/desktop/src/components/assistant-ui/tool-approval.test.tsx +133 -0
- package/agent/apps/desktop/src/components/assistant-ui/tool-approval.tsx +239 -0
- package/agent/apps/desktop/src/components/assistant-ui/tool-fallback-model.test.ts +31 -0
- package/agent/apps/desktop/src/components/assistant-ui/tool-fallback-model.ts +152 -134
- package/agent/apps/desktop/src/components/assistant-ui/tool-fallback.tsx +142 -150
- package/agent/apps/desktop/src/components/assistant-ui/tooltip-icon-button.tsx +14 -12
- package/agent/apps/desktop/src/components/assistant-ui/user-message-edit.test.tsx +141 -0
- package/agent/apps/desktop/src/components/assistant-ui/user-message-text.tsx +152 -0
- package/agent/apps/desktop/src/components/boot-failure-overlay.tsx +150 -33
- package/agent/apps/desktop/src/components/boot-failure-reauth.test.ts +100 -0
- package/agent/apps/desktop/src/components/boot-failure-reauth.ts +81 -0
- package/agent/apps/desktop/src/components/brand-mark.tsx +19 -0
- package/agent/apps/desktop/src/components/chat/code-card.tsx +1 -1
- package/agent/apps/desktop/src/components/chat/composer-dock.ts +31 -0
- package/agent/apps/desktop/src/components/chat/diff-lines.tsx +1 -1
- package/agent/apps/desktop/src/components/chat/disclosure-row.tsx +13 -3
- package/agent/apps/desktop/src/components/chat/expandable-block.tsx +52 -0
- package/agent/apps/desktop/src/components/chat/generated-image-result.tsx +174 -0
- package/agent/apps/desktop/src/components/chat/image-generation-placeholder.tsx +70 -37
- package/agent/apps/desktop/src/components/chat/intro.tsx +8 -7
- package/agent/apps/desktop/src/components/chat/preview-attachment.tsx +4 -2
- package/agent/apps/desktop/src/components/chat/shiki-highlighter.test.ts +37 -0
- package/agent/apps/desktop/src/components/chat/shiki-highlighter.tsx +96 -22
- package/agent/apps/desktop/src/components/chat/status-row.tsx +70 -0
- package/agent/apps/desktop/src/components/chat/status-section.tsx +42 -0
- package/agent/apps/desktop/src/components/chat/terminal-output.tsx +54 -0
- package/agent/apps/desktop/src/components/chat/zoomable-image.tsx +70 -109
- package/agent/apps/desktop/src/components/desktop-install-overlay.tsx +154 -84
- package/agent/apps/desktop/src/components/desktop-onboarding-overlay.test.tsx +38 -8
- package/agent/apps/desktop/src/components/desktop-onboarding-overlay.tsx +789 -233
- package/agent/apps/desktop/src/components/error-boundary.tsx +77 -0
- package/agent/apps/desktop/src/components/gateway-connecting-overlay.test.tsx +144 -0
- package/agent/apps/desktop/src/components/gateway-connecting-overlay.tsx +7 -1
- package/agent/apps/desktop/src/components/haptics-provider.tsx +24 -0
- package/agent/apps/desktop/src/components/language-switcher.test.tsx +53 -0
- package/agent/apps/desktop/src/components/language-switcher.tsx +175 -0
- package/agent/apps/desktop/src/components/model-picker.tsx +42 -40
- package/agent/apps/desktop/src/components/model-visibility-dialog.tsx +166 -0
- package/agent/apps/desktop/src/components/notifications.tsx +48 -27
- package/agent/apps/desktop/src/components/pane-shell/index.ts +1 -1
- package/agent/apps/desktop/src/components/pane-shell/pane-shell.tsx +146 -9
- package/agent/apps/desktop/src/components/prompt-overlays.tsx +234 -0
- package/agent/apps/desktop/src/components/session-picker.tsx +108 -0
- package/agent/apps/desktop/src/components/ui/action-status.tsx +25 -0
- package/agent/apps/desktop/src/components/ui/badge.tsx +35 -0
- package/agent/apps/desktop/src/components/ui/button.tsx +37 -13
- package/agent/apps/desktop/src/components/ui/confirm-dialog.tsx +109 -0
- package/agent/apps/desktop/src/components/ui/control.ts +25 -0
- package/agent/apps/desktop/src/components/ui/copy-button.test.tsx +36 -0
- package/agent/apps/desktop/src/components/ui/copy-button.tsx +38 -27
- package/agent/apps/desktop/src/components/ui/dialog.tsx +39 -11
- package/agent/apps/desktop/src/components/ui/dropdown-menu.tsx +98 -24
- package/agent/apps/desktop/src/components/ui/error-state.tsx +50 -0
- package/agent/apps/desktop/src/components/ui/fade-text.tsx +9 -2
- package/agent/apps/desktop/src/components/ui/{braille-spinner.tsx → glyph-spinner.tsx} +15 -13
- package/agent/apps/desktop/src/components/ui/input.tsx +5 -2
- package/agent/apps/desktop/src/components/ui/kbd.tsx +83 -12
- package/agent/apps/desktop/src/components/ui/log-view.tsx +19 -0
- package/agent/apps/desktop/src/components/ui/pagination.tsx +12 -5
- package/agent/apps/desktop/src/components/ui/popover.tsx +44 -0
- package/agent/apps/desktop/src/components/ui/search-field.tsx +80 -0
- package/agent/apps/desktop/src/components/ui/segmented-control.tsx +51 -0
- package/agent/apps/desktop/src/components/ui/select.tsx +10 -3
- package/agent/apps/desktop/src/components/ui/sheet.tsx +8 -2
- package/agent/apps/desktop/src/components/ui/sidebar.tsx +18 -25
- package/agent/apps/desktop/src/components/ui/switch.tsx +38 -15
- package/agent/apps/desktop/src/components/ui/textarea.tsx +4 -11
- package/agent/apps/desktop/src/components/ui/tool-icon.tsx +65 -0
- package/agent/apps/desktop/src/components/ui/tooltip.tsx +31 -4
- package/agent/apps/desktop/src/fonts/JetBrainsMono-Bold.woff2 +0 -0
- package/agent/apps/desktop/src/fonts/JetBrainsMono-Italic.woff2 +0 -0
- package/agent/apps/desktop/src/fonts/JetBrainsMono-Regular.woff2 +0 -0
- package/agent/apps/desktop/src/global.d.ts +181 -4
- package/agent/apps/desktop/src/hermes.test.ts +60 -0
- package/agent/apps/desktop/src/hermes.ts +190 -13
- package/agent/apps/desktop/src/hooks/use-image-download.ts +85 -0
- package/agent/apps/desktop/src/hooks/use-resize-observer.ts +13 -4
- package/agent/apps/desktop/src/hooks/use-worktree-info.ts +68 -0
- package/agent/apps/desktop/src/i18n/catalog.ts +12 -0
- package/agent/apps/desktop/src/i18n/context.test.tsx +232 -0
- package/agent/apps/desktop/src/i18n/context.tsx +183 -0
- package/agent/apps/desktop/src/i18n/define-locale.ts +41 -0
- package/agent/apps/desktop/src/i18n/en.ts +1921 -0
- package/agent/apps/desktop/src/i18n/index.ts +20 -0
- package/agent/apps/desktop/src/i18n/ja.ts +2053 -0
- package/agent/apps/desktop/src/i18n/languages.test.ts +43 -0
- package/agent/apps/desktop/src/i18n/languages.ts +86 -0
- package/agent/apps/desktop/src/i18n/runtime.test.ts +75 -0
- package/agent/apps/desktop/src/i18n/runtime.ts +53 -0
- package/agent/apps/desktop/src/i18n/types.ts +1559 -0
- package/agent/apps/desktop/src/i18n/zh-hant.ts +1992 -0
- package/agent/apps/desktop/src/i18n/zh.ts +2099 -0
- package/agent/apps/desktop/src/lib/ansi.test.ts +123 -0
- package/agent/apps/desktop/src/lib/ansi.ts +186 -0
- package/agent/apps/desktop/src/lib/chat-messages.test.ts +79 -0
- package/agent/apps/desktop/src/lib/chat-messages.ts +68 -29
- package/agent/apps/desktop/src/lib/chat-runtime.test.ts +65 -1
- package/agent/apps/desktop/src/lib/chat-runtime.ts +39 -3
- package/agent/apps/desktop/src/lib/completion-sound.ts +519 -0
- package/agent/apps/desktop/src/lib/desktop-fs.test.ts +116 -0
- package/agent/apps/desktop/src/lib/desktop-fs.ts +113 -0
- package/agent/apps/desktop/src/lib/desktop-slash-commands.test.ts +89 -6
- package/agent/apps/desktop/src/lib/desktop-slash-commands.ts +270 -131
- package/agent/apps/desktop/src/lib/external-link.test.tsx +27 -0
- package/agent/apps/desktop/src/lib/external-link.tsx +9 -2
- package/agent/apps/desktop/src/lib/gateway-events.test.ts +27 -0
- package/agent/apps/desktop/src/lib/gateway-events.ts +16 -0
- package/agent/apps/desktop/src/lib/gateway-ws-url.test.ts +78 -0
- package/agent/apps/desktop/src/lib/gateway-ws-url.ts +91 -0
- package/agent/apps/desktop/src/lib/generated-images.test.ts +97 -0
- package/agent/apps/desktop/src/lib/generated-images.ts +116 -0
- package/agent/apps/desktop/src/lib/haptics.ts +17 -0
- package/agent/apps/desktop/src/lib/icons.ts +10 -2
- package/agent/apps/desktop/src/lib/keybinds/actions.ts +137 -0
- package/agent/apps/desktop/src/lib/keybinds/combo.test.ts +86 -0
- package/agent/apps/desktop/src/lib/keybinds/combo.ts +195 -0
- package/agent/apps/desktop/src/lib/local-preview.ts +23 -2
- package/agent/apps/desktop/src/lib/markdown-preprocess.ts +20 -7
- package/agent/apps/desktop/src/lib/media.remote.test.ts +90 -0
- package/agent/apps/desktop/src/lib/media.ts +40 -1
- package/agent/apps/desktop/src/lib/model-status-label.test.ts +59 -0
- package/agent/apps/desktop/src/lib/model-status-label.ts +122 -0
- package/agent/apps/desktop/src/lib/mutable-ref.ts +6 -0
- package/agent/apps/desktop/src/lib/profile-color.ts +58 -0
- package/agent/apps/desktop/src/lib/query-client.ts +13 -0
- package/agent/apps/desktop/src/lib/remend-tail.test.ts +105 -0
- package/agent/apps/desktop/src/lib/remend-tail.ts +108 -0
- package/agent/apps/desktop/src/lib/session-export.ts +6 -3
- package/agent/apps/desktop/src/lib/session-ids.test.ts +44 -0
- package/agent/apps/desktop/src/lib/session-ids.ts +26 -0
- package/agent/apps/desktop/src/lib/session-search.test.ts +66 -0
- package/agent/apps/desktop/src/lib/session-search.ts +21 -0
- package/agent/apps/desktop/src/lib/session-source.ts +126 -0
- package/agent/apps/desktop/src/lib/storage.test.ts +25 -0
- package/agent/apps/desktop/src/lib/storage.ts +35 -1
- package/agent/apps/desktop/src/lib/todos.test.ts +46 -1
- package/agent/apps/desktop/src/lib/todos.ts +37 -0
- package/agent/apps/desktop/src/lib/tool-result-summary.ts +5 -1
- package/agent/apps/desktop/src/lib/update-copy.test.ts +38 -0
- package/agent/apps/desktop/src/lib/update-copy.ts +44 -0
- package/agent/apps/desktop/src/lib/use-enter-animation.ts +2 -2
- package/agent/apps/desktop/src/lib/yolo-session.ts +50 -0
- package/agent/apps/desktop/src/main.tsx +19 -19
- package/agent/apps/desktop/src/store/boot.ts +4 -3
- package/agent/apps/desktop/src/store/clarify.test.ts +81 -0
- package/agent/apps/desktop/src/store/clarify.ts +50 -13
- package/agent/apps/desktop/src/store/command-palette.ts +20 -0
- package/agent/apps/desktop/src/store/compaction.test.ts +53 -0
- package/agent/apps/desktop/src/store/compaction.ts +38 -0
- package/agent/apps/desktop/src/store/completion-sound.ts +32 -0
- package/agent/apps/desktop/src/store/composer-input-history.test.ts +147 -0
- package/agent/apps/desktop/src/store/composer-input-history.ts +158 -0
- package/agent/apps/desktop/src/store/composer-queue.test.ts +68 -0
- package/agent/apps/desktop/src/store/composer-queue.ts +76 -0
- package/agent/apps/desktop/src/store/composer-status.test.ts +99 -0
- package/agent/apps/desktop/src/store/composer-status.ts +277 -0
- package/agent/apps/desktop/src/store/composer.test.ts +106 -0
- package/agent/apps/desktop/src/store/composer.ts +116 -0
- package/agent/apps/desktop/src/store/cron.ts +19 -0
- package/agent/apps/desktop/src/store/gateway.ts +280 -6
- package/agent/apps/desktop/src/store/keybinds.ts +143 -0
- package/agent/apps/desktop/src/store/layout.ts +107 -9
- package/agent/apps/desktop/src/store/model-presets.test.ts +51 -0
- package/agent/apps/desktop/src/store/model-presets.ts +86 -0
- package/agent/apps/desktop/src/store/model-visibility.test.ts +99 -0
- package/agent/apps/desktop/src/store/model-visibility.ts +161 -0
- package/agent/apps/desktop/src/store/native-notifications.test.ts +192 -0
- package/agent/apps/desktop/src/store/native-notifications.ts +203 -0
- package/agent/apps/desktop/src/store/notifications.ts +10 -7
- package/agent/apps/desktop/src/store/onboarding.test.ts +271 -1
- package/agent/apps/desktop/src/store/onboarding.ts +268 -38
- package/agent/apps/desktop/src/store/preview.ts +10 -1
- package/agent/apps/desktop/src/store/profile.test.ts +89 -0
- package/agent/apps/desktop/src/store/profile.ts +395 -0
- package/agent/apps/desktop/src/store/prompts.test.ts +127 -0
- package/agent/apps/desktop/src/store/prompts.ts +117 -0
- package/agent/apps/desktop/src/store/session-switcher.test.ts +115 -0
- package/agent/apps/desktop/src/store/session-switcher.ts +128 -0
- package/agent/apps/desktop/src/store/session-sync.ts +25 -0
- package/agent/apps/desktop/src/store/session.test.ts +268 -2
- package/agent/apps/desktop/src/store/session.ts +392 -18
- package/agent/apps/desktop/src/store/subagents.ts +3 -0
- package/agent/apps/desktop/src/store/system-actions.ts +48 -0
- package/agent/apps/desktop/src/store/thread-scroll.ts +58 -5
- package/agent/apps/desktop/src/store/todos.test.ts +47 -0
- package/agent/apps/desktop/src/store/todos.ts +64 -0
- package/agent/apps/desktop/src/store/tool-dismiss.ts +45 -0
- package/agent/apps/desktop/src/store/translucency.ts +38 -0
- package/agent/apps/desktop/src/store/updates.test.ts +187 -2
- package/agent/apps/desktop/src/store/updates.ts +268 -18
- package/agent/apps/desktop/src/store/windows.test.ts +143 -0
- package/agent/apps/desktop/src/store/windows.ts +115 -0
- package/agent/apps/desktop/src/styles.css +510 -119
- package/agent/apps/desktop/src/themes/color.ts +142 -0
- package/agent/apps/desktop/src/themes/context.tsx +128 -75
- package/agent/apps/desktop/src/themes/install.test.ts +119 -0
- package/agent/apps/desktop/src/themes/install.ts +95 -0
- package/agent/apps/desktop/src/themes/presets.test.ts +33 -0
- package/agent/apps/desktop/src/themes/presets.ts +13 -4
- package/agent/apps/desktop/src/themes/profile-theme.test.ts +41 -0
- package/agent/apps/desktop/src/themes/types.ts +35 -0
- package/agent/apps/desktop/src/themes/user-themes.test.ts +63 -0
- package/agent/apps/desktop/src/themes/user-themes.ts +122 -0
- package/agent/apps/desktop/src/themes/vscode.test.ts +171 -0
- package/agent/apps/desktop/src/themes/vscode.ts +343 -0
- package/agent/apps/desktop/src/types/hermes.ts +138 -1
- package/agent/apps/desktop/tsconfig.json +2 -2
- package/agent/apps/desktop/vite.config.ts +18 -0
- package/agent/apps/shared/package.json +1 -1
- package/agent/apps/shared/src/json-rpc-gateway.ts +63 -2
- package/agent/apps/shared/tsconfig.json +2 -2
- package/agent/cli-config.yaml.example +78 -1
- package/agent/cli.py +2177 -3162
- package/agent/cron/blueprint_catalog.py +713 -0
- package/agent/cron/jobs.py +226 -110
- package/agent/cron/scheduler.py +468 -193
- package/agent/cron/scheduler_provider.py +177 -0
- package/agent/cron/scripts/__init__.py +1 -0
- package/agent/cron/scripts/classify_items.py +226 -0
- package/agent/cron/suggestion_catalog.py +154 -0
- package/agent/cron/suggestions.py +257 -0
- package/agent/docs/chronos-managed-cron-contract.md +196 -0
- package/agent/docs/design/profile-builder.md +146 -0
- package/agent/docs/middleware/README.md +260 -0
- package/agent/docs/observability/README.md +316 -0
- package/agent/docs/plans/2026-06-09-003-fix-telegram-stream-overflow-continuations-plan.md +240 -0
- package/agent/docs/rca-ssl-cacert-post-git-pull.md +54 -0
- package/agent/docs/relay-connector-contract.md +285 -0
- package/agent/gateway/authz_mixin.py +536 -0
- package/agent/gateway/channel_directory.py +65 -3
- package/agent/gateway/config.py +222 -12
- package/agent/gateway/display_config.py +10 -0
- package/agent/gateway/hooks.py +17 -0
- package/agent/gateway/kanban_watchers.py +1146 -0
- package/agent/gateway/message_timestamps.py +166 -0
- package/agent/gateway/platforms/ADDING_A_PLATFORM.md +29 -0
- package/agent/gateway/platforms/api_server.py +216 -38
- package/agent/gateway/platforms/base.py +210 -58
- package/agent/gateway/platforms/email.py +122 -12
- package/agent/gateway/platforms/feishu.py +80 -11
- package/agent/gateway/platforms/feishu_meeting_invite.py +212 -0
- package/agent/gateway/platforms/matrix.py +1498 -297
- package/agent/gateway/platforms/qqbot/adapter.py +6 -0
- package/agent/gateway/platforms/signal.py +8 -0
- package/agent/gateway/platforms/slack.py +308 -12
- package/agent/gateway/platforms/telegram.py +831 -24
- package/agent/gateway/platforms/webhook.py +109 -21
- package/agent/gateway/platforms/weixin.py +113 -2
- package/agent/gateway/platforms/whatsapp.py +94 -288
- package/agent/gateway/platforms/whatsapp_cloud.py +1956 -0
- package/agent/gateway/platforms/whatsapp_common.py +367 -0
- package/agent/gateway/platforms/yuanbao.py +608 -191
- package/agent/gateway/platforms/yuanbao_proto.py +232 -23
- package/agent/gateway/relay/__init__.py +375 -0
- package/agent/gateway/relay/adapter.py +222 -0
- package/agent/gateway/relay/auth.py +168 -0
- package/agent/gateway/relay/descriptor.py +118 -0
- package/agent/gateway/relay/transport.py +101 -0
- package/agent/gateway/relay/ws_transport.py +327 -0
- package/agent/gateway/response_filters.py +53 -0
- package/agent/gateway/rich_sent_store.py +80 -0
- package/agent/gateway/run.py +2940 -5001
- package/agent/gateway/session.py +109 -8
- package/agent/gateway/session_context.py +22 -4
- package/agent/gateway/slash_commands.py +3854 -0
- package/agent/gateway/status.py +141 -21
- package/agent/gateway/stream_consumer.py +288 -31
- package/agent/hermes-already-has-routines.md +1 -1
- package/agent/hermes_cli/__init__.py +62 -17
- package/agent/hermes_cli/_parser.py +30 -0
- package/agent/hermes_cli/_subprocess_compat.py +61 -0
- package/agent/hermes_cli/active_sessions.py +320 -0
- package/agent/hermes_cli/auth.py +707 -59
- package/agent/hermes_cli/auth_commands.py +39 -22
- package/agent/hermes_cli/backup.py +109 -7
- package/agent/hermes_cli/banner.py +88 -0
- package/agent/hermes_cli/blueprint_cmd.py +318 -0
- package/agent/hermes_cli/cli_agent_setup_mixin.py +684 -0
- package/agent/hermes_cli/cli_commands_mixin.py +2293 -0
- package/agent/hermes_cli/commands.py +215 -91
- package/agent/hermes_cli/config.py +967 -130
- package/agent/hermes_cli/container_boot.py +76 -11
- package/agent/hermes_cli/cron.py +5 -11
- package/agent/hermes_cli/curator.py +21 -0
- package/agent/hermes_cli/dashboard_auth/__init__.py +2 -0
- package/agent/hermes_cli/dashboard_auth/base.py +62 -0
- package/agent/hermes_cli/dashboard_auth/cookies.py +32 -19
- package/agent/hermes_cli/dashboard_auth/login_page.py +156 -6
- package/agent/hermes_cli/dashboard_auth/middleware.py +28 -4
- package/agent/hermes_cli/dashboard_auth/prefix.py +46 -2
- package/agent/hermes_cli/dashboard_auth/public_paths.py +6 -0
- package/agent/hermes_cli/dashboard_auth/routes.py +158 -2
- package/agent/hermes_cli/dashboard_auth/ws_tickets.py +85 -11
- package/agent/hermes_cli/dashboard_register.py +427 -0
- package/agent/hermes_cli/debug.py +155 -50
- package/agent/hermes_cli/doctor.py +255 -14
- package/agent/hermes_cli/dump.py +60 -6
- package/agent/hermes_cli/env_loader.py +33 -0
- package/agent/hermes_cli/gateway.py +755 -103
- package/agent/hermes_cli/gateway_enroll.py +250 -0
- package/agent/hermes_cli/gateway_windows.py +254 -11
- package/agent/hermes_cli/gui_uninstall.py +285 -0
- package/agent/hermes_cli/inventory.py +105 -4
- package/agent/hermes_cli/kanban.py +58 -71
- package/agent/hermes_cli/kanban_db.py +391 -14
- package/agent/hermes_cli/kanban_decompose.py +2 -2
- package/agent/hermes_cli/kanban_specify.py +3 -1
- package/agent/hermes_cli/logs.py +2 -0
- package/agent/hermes_cli/main.py +2889 -5287
- package/agent/hermes_cli/managed_scope.py +214 -0
- package/agent/hermes_cli/managed_uv.py +254 -0
- package/agent/hermes_cli/mcp_catalog.py +6 -3
- package/agent/hermes_cli/mcp_config.py +145 -21
- package/agent/hermes_cli/mcp_security.py +96 -0
- package/agent/hermes_cli/mcp_startup.py +32 -3
- package/agent/hermes_cli/memory_providers.py +149 -0
- package/agent/hermes_cli/memory_setup.py +97 -42
- package/agent/hermes_cli/middleware.py +313 -0
- package/agent/hermes_cli/model_catalog.py +31 -0
- package/agent/hermes_cli/model_cost_guard.py +134 -0
- package/agent/hermes_cli/model_normalize.py +2 -1
- package/agent/hermes_cli/model_setup_flows.py +2759 -0
- package/agent/hermes_cli/model_switch.py +242 -27
- package/agent/hermes_cli/models.py +284 -44
- package/agent/hermes_cli/nous_account.py +33 -6
- package/agent/hermes_cli/nous_billing.py +406 -0
- package/agent/hermes_cli/nous_subscription.py +202 -5
- package/agent/hermes_cli/platforms.py +1 -0
- package/agent/hermes_cli/plugins.py +218 -18
- package/agent/hermes_cli/plugins_cmd.py +249 -105
- package/agent/hermes_cli/portal_cli.py +56 -16
- package/agent/hermes_cli/profile_distribution.py +6 -1
- package/agent/hermes_cli/profiles.py +283 -32
- package/agent/hermes_cli/provider_catalog.py +170 -0
- package/agent/hermes_cli/providers.py +4 -1
- package/agent/hermes_cli/pty_bridge.py +53 -4
- package/agent/hermes_cli/runtime_provider.py +216 -34
- package/agent/hermes_cli/secret_prompt.py +4 -4
- package/agent/hermes_cli/secrets_cli.py +24 -0
- package/agent/hermes_cli/send_cmd.py +28 -2
- package/agent/hermes_cli/service_manager.py +166 -19
- package/agent/hermes_cli/session_listing.py +97 -0
- package/agent/hermes_cli/setup.py +158 -94
- package/agent/hermes_cli/setup_whatsapp_cloud.py +541 -0
- package/agent/hermes_cli/skills_config.py +8 -2
- package/agent/hermes_cli/skills_hub.py +149 -7
- package/agent/hermes_cli/status.py +2 -2
- package/agent/hermes_cli/subcommands/__init__.py +18 -0
- package/agent/hermes_cli/subcommands/_shared.py +29 -0
- package/agent/hermes_cli/subcommands/acp.py +52 -0
- package/agent/hermes_cli/subcommands/auth.py +109 -0
- package/agent/hermes_cli/subcommands/backup.py +38 -0
- package/agent/hermes_cli/subcommands/claw.py +92 -0
- package/agent/hermes_cli/subcommands/config.py +49 -0
- package/agent/hermes_cli/subcommands/cron.py +163 -0
- package/agent/hermes_cli/subcommands/dashboard.py +143 -0
- package/agent/hermes_cli/subcommands/debug.py +77 -0
- package/agent/hermes_cli/subcommands/doctor.py +35 -0
- package/agent/hermes_cli/subcommands/dump.py +28 -0
- package/agent/hermes_cli/subcommands/gateway.py +332 -0
- package/agent/hermes_cli/subcommands/gui.py +63 -0
- package/agent/hermes_cli/subcommands/hooks.py +77 -0
- package/agent/hermes_cli/subcommands/import_cmd.py +31 -0
- package/agent/hermes_cli/subcommands/insights.py +25 -0
- package/agent/hermes_cli/subcommands/login.py +78 -0
- package/agent/hermes_cli/subcommands/logout.py +28 -0
- package/agent/hermes_cli/subcommands/logs.py +78 -0
- package/agent/hermes_cli/subcommands/mcp.py +108 -0
- package/agent/hermes_cli/subcommands/memory.py +53 -0
- package/agent/hermes_cli/subcommands/model.py +72 -0
- package/agent/hermes_cli/subcommands/pairing.py +36 -0
- package/agent/hermes_cli/subcommands/plugins.py +94 -0
- package/agent/hermes_cli/subcommands/postinstall.py +23 -0
- package/agent/hermes_cli/subcommands/profile.py +203 -0
- package/agent/hermes_cli/subcommands/prompt_size.py +36 -0
- package/agent/hermes_cli/subcommands/security.py +62 -0
- package/agent/hermes_cli/subcommands/setup.py +58 -0
- package/agent/hermes_cli/subcommands/skills.py +298 -0
- package/agent/hermes_cli/subcommands/slack.py +60 -0
- package/agent/hermes_cli/subcommands/status.py +28 -0
- package/agent/hermes_cli/subcommands/tools.py +95 -0
- package/agent/hermes_cli/subcommands/uninstall.py +41 -0
- package/agent/hermes_cli/subcommands/update.py +70 -0
- package/agent/hermes_cli/subcommands/version.py +18 -0
- package/agent/hermes_cli/subcommands/webhook.py +76 -0
- package/agent/hermes_cli/subcommands/whatsapp.py +22 -0
- package/agent/hermes_cli/suggestions_cmd.py +153 -0
- package/agent/hermes_cli/telegram_managed_bot.py +358 -0
- package/agent/hermes_cli/tips.py +3 -4
- package/agent/hermes_cli/tools_config.py +155 -28
- package/agent/hermes_cli/uninstall.py +231 -35
- package/agent/hermes_cli/web_server.py +6188 -975
- package/agent/hermes_cli/win_pty_bridge.py +179 -0
- package/agent/hermes_cli/write_approval_commands.py +209 -0
- package/agent/hermes_constants.py +164 -33
- package/agent/hermes_logging.py +74 -2
- package/agent/hermes_state.py +919 -106
- package/agent/hermes_time.py +20 -0
- package/agent/locales/af.yaml +23 -0
- package/agent/locales/de.yaml +23 -0
- package/agent/locales/en.yaml +20 -0
- package/agent/locales/es.yaml +23 -0
- package/agent/locales/fr.yaml +23 -0
- package/agent/locales/ga.yaml +23 -0
- package/agent/locales/hu.yaml +23 -0
- package/agent/locales/it.yaml +23 -0
- package/agent/locales/ja.yaml +23 -0
- package/agent/locales/ko.yaml +23 -0
- package/agent/locales/pt.yaml +23 -0
- package/agent/locales/ru.yaml +23 -0
- package/agent/locales/tr.yaml +23 -0
- package/agent/locales/uk.yaml +23 -0
- package/agent/locales/zh-hant.yaml +23 -0
- package/agent/locales/zh.yaml +23 -0
- package/agent/model_tools.py +204 -40
- package/agent/optional-mcps/clawpump/manifest.yaml +4 -2
- package/agent/optional-mcps/clawpump-stdio/manifest.yaml +2 -0
- package/agent/optional-mcps/unreal-engine/manifest.yaml +54 -0
- package/agent/optional-skills/blockchain/hyperliquid/SKILL.md +2 -2
- package/agent/optional-skills/blockchain/hyperliquid/scripts/hyperliquid_client.py +1 -1
- package/agent/optional-skills/creative/kanban-video-orchestrator/SKILL.md +1 -1
- package/agent/optional-skills/creative/kanban-video-orchestrator/assets/setup.sh.tmpl +4 -3
- package/agent/optional-skills/creative/kanban-video-orchestrator/references/kanban-setup.md +6 -4
- package/agent/optional-skills/creative/kanban-video-orchestrator/references/tool-matrix.md +2 -2
- package/agent/{skills/software-development → optional-skills/devops}/hermes-s6-container-supervision/SKILL.md +2 -0
- package/agent/optional-skills/devops/watchers/SKILL.md +1 -1
- package/agent/optional-skills/devops/watchers/scripts/watch_github.py +2 -1
- package/agent/optional-skills/payments/mpp-agent/SKILL.md +124 -0
- package/agent/optional-skills/payments/stripe-link-cli/SKILL.md +184 -0
- package/agent/optional-skills/payments/stripe-projects/SKILL.md +120 -0
- package/agent/optional-skills/productivity/canvas/SKILL.md +1 -1
- package/agent/optional-skills/productivity/canvas/scripts/canvas_api.py +4 -1
- package/agent/optional-skills/productivity/shop/SKILL.md +224 -0
- package/agent/optional-skills/productivity/shop/references/catalog-mcp.md +236 -0
- package/agent/optional-skills/productivity/shop/references/direct-api.md +278 -0
- package/agent/optional-skills/productivity/shop/references/legal.md +3 -0
- package/agent/optional-skills/productivity/shop/references/safety.md +36 -0
- package/agent/optional-skills/productivity/shopify/SKILL.md +1 -1
- package/agent/optional-skills/productivity/siyuan/SKILL.md +1 -1
- package/agent/optional-skills/productivity/telephony/SKILL.md +4 -4
- package/agent/optional-skills/productivity/telephony/scripts/telephony.py +15 -15
- package/agent/optional-skills/security/1password/SKILL.md +1 -1
- package/agent/{skills/red-teaming → optional-skills/security}/godmode/SKILL.md +3 -4
- package/agent/{skills/red-teaming → optional-skills/security}/godmode/scripts/auto_jailbreak.py +3 -1
- package/agent/optional-skills/software-development/rest-graphql-debug/SKILL.md +1 -1
- package/agent/{skills → optional-skills}/software-development/subagent-driven-development/SKILL.md +5 -5
- package/agent/package-lock.json +4082 -7907
- package/agent/package.json +18 -3
- package/agent/plugins/browser/firecrawl/provider.py +4 -1
- package/agent/plugins/cron/__init__.py +344 -0
- package/agent/plugins/cron/chronos/__init__.py +241 -0
- package/agent/plugins/cron/chronos/_nas_client.py +123 -0
- package/agent/plugins/cron/chronos/plugin.yaml +9 -0
- package/agent/plugins/cron/chronos/verify.py +103 -0
- package/agent/plugins/dashboard_auth/basic/__init__.py +491 -0
- package/agent/plugins/dashboard_auth/basic/plugin.yaml +7 -0
- package/agent/plugins/dashboard_auth/nous/__init__.py +12 -14
- package/agent/plugins/dashboard_auth/self_hosted/__init__.py +736 -0
- package/agent/plugins/dashboard_auth/self_hosted/plugin.yaml +8 -0
- package/agent/plugins/disk-cleanup/disk_cleanup.py +100 -20
- package/agent/plugins/google_meet/audio_bridge.py +4 -0
- package/agent/plugins/google_meet/meet_bot.py +7 -1
- package/agent/plugins/hermes-achievements/dashboard/dist/index.js +9 -15
- package/agent/plugins/image_gen/fal/__init__.py +35 -6
- package/agent/plugins/image_gen/krea/__init__.py +56 -13
- package/agent/plugins/image_gen/openai/__init__.py +122 -24
- package/agent/plugins/image_gen/openai-codex/__init__.py +28 -2
- package/agent/plugins/image_gen/xai/__init__.py +92 -12
- package/agent/plugins/kanban/dashboard/dist/index.js +63 -48
- package/agent/plugins/kanban/dashboard/plugin_api.py +39 -35
- package/agent/plugins/memory/__init__.py +48 -5
- package/agent/plugins/memory/byterover/__init__.py +1 -0
- package/agent/plugins/memory/hindsight/README.md +1 -1
- package/agent/plugins/memory/hindsight/__init__.py +138 -24
- package/agent/plugins/memory/hindsight/plugin.yaml +1 -1
- package/agent/plugins/memory/honcho/README.md +13 -10
- package/agent/plugins/memory/honcho/cli.py +247 -122
- package/agent/plugins/memory/honcho/client.py +112 -102
- package/agent/plugins/memory/openviking/README.md +12 -1
- package/agent/plugins/memory/openviking/__init__.py +2281 -107
- package/agent/plugins/memory/openviking/plugin.yaml +1 -2
- package/agent/plugins/memory/supermemory/README.md +22 -10
- package/agent/plugins/memory/supermemory/__init__.py +142 -37
- package/agent/plugins/memory/supermemory/plugin.yaml +1 -1
- package/agent/plugins/model-providers/anthropic/__init__.py +1 -0
- package/agent/plugins/model-providers/bedrock/__init__.py +1 -0
- package/agent/plugins/model-providers/copilot-acp/__init__.py +1 -0
- package/agent/plugins/model-providers/custom/__init__.py +8 -2
- package/agent/plugins/model-providers/kimi-coding/__init__.py +16 -7
- package/agent/plugins/model-providers/minimax/__init__.py +60 -8
- package/agent/plugins/model-providers/opencode-zen/__init__.py +12 -3
- package/agent/plugins/model-providers/openrouter/__init__.py +75 -4
- package/agent/plugins/model-providers/xiaomi/__init__.py +2 -0
- package/agent/plugins/model-providers/zai/__init__.py +1 -0
- package/agent/plugins/observability/langfuse/__init__.py +147 -14
- package/agent/plugins/observability/nemo_relay/README.md +559 -0
- package/agent/plugins/observability/nemo_relay/__init__.py +962 -0
- package/agent/plugins/observability/nemo_relay/plugin.yaml +20 -0
- package/agent/plugins/platforms/discord/adapter.py +932 -61
- package/agent/plugins/platforms/discord/voice_mixer.py +379 -0
- package/agent/plugins/platforms/google_chat/adapter.py +9 -3
- package/agent/plugins/platforms/google_chat/oauth.py +1 -1
- package/agent/plugins/platforms/homeassistant/__init__.py +3 -0
- package/agent/{gateway/platforms/homeassistant.py → plugins/platforms/homeassistant/adapter.py} +128 -0
- package/agent/plugins/platforms/homeassistant/plugin.yaml +22 -0
- package/agent/plugins/platforms/irc/adapter.py +4 -1
- package/agent/plugins/platforms/line/adapter.py +16 -1
- package/agent/plugins/platforms/mattermost/adapter.py +100 -24
- package/agent/plugins/platforms/photon/README.md +179 -0
- package/agent/plugins/platforms/photon/__init__.py +4 -0
- package/agent/plugins/platforms/photon/adapter.py +1586 -0
- package/agent/plugins/platforms/photon/auth.py +1046 -0
- package/agent/plugins/platforms/photon/cli.py +439 -0
- package/agent/plugins/platforms/photon/plugin.yaml +88 -0
- package/agent/plugins/platforms/photon/sidecar/README.md +52 -0
- package/agent/plugins/platforms/photon/sidecar/index.mjs +720 -0
- package/agent/plugins/platforms/photon/sidecar/package-lock.json +1730 -0
- package/agent/plugins/platforms/photon/sidecar/package.json +25 -0
- package/agent/plugins/platforms/photon/sidecar/patch-spectrum-mixed-attachments.mjs +155 -0
- package/agent/plugins/platforms/raft/__init__.py +3 -0
- package/agent/plugins/platforms/raft/adapter.py +774 -0
- package/agent/plugins/platforms/raft/plugin.yaml +19 -0
- package/agent/plugins/platforms/simplex/adapter.py +777 -220
- package/agent/plugins/platforms/simplex/plugin.yaml +21 -2
- package/agent/plugins/platforms/teams/adapter.py +175 -5
- package/agent/plugins/plugin_utils.py +135 -0
- package/agent/plugins/video_gen/fal/__init__.py +10 -3
- package/agent/plugins/web/searxng/provider.py +15 -2
- package/agent/plugins/web/xai/provider.py +2 -2
- package/agent/providers/base.py +22 -3
- package/agent/pyproject.toml +115 -21
- package/agent/run_agent.py +733 -39
- package/agent/scripts/build_skills_index.py +51 -19
- package/agent/scripts/check_subprocess_stdin.py +177 -0
- package/agent/scripts/contributor_audit.py +2 -0
- package/agent/scripts/docker_config_migrate.py +67 -0
- package/agent/scripts/install.cmd +3 -3
- package/agent/scripts/install.ps1 +580 -154
- package/agent/scripts/install.sh +402 -185
- package/agent/scripts/lib/node-bootstrap.sh +39 -4
- package/agent/scripts/release.py +183 -0
- package/agent/scripts/run_tests.sh +1 -0
- package/agent/scripts/run_tests_parallel.py +18 -23
- package/agent/scripts/whatsapp-bridge/bridge.js +25 -4
- package/agent/setup.py +59 -0
- package/agent/skills/autonomous-ai-agents/codex/SKILL.md +19 -0
- package/agent/skills/autonomous-ai-agents/hermes-agent/SKILL.md +10 -3
- package/agent/skills/{mcp/native-mcp/SKILL.md → autonomous-ai-agents/hermes-agent/references/native-mcp.md} +0 -13
- package/agent/skills/{devops/webhook-subscriptions/SKILL.md → autonomous-ai-agents/hermes-agent/references/webhooks.md} +1 -11
- package/agent/skills/clawpump/SKILL.md +3 -1
- package/agent/skills/devops/kanban-orchestrator/SKILL.md +1 -0
- package/agent/skills/devops/kanban-worker/SKILL.md +1 -0
- package/agent/skills/github/github-auth/SKILL.md +2 -2
- package/agent/skills/github/github-auth/scripts/gh-env.sh +2 -2
- package/agent/skills/github/github-code-review/SKILL.md +2 -2
- package/agent/skills/github/github-issues/SKILL.md +2 -2
- package/agent/skills/github/github-pr-workflow/SKILL.md +2 -2
- package/agent/skills/github/github-repo-management/SKILL.md +2 -2
- package/agent/skills/media/gif-search/SKILL.md +1 -1
- package/agent/skills/media/youtube-content/SKILL.md +10 -7
- package/agent/skills/media/youtube-content/scripts/fetch_transcript.py +3 -3
- package/agent/skills/note-taking/obsidian/SKILL.md +1 -1
- package/agent/skills/productivity/airtable/SKILL.md +2 -2
- package/agent/skills/productivity/google-workspace/scripts/setup.py +33 -7
- package/agent/skills/productivity/notion/SKILL.md +2 -2
- package/agent/skills/productivity/teams-meeting-pipeline/SKILL.md +1 -1
- package/agent/skills/research/llm-wiki/SKILL.md +1 -1
- package/agent/skills/social-media/xurl/SKILL.md +9 -0
- package/agent/skills/software-development/hermes-agent-skill-authoring/SKILL.md +1 -1
- package/agent/skills/software-development/plan/SKILL.md +285 -5
- package/agent/skills/software-development/requesting-code-review/SKILL.md +2 -2
- package/agent/skills/software-development/simplify-code/SKILL.md +212 -0
- package/agent/skills/software-development/spike/SKILL.md +2 -2
- package/agent/skills/software-development/systematic-debugging/SKILL.md +1 -1
- package/agent/skills/software-development/test-driven-development/SKILL.md +1 -1
- package/agent/tools/approval.py +302 -4
- package/agent/tools/async_delegation.py +386 -0
- package/agent/tools/blueprints.py +325 -0
- package/agent/tools/browser_cdp_tool.py +3 -3
- package/agent/tools/browser_tool.py +34 -6
- package/agent/tools/checkpoint_manager.py +31 -1
- package/agent/tools/clarify_tool.py +55 -5
- package/agent/tools/code_execution_tool.py +31 -14
- package/agent/tools/computer_use/cua_backend.py +81 -3
- package/agent/tools/computer_use/tool.py +79 -5
- package/agent/tools/computer_use/vision_routing.py +55 -3
- package/agent/tools/credential_files.py +31 -12
- package/agent/tools/cronjob_tools.py +30 -20
- package/agent/tools/delegate_tool.py +356 -31
- package/agent/tools/env_probe.py +1 -0
- package/agent/tools/environments/docker.py +163 -8
- package/agent/tools/environments/file_sync.py +2 -1
- package/agent/tools/environments/local.py +74 -23
- package/agent/tools/environments/singularity.py +4 -1
- package/agent/tools/environments/ssh.py +78 -11
- package/agent/tools/file_operations.py +277 -41
- package/agent/tools/file_tools.py +166 -28
- package/agent/tools/image_generation_tool.py +515 -29
- package/agent/tools/kanban_tools.py +99 -0
- package/agent/tools/lazy_deps.py +33 -2
- package/agent/tools/mcp_oauth.py +5 -5
- package/agent/tools/mcp_oauth_manager.py +7 -5
- package/agent/tools/mcp_tool.py +840 -33
- package/agent/tools/memory_tool.py +335 -38
- package/agent/tools/osv_check.py +15 -1
- package/agent/tools/process_registry.py +155 -11
- package/agent/tools/read_extract.py +248 -0
- package/agent/tools/read_terminal_tool.py +93 -0
- package/agent/tools/schema_sanitizer.py +38 -0
- package/agent/tools/send_message_tool.py +163 -49
- package/agent/tools/session_search_tool.py +189 -7
- package/agent/tools/skill_manager_tool.py +202 -3
- package/agent/tools/skill_usage.py +52 -4
- package/agent/tools/skills_hub.py +184 -44
- package/agent/tools/skills_sync.py +232 -5
- package/agent/tools/skills_tool.py +125 -11
- package/agent/tools/terminal_tool.py +148 -26
- package/agent/tools/tirith_security.py +2 -0
- package/agent/tools/todo_tool.py +32 -1
- package/agent/tools/transcription_tools.py +13 -5
- package/agent/tools/tts_tool.py +332 -38
- package/agent/tools/url_safety.py +52 -1
- package/agent/tools/vision_tools.py +124 -39
- package/agent/tools/voice_mode.py +4 -3
- package/agent/tools/web_tools.py +45 -15
- package/agent/tools/write_approval.py +493 -0
- package/agent/toolsets.py +34 -10
- package/agent/trajectory_compressor.py +81 -10
- package/agent/tui_gateway/entry.py +43 -6
- package/agent/tui_gateway/server.py +3335 -330
- package/agent/tui_gateway/slash_worker.py +61 -0
- package/agent/tui_gateway/ws.py +67 -9
- package/agent/ui-tui/eslint.config.mjs +0 -4
- package/agent/ui-tui/package.json +6 -6
- package/agent/ui-tui/packages/hermes-ink/package.json +1 -1
- package/agent/ui-tui/packages/hermes-ink/src/ink/app-mouse.test.ts +34 -1
- package/agent/ui-tui/packages/hermes-ink/src/ink/app-rawmode-mouse.test.ts +91 -0
- package/agent/ui-tui/packages/hermes-ink/src/ink/components/App.tsx +35 -2
- package/agent/ui-tui/packages/hermes-ink/src/ink/events/input-event.ts +4 -11
- package/agent/ui-tui/packages/hermes-ink/src/ink/parse-keypress.test.ts +23 -57
- package/agent/ui-tui/packages/hermes-ink/src/ink/parse-keypress.ts +11 -135
- package/agent/ui-tui/packages/hermes-ink/src/ink/termio/tokenize.test.ts +185 -0
- package/agent/ui-tui/packages/hermes-ink/src/ink/termio/tokenize.ts +37 -3
- package/agent/ui-tui/packages/hermes-ink/src/utils/execFileNoThrow.ts +5 -5
- package/agent/ui-tui/src/__tests__/appChromeStatusRule.test.tsx +217 -0
- package/agent/ui-tui/src/__tests__/appChromeStatusRuleDevCredits.test.tsx +73 -0
- package/agent/ui-tui/src/__tests__/approvalAction.test.ts +11 -0
- package/agent/ui-tui/src/__tests__/billingCommand.test.ts +301 -0
- package/agent/ui-tui/src/__tests__/blockLayout.test.ts +122 -0
- package/agent/ui-tui/src/__tests__/brandingMcpCount.test.ts +111 -0
- package/agent/ui-tui/src/__tests__/completionApply.test.ts +51 -0
- package/agent/ui-tui/src/__tests__/createGatewayEventHandler.test.ts +487 -2
- package/agent/ui-tui/src/__tests__/createSlashHandler.test.ts +54 -0
- package/agent/ui-tui/src/__tests__/creditsCommand.test.ts +144 -0
- package/agent/ui-tui/src/__tests__/gatewayClient.test.ts +120 -99
- package/agent/ui-tui/src/__tests__/gracefulExit.test.ts +11 -0
- package/agent/ui-tui/src/__tests__/memoryMonitor.test.ts +102 -0
- package/agent/ui-tui/src/__tests__/paths.test.ts +41 -1
- package/agent/ui-tui/src/__tests__/terminalModes.test.ts +22 -0
- package/agent/ui-tui/src/__tests__/text.test.ts +23 -0
- package/agent/ui-tui/src/__tests__/textInputFastEcho.test.ts +37 -0
- package/agent/ui-tui/src/__tests__/turnControllerNotice.test.ts +43 -0
- package/agent/ui-tui/src/__tests__/useInputHandlers.test.ts +38 -1
- package/agent/ui-tui/src/__tests__/virtualHeights.test.ts +8 -0
- package/agent/ui-tui/src/app/createGatewayEventHandler.ts +102 -7
- package/agent/ui-tui/src/app/interfaces.ts +64 -1
- package/agent/ui-tui/src/app/overlayStore.ts +18 -2
- package/agent/ui-tui/src/app/slash/commands/billing.ts +332 -0
- package/agent/ui-tui/src/app/slash/commands/core.ts +31 -2
- package/agent/ui-tui/src/app/slash/commands/credits.ts +57 -0
- package/agent/ui-tui/src/app/slash/commands/ops.ts +28 -0
- package/agent/ui-tui/src/app/slash/commands/session.ts +32 -4
- package/agent/ui-tui/src/app/slash/registry.ts +4 -0
- package/agent/ui-tui/src/app/turnController.ts +145 -2
- package/agent/ui-tui/src/app/uiStore.ts +2 -0
- package/agent/ui-tui/src/app/useInputHandlers.ts +42 -4
- package/agent/ui-tui/src/app/useMainApp.ts +54 -8
- package/agent/ui-tui/src/app/useSessionLifecycle.ts +40 -31
- package/agent/ui-tui/src/app/useSubmission.ts +23 -31
- package/agent/ui-tui/src/components/appChrome.tsx +112 -5
- package/agent/ui-tui/src/components/appLayout.tsx +9 -0
- package/agent/ui-tui/src/components/appOverlays.tsx +25 -1
- package/agent/ui-tui/src/components/billingOverlay.tsx +684 -0
- package/agent/ui-tui/src/components/branding.tsx +15 -3
- package/agent/ui-tui/src/components/messageLine.tsx +25 -3
- package/agent/ui-tui/src/components/pluginsHub.tsx +238 -0
- package/agent/ui-tui/src/components/prompts.tsx +31 -17
- package/agent/ui-tui/src/components/streamingAssistant.tsx +63 -55
- package/agent/ui-tui/src/components/textInput.tsx +16 -0
- package/agent/ui-tui/src/config/env.ts +12 -0
- package/agent/ui-tui/src/config/limits.ts +13 -0
- package/agent/ui-tui/src/domain/blockLayout.ts +146 -0
- package/agent/ui-tui/src/domain/paths.ts +24 -0
- package/agent/ui-tui/src/domain/slash.ts +40 -0
- package/agent/ui-tui/src/entry.tsx +35 -4
- package/agent/ui-tui/src/gatewayClient.ts +22 -10
- package/agent/ui-tui/src/gatewayTypes.ts +130 -1
- package/agent/ui-tui/src/lib/gracefulExit.ts +24 -4
- package/agent/ui-tui/src/lib/memory.test.ts +162 -0
- package/agent/ui-tui/src/lib/memory.ts +60 -1
- package/agent/ui-tui/src/lib/memoryMonitor.ts +79 -4
- package/agent/ui-tui/src/lib/osc52.ts +1 -1
- package/agent/ui-tui/src/lib/text.test.ts +32 -1
- package/agent/ui-tui/src/lib/text.ts +29 -2
- package/agent/ui-tui/src/lib/virtualHeights.ts +13 -0
- package/agent/ui-tui/src/types.ts +5 -0
- package/agent/ui-tui/tsconfig.build.json +0 -1
- package/agent/ui-tui/tsconfig.json +2 -1
- package/agent/utils.py +66 -2
- package/agent/uv.lock +300 -684
- package/agent/web/index.html +2 -2
- package/agent/web/package.json +11 -6
- package/agent/web/public/claw-bg.webp +0 -0
- package/agent/web/public/claw-logo.webp +0 -0
- package/agent/web/src/App.tsx +138 -48
- package/agent/web/src/components/AutomationBlueprints.tsx +225 -0
- package/agent/web/src/components/Backdrop.tsx +15 -0
- package/agent/web/src/components/ChatSessionList.tsx +260 -0
- package/agent/web/src/components/ChatSidebar.tsx +262 -78
- package/agent/web/src/components/ConfirmDialog.tsx +122 -0
- package/agent/web/src/components/ModelPickerDialog.tsx +111 -16
- package/agent/web/src/components/ModelReloadConfirm.tsx +40 -0
- package/agent/web/src/components/ProfileScopeBanner.tsx +30 -0
- package/agent/web/src/components/ProfileSwitcher.tsx +67 -0
- package/agent/web/src/components/ReasoningPicker.tsx +167 -0
- package/agent/web/src/components/SkillEditorDialog.tsx +215 -0
- package/agent/web/src/components/ThemeSwitcher.tsx +119 -4
- package/agent/web/src/components/ToolsetConfigDrawer.tsx +457 -0
- package/agent/web/src/contexts/PageHeaderProvider.tsx +7 -4
- package/agent/web/src/contexts/ProfileProvider.tsx +137 -0
- package/agent/web/src/contexts/SystemActions.tsx +6 -8
- package/agent/web/src/contexts/profile-context.ts +19 -0
- package/agent/web/src/contexts/useProfileScope.ts +6 -0
- package/agent/web/src/i18n/af.ts +5 -4
- package/agent/web/src/i18n/de.ts +5 -4
- package/agent/web/src/i18n/en.ts +58 -4
- package/agent/web/src/i18n/es.ts +5 -3
- package/agent/web/src/i18n/fr.ts +5 -3
- package/agent/web/src/i18n/ga.ts +5 -4
- package/agent/web/src/i18n/hu.ts +5 -4
- package/agent/web/src/i18n/it.ts +5 -4
- package/agent/web/src/i18n/ja.ts +5 -4
- package/agent/web/src/i18n/ko.ts +5 -4
- package/agent/web/src/i18n/pt.ts +5 -3
- package/agent/web/src/i18n/ru.ts +5 -4
- package/agent/web/src/i18n/tr.ts +5 -4
- package/agent/web/src/i18n/types.ts +59 -1
- package/agent/web/src/i18n/uk.ts +5 -3
- package/agent/web/src/i18n/zh-hant.ts +5 -4
- package/agent/web/src/i18n/zh.ts +5 -4
- package/agent/web/src/index.css +2 -2
- package/agent/web/src/lib/api.ts +819 -52
- package/agent/web/src/lib/dashboard-flags.ts +16 -7
- package/agent/web/src/lib/reasoning-effort.test.ts +48 -0
- package/agent/web/src/lib/reasoning-effort.ts +36 -0
- package/agent/web/src/lib/session-refresh.test.ts +21 -0
- package/agent/web/src/lib/session-refresh.ts +26 -0
- package/agent/web/src/pages/ChannelsPage.tsx +529 -68
- package/agent/web/src/pages/ChatPage.tsx +249 -56
- package/agent/web/src/pages/ConfigPage.tsx +11 -1
- package/agent/web/src/pages/CronPage.tsx +219 -31
- package/agent/web/src/pages/EnvPage.tsx +25 -6
- package/agent/web/src/pages/FilesPage.tsx +525 -0
- package/agent/web/src/pages/McpPage.tsx +80 -3
- package/agent/web/src/pages/ModelsPage.tsx +97 -12
- package/agent/web/src/pages/PluginsPage.tsx +1 -1
- package/agent/web/src/pages/ProfileBuilderPage.tsx +611 -0
- package/agent/web/src/pages/ProfilesPage.tsx +1038 -172
- package/agent/web/src/pages/SessionsPage.tsx +144 -13
- package/agent/web/src/pages/SkillsPage.tsx +851 -70
- package/agent/web/src/pages/SystemPage.tsx +340 -4
- package/agent/web/src/pages/WalletPage.tsx +401 -0
- package/agent/web/src/pages/WebhooksPage.tsx +145 -15
- package/agent/web/src/pages/X402Page.tsx +207 -0
- package/agent/web/src/plugins/registry.ts +28 -11
- package/agent/web/src/plugins/sdk.d.ts +160 -0
- package/agent/web/src/themes/context.tsx +112 -5
- package/agent/web/src/themes/fonts.ts +167 -0
- package/agent/web/src/themes/index.ts +7 -0
- package/agent/web/tsconfig.app.json +0 -1
- package/agent/web/vite.config.ts +1 -8
- package/agent/web/vitest.config.ts +16 -0
- package/package.json +1 -1
- package/agent/apps/desktop/package-lock.json +0 -18363
- package/agent/apps/desktop/src/app/chat/composer/skin-slash-popover.tsx +0 -56
- package/agent/apps/desktop/src/components/assistant-ui/thread-virtualizer.tsx +0 -382
- package/agent/apps/desktop/src/components/assistant-ui/todo-tool.tsx +0 -109
- package/agent/apps/desktop/src/components/chat/generated-image-context.tsx +0 -19
- package/agent/optional-skills/productivity/shop-app/SKILL.md +0 -340
- package/agent/skills/autonomous-ai-agents/kanban-codex-lane/SKILL.md +0 -277
- package/agent/skills/autonomous-ai-agents/kanban-codex-lane/templates/pmb-codex-lane-prompt.md +0 -57
- package/agent/skills/diagramming/DESCRIPTION.md +0 -3
- package/agent/skills/domain/DESCRIPTION.md +0 -24
- package/agent/skills/gifs/DESCRIPTION.md +0 -3
- package/agent/skills/inference-sh/DESCRIPTION.md +0 -19
- package/agent/skills/mcp/DESCRIPTION.md +0 -3
- package/agent/skills/media/spotify/SKILL.md +0 -135
- package/agent/skills/mlops/training/DESCRIPTION.md +0 -3
- package/agent/skills/mlops/vector-databases/DESCRIPTION.md +0 -3
- package/agent/skills/productivity/linear/SKILL.md +0 -380
- package/agent/skills/productivity/linear/scripts/linear_api.py +0 -445
- package/agent/skills/software-development/debugging-hermes-tui-commands/SKILL.md +0 -152
- package/agent/skills/software-development/writing-plans/SKILL.md +0 -297
- package/agent/ui-tui/package-lock.json +0 -7449
- package/agent/ui-tui/packages/hermes-ink/package-lock.json +0 -1289
- package/agent/web/package-lock.json +0 -8887
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/PORT_NOTES.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/SKILL.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/prompts/system.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/palettes/macaron.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/palettes/mono-ink.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/palettes/neon.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/palettes/warm.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/prompt-construction.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/style-presets.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/blueprint.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/chalkboard.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/editorial.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/elegant.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/fantasy-animation.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/flat-doodle.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/flat.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/ink-notes.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/intuition-machine.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/minimal.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/nature.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/notion.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/pixel-art.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/playful.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/retro.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/scientific.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/screen-print.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/sketch-notes.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/sketch.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/vector-illustration.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/vintage.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/warm.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/watercolor.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/usage.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/workflow.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/PORT_NOTES.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/SKILL.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/analysis-framework.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/art-styles/chalk.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/art-styles/ink-brush.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/art-styles/ligne-claire.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/art-styles/manga.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/art-styles/minimalist.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/art-styles/realistic.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/auto-selection.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/base-prompt.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/character-template.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/layouts/cinematic.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/layouts/dense.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/layouts/four-panel.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/layouts/mixed.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/layouts/splash.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/layouts/standard.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/layouts/webtoon.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/ohmsha-guide.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/partial-workflows.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/presets/concept-story.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/presets/four-panel.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/presets/ohmsha.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/presets/shoujo.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/presets/wuxia.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/storyboard-template.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/tones/action.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/tones/dramatic.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/tones/energetic.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/tones/neutral.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/tones/romantic.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/tones/vintage.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/tones/warm.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/workflow.md +0 -0
- /package/agent/{skills → optional-skills}/creative/creative-ideation/SKILL.md +0 -0
- /package/agent/{skills → optional-skills}/creative/creative-ideation/references/full-prompt-library.md +0 -0
- /package/agent/{skills → optional-skills}/creative/pixel-art/ATTRIBUTION.md +0 -0
- /package/agent/{skills → optional-skills}/creative/pixel-art/SKILL.md +0 -0
- /package/agent/{skills → optional-skills}/creative/pixel-art/references/palettes.md +0 -0
- /package/agent/{skills → optional-skills}/creative/pixel-art/scripts/__init__.py +0 -0
- /package/agent/{skills → optional-skills}/creative/pixel-art/scripts/palettes.py +0 -0
- /package/agent/{skills → optional-skills}/creative/pixel-art/scripts/pixel_art.py +0 -0
- /package/agent/{skills → optional-skills}/creative/pixel-art/scripts/pixel_art_video.py +0 -0
- /package/agent/{skills/mlops/inference → optional-skills/mlops}/obliteratus/SKILL.md +0 -0
- /package/agent/{skills/mlops/inference → optional-skills/mlops}/obliteratus/references/analysis-modules.md +0 -0
- /package/agent/{skills/mlops/inference → optional-skills/mlops}/obliteratus/references/methods-guide.md +0 -0
- /package/agent/{skills/mlops/inference → optional-skills/mlops}/obliteratus/templates/abliteration-config.yaml +0 -0
- /package/agent/{skills/mlops/inference → optional-skills/mlops}/obliteratus/templates/analysis-study.yaml +0 -0
- /package/agent/{skills/mlops/inference → optional-skills/mlops}/obliteratus/templates/batch-abliteration.yaml +0 -0
- /package/agent/{skills → optional-skills}/mlops/research/DESCRIPTION.md +0 -0
- /package/agent/{skills → optional-skills}/mlops/research/dspy/SKILL.md +0 -0
- /package/agent/{skills → optional-skills}/mlops/research/dspy/references/examples.md +0 -0
- /package/agent/{skills → optional-skills}/mlops/research/dspy/references/modules.md +0 -0
- /package/agent/{skills → optional-skills}/mlops/research/dspy/references/optimizers.md +0 -0
- /package/agent/{skills/red-teaming → optional-skills/security}/godmode/references/jailbreak-templates.md +0 -0
- /package/agent/{skills/red-teaming → optional-skills/security}/godmode/references/refusal-detection.md +0 -0
- /package/agent/{skills/red-teaming → optional-skills/security}/godmode/scripts/godmode_race.py +0 -0
- /package/agent/{skills/red-teaming → optional-skills/security}/godmode/scripts/load_godmode.py +0 -0
- /package/agent/{skills/red-teaming → optional-skills/security}/godmode/scripts/parseltongue.py +0 -0
- /package/agent/{skills/red-teaming → optional-skills/security}/godmode/templates/prefill-subtle.json +0 -0
- /package/agent/{skills/red-teaming → optional-skills/security}/godmode/templates/prefill.json +0 -0
- /package/agent/{skills → optional-skills}/software-development/subagent-driven-development/references/context-budget-discipline.md +0 -0
- /package/agent/{skills → optional-skills}/software-development/subagent-driven-development/references/gates-taxonomy.md +0 -0
package/agent/hermes_cli/auth.py
CHANGED
|
@@ -71,6 +71,7 @@ DEFAULT_NOUS_PORTAL_URL = "https://portal.nousresearch.com"
|
|
|
71
71
|
DEFAULT_NOUS_INFERENCE_URL = "https://inference-api.nousresearch.com/v1"
|
|
72
72
|
DEFAULT_NOUS_CLIENT_ID = "hermes-cli"
|
|
73
73
|
NOUS_INFERENCE_INVOKE_SCOPE = "inference:invoke"
|
|
74
|
+
NOUS_BILLING_MANAGE_SCOPE = "billing:manage"
|
|
74
75
|
DEFAULT_NOUS_SCOPE = NOUS_INFERENCE_INVOKE_SCOPE
|
|
75
76
|
NOUS_DEVICE_CODE_SOURCE = "device_code"
|
|
76
77
|
NOUS_AUTH_PATH_INVOKE_JWT = "invoke_jwt"
|
|
@@ -103,7 +104,12 @@ XAI_OAUTH_SCOPE = "openid profile email offline_access grok-cli:access api:acces
|
|
|
103
104
|
XAI_OAUTH_REDIRECT_HOST = "127.0.0.1"
|
|
104
105
|
XAI_OAUTH_REDIRECT_PORT = 56121
|
|
105
106
|
XAI_OAUTH_REDIRECT_PATH = "/callback"
|
|
106
|
-
|
|
107
|
+
# xAI/Grok OAuth access tokens are intentionally short-lived (about 6h in
|
|
108
|
+
# current SuperGrok flows). A two-minute refresh window is too narrow for
|
|
109
|
+
# gateway/cron workloads that may only touch the provider every 30 minutes,
|
|
110
|
+
# leaving brief but noisy credential-expiry gaps. Refresh up to one hour
|
|
111
|
+
# early so ordinary runtime calls keep the token warm without user reauth.
|
|
112
|
+
XAI_ACCESS_TOKEN_REFRESH_SKEW_SECONDS = 3600
|
|
107
113
|
QWEN_OAUTH_CLIENT_ID = "f0304373b74a44d2b584a3fb70ca9e56"
|
|
108
114
|
QWEN_OAUTH_TOKEN_URL = "https://chat.qwen.ai/api/v1/oauth2/token"
|
|
109
115
|
QWEN_ACCESS_TOKEN_REFRESH_SKEW_SECONDS = 120
|
|
@@ -643,8 +649,8 @@ ZAI_ENDPOINTS = [
|
|
|
643
649
|
# (id, base_url, probe_models, label)
|
|
644
650
|
("global", "https://api.z.ai/api/paas/v4", ["glm-5"], "Global"),
|
|
645
651
|
("cn", "https://open.bigmodel.cn/api/paas/v4", ["glm-5"], "China"),
|
|
646
|
-
("coding-global", "https://api.z.ai/api/coding/paas/v4", ["glm-5.1", "glm-5v-turbo", "glm-4.7"], "Global (Coding Plan)"),
|
|
647
|
-
("coding-cn", "https://open.bigmodel.cn/api/coding/paas/v4", ["glm-5.1", "glm-5v-turbo", "glm-4.7"], "China (Coding Plan)"),
|
|
652
|
+
("coding-global", "https://api.z.ai/api/coding/paas/v4", ["glm-5.2", "glm-5.1", "glm-5v-turbo", "glm-4.7"], "Global (Coding Plan)"),
|
|
653
|
+
("coding-cn", "https://open.bigmodel.cn/api/coding/paas/v4", ["glm-5.2", "glm-5.1", "glm-5v-turbo", "glm-4.7"], "China (Coding Plan)"),
|
|
648
654
|
]
|
|
649
655
|
|
|
650
656
|
|
|
@@ -1106,8 +1112,13 @@ def _load_auth_store(auth_file: Optional[Path] = None) -> Dict[str, Any]:
|
|
|
1106
1112
|
return {"version": AUTH_STORE_VERSION, "providers": {}}
|
|
1107
1113
|
|
|
1108
1114
|
|
|
1109
|
-
def _save_auth_store(auth_store: Dict[str, Any]) -> Path:
|
|
1110
|
-
|
|
1115
|
+
def _save_auth_store(auth_store: Dict[str, Any], target_path: Optional[Path] = None) -> Path:
|
|
1116
|
+
# target_path=None preserves the existing contract (write the active
|
|
1117
|
+
# store at _auth_file_path()). An explicit path lets callers persist a
|
|
1118
|
+
# specific store — e.g. the global-root write-through for rotating xAI
|
|
1119
|
+
# OAuth grants (#43589) — reusing this function's atomic O_EXCL + 0o600
|
|
1120
|
+
# write so the root auth.json gets the same TOCTOU-safe treatment.
|
|
1121
|
+
auth_file = target_path if target_path is not None else _auth_file_path()
|
|
1111
1122
|
auth_file.parent.mkdir(parents=True, exist_ok=True)
|
|
1112
1123
|
# Tighten parent dir to 0o700 so siblings can't traverse to creds.
|
|
1113
1124
|
# No-op on Windows (POSIX mode bits not enforced); ignore failures.
|
|
@@ -1209,6 +1220,24 @@ def _store_provider_state(
|
|
|
1209
1220
|
auth_store["active_provider"] = provider_id
|
|
1210
1221
|
|
|
1211
1222
|
|
|
1223
|
+
def mark_provider_active_if_unset(provider_id: str) -> None:
|
|
1224
|
+
"""Set ``active_provider`` to *provider_id* only when none is set yet.
|
|
1225
|
+
|
|
1226
|
+
Used by ``hermes auth add`` OAuth paths that create credential-pool
|
|
1227
|
+
entries directly (no singleton ``providers.<id>`` block). Adding the
|
|
1228
|
+
very first credential for a provider should make it the active provider
|
|
1229
|
+
so the setup wizard's ``_model_section_has_credentials()`` check (which
|
|
1230
|
+
consults ``get_active_provider()``) does not report "No inference
|
|
1231
|
+
provider configured". Subsequent adds for an already-active setup leave
|
|
1232
|
+
the user's chosen active provider untouched.
|
|
1233
|
+
"""
|
|
1234
|
+
with _auth_store_lock():
|
|
1235
|
+
auth_store = _load_auth_store()
|
|
1236
|
+
if not (auth_store.get("active_provider") or "").strip():
|
|
1237
|
+
auth_store["active_provider"] = provider_id
|
|
1238
|
+
_save_auth_store(auth_store)
|
|
1239
|
+
|
|
1240
|
+
|
|
1212
1241
|
def is_known_auth_provider(provider_id: str) -> bool:
|
|
1213
1242
|
normalized = (provider_id or "").strip().lower()
|
|
1214
1243
|
return normalized in PROVIDER_REGISTRY or normalized in SERVICE_PROVIDER_NAMES
|
|
@@ -1588,6 +1617,21 @@ def resolve_provider(
|
|
|
1588
1617
|
if has_usable_secret(os.getenv("OPENAI_API_KEY")) or has_usable_secret(os.getenv("OPENROUTER_API_KEY")):
|
|
1589
1618
|
return "openrouter"
|
|
1590
1619
|
|
|
1620
|
+
# Auto-detect an OpenRouter credential added via `hermes auth add openrouter`
|
|
1621
|
+
# (manual pool entry, no env var). Without this, a key that only lives in
|
|
1622
|
+
# the credential pool is invisible to auto-detection — the user sees
|
|
1623
|
+
# `hermes auth list` showing the credential while requests go out with no
|
|
1624
|
+
# Authorization header ("HTTP 401: Missing Authentication header"). The
|
|
1625
|
+
# env-var check above only covers keys exported as OPENROUTER_API_KEY /
|
|
1626
|
+
# OPENAI_API_KEY. See issue #42130.
|
|
1627
|
+
try:
|
|
1628
|
+
from agent.credential_pool import load_pool as _load_pool
|
|
1629
|
+
|
|
1630
|
+
if _load_pool("openrouter").has_credentials():
|
|
1631
|
+
return "openrouter"
|
|
1632
|
+
except Exception as e:
|
|
1633
|
+
logger.debug("Could not check OpenRouter credential pool: %s", e)
|
|
1634
|
+
|
|
1591
1635
|
# Auto-detect API-key providers by checking their env vars
|
|
1592
1636
|
for pid, pconfig in PROVIDER_REGISTRY.items():
|
|
1593
1637
|
if pconfig.auth_type != "api_key":
|
|
@@ -2064,6 +2108,25 @@ def _refresh_qwen_cli_tokens(tokens: Dict[str, Any], timeout_seconds: float = 20
|
|
|
2064
2108
|
return refreshed
|
|
2065
2109
|
|
|
2066
2110
|
|
|
2111
|
+
def _mark_qwen_oauth_active(creds: Dict[str, Any]) -> None:
|
|
2112
|
+
"""Set active_provider to qwen-oauth in auth.json.
|
|
2113
|
+
|
|
2114
|
+
Qwen OAuth tokens live in the Qwen CLI credential file managed by
|
|
2115
|
+
_save_qwen_cli_tokens / resolve_qwen_runtime_credentials. This function
|
|
2116
|
+
only writes a minimal provider-state entry (base_url for display) and
|
|
2117
|
+
sets active_provider so that get_active_provider() and
|
|
2118
|
+
_model_section_has_credentials() detect the provider for the setup wizard
|
|
2119
|
+
and status commands.
|
|
2120
|
+
"""
|
|
2121
|
+
with _auth_store_lock():
|
|
2122
|
+
auth_store = _load_auth_store()
|
|
2123
|
+
state: Dict[str, Any] = {}
|
|
2124
|
+
if creds.get("base_url"):
|
|
2125
|
+
state["base_url"] = str(creds["base_url"])
|
|
2126
|
+
_save_provider_state(auth_store, "qwen-oauth", state)
|
|
2127
|
+
_save_auth_store(auth_store)
|
|
2128
|
+
|
|
2129
|
+
|
|
2067
2130
|
def resolve_qwen_runtime_credentials(
|
|
2068
2131
|
*,
|
|
2069
2132
|
force_refresh: bool = False,
|
|
@@ -2127,6 +2190,24 @@ def get_qwen_auth_status() -> Dict[str, Any]:
|
|
|
2127
2190
|
# Actual HTTP traffic goes to https://cloudcode-pa.googleapis.com/v1internal:*.
|
|
2128
2191
|
# =============================================================================
|
|
2129
2192
|
|
|
2193
|
+
def _mark_google_gemini_cli_active(creds: Dict[str, Any]) -> None:
|
|
2194
|
+
"""Set active_provider to google-gemini-cli in auth.json.
|
|
2195
|
+
|
|
2196
|
+
The actual OAuth tokens live in the Google credential file managed by
|
|
2197
|
+
agent.google_oauth. This function only writes a minimal provider-state
|
|
2198
|
+
entry (email for display) and sets active_provider so that
|
|
2199
|
+
get_active_provider() and _model_section_has_credentials() detect the
|
|
2200
|
+
provider for the setup wizard and status commands.
|
|
2201
|
+
"""
|
|
2202
|
+
with _auth_store_lock():
|
|
2203
|
+
auth_store = _load_auth_store()
|
|
2204
|
+
state: Dict[str, Any] = {}
|
|
2205
|
+
if creds.get("email"):
|
|
2206
|
+
state["email"] = str(creds["email"])
|
|
2207
|
+
_save_provider_state(auth_store, "google-gemini-cli", state)
|
|
2208
|
+
_save_auth_store(auth_store)
|
|
2209
|
+
|
|
2210
|
+
|
|
2130
2211
|
def resolve_gemini_oauth_runtime_credentials(
|
|
2131
2212
|
*,
|
|
2132
2213
|
force_refresh: bool = False,
|
|
@@ -2622,12 +2703,23 @@ def _xai_wait_for_callback(
|
|
|
2622
2703
|
result: dict[str, Any],
|
|
2623
2704
|
*,
|
|
2624
2705
|
timeout_seconds: float = 180.0,
|
|
2706
|
+
manual_paste_redirect_uri: Optional[str] = None,
|
|
2625
2707
|
) -> dict[str, Any]:
|
|
2626
2708
|
deadline = time.monotonic() + max(5.0, timeout_seconds)
|
|
2709
|
+
if manual_paste_redirect_uri and sys.stdin.isatty():
|
|
2710
|
+
print()
|
|
2711
|
+
print("If xAI shows a Grok Build code instead of redirecting,")
|
|
2712
|
+
print("paste that code here and press Enter.")
|
|
2627
2713
|
try:
|
|
2628
2714
|
while time.monotonic() < deadline:
|
|
2629
2715
|
if result["code"] or result["error"]:
|
|
2630
2716
|
return result
|
|
2717
|
+
if manual_paste_redirect_uri:
|
|
2718
|
+
raw_paste = _read_ready_stdin_line()
|
|
2719
|
+
if raw_paste and raw_paste.strip():
|
|
2720
|
+
pasted = _parse_pasted_callback(raw_paste)
|
|
2721
|
+
pasted["_manual_paste"] = True
|
|
2722
|
+
return pasted
|
|
2631
2723
|
time.sleep(0.1)
|
|
2632
2724
|
finally:
|
|
2633
2725
|
server.shutdown()
|
|
@@ -2651,6 +2743,21 @@ def _xai_wait_for_callback(
|
|
|
2651
2743
|
)
|
|
2652
2744
|
|
|
2653
2745
|
|
|
2746
|
+
def _read_ready_stdin_line() -> Optional[str]:
|
|
2747
|
+
"""Return one pending stdin line without blocking, if the terminal has one."""
|
|
2748
|
+
try:
|
|
2749
|
+
if not sys.stdin.isatty():
|
|
2750
|
+
return None
|
|
2751
|
+
import select
|
|
2752
|
+
|
|
2753
|
+
ready, _, _ = select.select([sys.stdin], [], [], 0)
|
|
2754
|
+
if not ready:
|
|
2755
|
+
return None
|
|
2756
|
+
return sys.stdin.readline()
|
|
2757
|
+
except Exception:
|
|
2758
|
+
return None
|
|
2759
|
+
|
|
2760
|
+
|
|
2654
2761
|
def _spotify_token_payload_to_state(
|
|
2655
2762
|
token_payload: Dict[str, Any],
|
|
2656
2763
|
*,
|
|
@@ -3330,6 +3437,7 @@ def _sync_codex_pool_entries(
|
|
|
3330
3437
|
auth_store: Dict[str, Any],
|
|
3331
3438
|
tokens: Dict[str, str],
|
|
3332
3439
|
last_refresh: Optional[str],
|
|
3440
|
+
previous_singleton_tokens: Optional[Dict[str, str]] = None,
|
|
3333
3441
|
) -> None:
|
|
3334
3442
|
"""Mirror a fresh Codex re-auth into the credential_pool OAuth entries.
|
|
3335
3443
|
|
|
@@ -3345,24 +3453,34 @@ def _sync_codex_pool_entries(
|
|
|
3345
3453
|
OAuth flow when the user logged in via ``hermes setup`` / the model
|
|
3346
3454
|
picker. Always synced with the fresh tokens.
|
|
3347
3455
|
* ``manual:device_code`` — entries created by ``hermes auth add openai-codex``
|
|
3348
|
-
that use the same device-code OAuth mechanism.
|
|
3349
|
-
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
|
|
3353
|
-
|
|
3456
|
+
that use the same device-code OAuth mechanism. ONLY synced if the
|
|
3457
|
+
entry's existing access_token matches the *previous* singleton
|
|
3458
|
+
access_token (i.e. the entry is a legacy singleton-alias from the
|
|
3459
|
+
#33000 workaround era). Manual entries whose tokens never matched the
|
|
3460
|
+
singleton represent INDEPENDENT accounts added via
|
|
3461
|
+
``hermes auth add openai-codex`` and must not be overwritten by a
|
|
3462
|
+
re-auth that targeted a different account (regression for #39236).
|
|
3463
|
+
|
|
3464
|
+
The original #33538 fix refreshed every ``manual:device_code`` entry
|
|
3465
|
+
unconditionally. That worked when ``manual:device_code`` only meant
|
|
3466
|
+
"legacy alias of the singleton", but the same source string is now
|
|
3467
|
+
also produced by independent-account additions, and the broad sync
|
|
3468
|
+
silently clobbered distinct accounts with the latest-authenticated
|
|
3469
|
+
token pair. The access_token-match check distinguishes the two cases
|
|
3470
|
+
without changing the source-string contract.
|
|
3354
3471
|
|
|
3355
3472
|
What does NOT get refreshed:
|
|
3356
3473
|
|
|
3357
3474
|
* ``manual:api_key`` and any other non-device-code manual sources — those
|
|
3358
3475
|
are independent credentials (an explicit API key, a different ChatGPT
|
|
3359
3476
|
account, etc.) and must not be overwritten by a single re-auth.
|
|
3477
|
+
* ``manual:device_code`` entries whose access_token does NOT match the
|
|
3478
|
+
previous singleton — see above; these are independent accounts.
|
|
3360
3479
|
|
|
3361
|
-
Error markers (``last_status``, ``last_error_*``) are
|
|
3362
|
-
|
|
3363
|
-
|
|
3364
|
-
|
|
3365
|
-
pre-re-auth 401.
|
|
3480
|
+
Error markers (``last_status``, ``last_error_*``) are cleared ONLY on
|
|
3481
|
+
entries that actually had their tokens rewritten by this re-auth.
|
|
3482
|
+
Independent entries keep their own error state (their 401/429 markers
|
|
3483
|
+
belong to that account's own auth flow, not this re-auth).
|
|
3366
3484
|
"""
|
|
3367
3485
|
access_token = tokens.get("access_token")
|
|
3368
3486
|
if not access_token:
|
|
@@ -3374,15 +3492,34 @@ def _sync_codex_pool_entries(
|
|
|
3374
3492
|
entries = pool.get("openai-codex")
|
|
3375
3493
|
if not isinstance(entries, list):
|
|
3376
3494
|
return
|
|
3377
|
-
#
|
|
3378
|
-
#
|
|
3379
|
-
#
|
|
3380
|
-
|
|
3495
|
+
# Previous singleton access_token (before this re-auth overwrote it) —
|
|
3496
|
+
# used to distinguish legacy singleton-aliases from independent accounts.
|
|
3497
|
+
# When None or empty, no manual entry can be treated as an alias (which
|
|
3498
|
+
# is the right default for first-ever-save or a freshly initialized
|
|
3499
|
+
# auth.json).
|
|
3500
|
+
prev_at = None
|
|
3501
|
+
if isinstance(previous_singleton_tokens, dict):
|
|
3502
|
+
prev_at = previous_singleton_tokens.get("access_token") or None
|
|
3381
3503
|
for entry in entries:
|
|
3382
3504
|
if not isinstance(entry, dict):
|
|
3383
3505
|
continue
|
|
3384
3506
|
source = entry.get("source")
|
|
3385
|
-
if source
|
|
3507
|
+
if source == "device_code":
|
|
3508
|
+
# Singleton-seeded mirror — always refresh.
|
|
3509
|
+
refresh_this_entry = True
|
|
3510
|
+
elif source == "manual:device_code":
|
|
3511
|
+
# Refresh only if this entry's existing access_token matches the
|
|
3512
|
+
# previous singleton access_token (i.e. it is a true alias of the
|
|
3513
|
+
# singleton from the #33000 workaround era). An entry with its
|
|
3514
|
+
# own distinct token material is an independent account and must
|
|
3515
|
+
# be left alone (#39236).
|
|
3516
|
+
refresh_this_entry = bool(
|
|
3517
|
+
prev_at and entry.get("access_token") == prev_at
|
|
3518
|
+
)
|
|
3519
|
+
else:
|
|
3520
|
+
# ``manual:api_key`` and any future non-device-code sources.
|
|
3521
|
+
refresh_this_entry = False
|
|
3522
|
+
if not refresh_this_entry:
|
|
3386
3523
|
continue
|
|
3387
3524
|
entry["access_token"] = access_token
|
|
3388
3525
|
if refresh_token:
|
|
@@ -3404,16 +3541,43 @@ def _save_codex_tokens(tokens: Dict[str, str], last_refresh: str = None, label:
|
|
|
3404
3541
|
with _auth_store_lock():
|
|
3405
3542
|
auth_store = _load_auth_store()
|
|
3406
3543
|
state = _load_provider_state(auth_store, "openai-codex") or {}
|
|
3544
|
+
# Capture the previous singleton tokens BEFORE overwriting them. The
|
|
3545
|
+
# pool-sync step uses this to distinguish legacy singleton-aliases
|
|
3546
|
+
# (which should be refreshed) from independent accounts that
|
|
3547
|
+
# ``hermes auth add openai-codex`` created (which must not be
|
|
3548
|
+
# overwritten — see #39236).
|
|
3549
|
+
previous_singleton_tokens = state.get("tokens") if isinstance(state.get("tokens"), dict) else None
|
|
3407
3550
|
state["tokens"] = tokens
|
|
3408
3551
|
state["last_refresh"] = last_refresh
|
|
3409
3552
|
state["auth_mode"] = "chatgpt"
|
|
3410
3553
|
if label and str(label).strip():
|
|
3411
3554
|
state["label"] = str(label).strip()
|
|
3412
3555
|
_save_provider_state(auth_store, "openai-codex", state)
|
|
3413
|
-
_sync_codex_pool_entries(
|
|
3556
|
+
_sync_codex_pool_entries(
|
|
3557
|
+
auth_store,
|
|
3558
|
+
tokens,
|
|
3559
|
+
last_refresh,
|
|
3560
|
+
previous_singleton_tokens=previous_singleton_tokens,
|
|
3561
|
+
)
|
|
3414
3562
|
_save_auth_store(auth_store)
|
|
3415
3563
|
|
|
3416
3564
|
|
|
3565
|
+
def _recover_codex_tokens_from_cli(reason: str) -> Optional[Dict[str, str]]:
|
|
3566
|
+
"""Adopt a valid Codex CLI token pair into Hermes auth, if available."""
|
|
3567
|
+
imported = _import_codex_cli_tokens()
|
|
3568
|
+
# Require BOTH tokens before adopting: persisting a payload without a
|
|
3569
|
+
# usable refresh_token would only break the next refresh cycle.
|
|
3570
|
+
if not (
|
|
3571
|
+
imported
|
|
3572
|
+
and str(imported.get("access_token", "") or "").strip()
|
|
3573
|
+
and str(imported.get("refresh_token", "") or "").strip()
|
|
3574
|
+
):
|
|
3575
|
+
return None
|
|
3576
|
+
logger.info("Codex auth recovered from Codex CLI auth.json (%s).", reason)
|
|
3577
|
+
_save_codex_tokens(imported)
|
|
3578
|
+
return dict(imported)
|
|
3579
|
+
|
|
3580
|
+
|
|
3417
3581
|
def refresh_codex_oauth_pure(
|
|
3418
3582
|
access_token: str,
|
|
3419
3583
|
refresh_token: str,
|
|
@@ -3550,11 +3714,34 @@ def _refresh_codex_auth_tokens(
|
|
|
3550
3714
|
|
|
3551
3715
|
Saves the new tokens to Hermes auth store automatically.
|
|
3552
3716
|
"""
|
|
3553
|
-
|
|
3554
|
-
|
|
3555
|
-
|
|
3556
|
-
|
|
3557
|
-
|
|
3717
|
+
try:
|
|
3718
|
+
refreshed = refresh_codex_oauth_pure(
|
|
3719
|
+
str(tokens.get("access_token", "") or ""),
|
|
3720
|
+
str(tokens.get("refresh_token", "") or ""),
|
|
3721
|
+
timeout_seconds=timeout_seconds,
|
|
3722
|
+
)
|
|
3723
|
+
except AuthError as exc:
|
|
3724
|
+
# Self-heal cross-store refresh_token rotation. Hermes keeps its OWN
|
|
3725
|
+
# Codex OAuth token (per profile + top-level), separate from the Codex
|
|
3726
|
+
# CLI's ~/.codex/auth.json. OAuth refresh_tokens are single-use, so when
|
|
3727
|
+
# the Codex CLI (or another Hermes process) rotates the shared token,
|
|
3728
|
+
# this frozen copy's refresh_token goes stale and the refresh fails with
|
|
3729
|
+
# a relogin-required error (invalid_grant / refresh_token_reused / 401).
|
|
3730
|
+
# Before surfacing that as a hard 401 to the turn, adopt the canonical
|
|
3731
|
+
# fresh token from ~/.codex/auth.json (the Codex CLI keeps it current) so
|
|
3732
|
+
# idle profiles / desktop sessions recover automatically instead of
|
|
3733
|
+
# 401'ing until a manual re-auth. Transient failures (e.g. 429 quota)
|
|
3734
|
+
# keep relogin_required=False — the stored token is still valid there, so
|
|
3735
|
+
# we never self-heal those and re-raise unchanged.
|
|
3736
|
+
if not getattr(exc, "relogin_required", False):
|
|
3737
|
+
raise
|
|
3738
|
+
imported = _recover_codex_tokens_from_cli(
|
|
3739
|
+
f"refresh_token rejected: {getattr(exc, 'code', None) or 'auth_error'}"
|
|
3740
|
+
)
|
|
3741
|
+
if not imported:
|
|
3742
|
+
raise
|
|
3743
|
+
return imported
|
|
3744
|
+
|
|
3558
3745
|
updated_tokens = dict(tokens)
|
|
3559
3746
|
updated_tokens["access_token"] = refreshed["access_token"]
|
|
3560
3747
|
updated_tokens["refresh_token"] = refreshed["refresh_token"]
|
|
@@ -3614,9 +3801,25 @@ def resolve_codex_runtime_credentials(
|
|
|
3614
3801
|
HTTP 401 ``Missing Authentication header`` from the wire instead of a usable
|
|
3615
3802
|
credential. See issue #32992.
|
|
3616
3803
|
"""
|
|
3804
|
+
read_error: Optional[AuthError] = None
|
|
3617
3805
|
try:
|
|
3618
3806
|
data = _read_codex_tokens()
|
|
3619
|
-
except AuthError:
|
|
3807
|
+
except AuthError as exc:
|
|
3808
|
+
read_error = exc
|
|
3809
|
+
if getattr(exc, "relogin_required", False) and getattr(exc, "code", None) in {
|
|
3810
|
+
"codex_auth_missing_access_token",
|
|
3811
|
+
"codex_auth_missing_refresh_token",
|
|
3812
|
+
"codex_auth_invalid_shape",
|
|
3813
|
+
}:
|
|
3814
|
+
imported = _recover_codex_tokens_from_cli(str(getattr(exc, "code", None) or "auth_error"))
|
|
3815
|
+
if imported:
|
|
3816
|
+
data = {"tokens": imported, "last_refresh": imported.get("last_refresh")}
|
|
3817
|
+
else:
|
|
3818
|
+
data = None
|
|
3819
|
+
else:
|
|
3820
|
+
data = None
|
|
3821
|
+
|
|
3822
|
+
if data is None:
|
|
3620
3823
|
pool_token = _pool_codex_access_token()
|
|
3621
3824
|
if pool_token:
|
|
3622
3825
|
base_url = (
|
|
@@ -3631,7 +3834,34 @@ def resolve_codex_runtime_credentials(
|
|
|
3631
3834
|
"last_refresh": None,
|
|
3632
3835
|
"auth_mode": "chatgpt",
|
|
3633
3836
|
}
|
|
3634
|
-
|
|
3837
|
+
pool_rate_limit = _codex_pool_rate_limit_status()
|
|
3838
|
+
if pool_rate_limit:
|
|
3839
|
+
reset_at = pool_rate_limit.get("reset_at")
|
|
3840
|
+
if isinstance(reset_at, (int, float)) and reset_at > time.time():
|
|
3841
|
+
remaining = int(reset_at - time.time())
|
|
3842
|
+
message = (
|
|
3843
|
+
f"Codex provider quota exhausted (429); retry after {remaining}s. "
|
|
3844
|
+
"Credentials are still valid."
|
|
3845
|
+
)
|
|
3846
|
+
else:
|
|
3847
|
+
message = (
|
|
3848
|
+
"Codex provider quota exhausted (429). Credentials are still valid; "
|
|
3849
|
+
"retry after the usage limit resets."
|
|
3850
|
+
)
|
|
3851
|
+
raise AuthError(
|
|
3852
|
+
message,
|
|
3853
|
+
provider="openai-codex",
|
|
3854
|
+
code=CODEX_RATE_LIMITED_CODE,
|
|
3855
|
+
relogin_required=False,
|
|
3856
|
+
)
|
|
3857
|
+
if read_error is not None:
|
|
3858
|
+
raise read_error
|
|
3859
|
+
raise AuthError(
|
|
3860
|
+
"No Codex credentials stored. Run `hermes auth` to authenticate.",
|
|
3861
|
+
provider="openai-codex",
|
|
3862
|
+
code="codex_auth_missing",
|
|
3863
|
+
relogin_required=True,
|
|
3864
|
+
)
|
|
3635
3865
|
|
|
3636
3866
|
tokens = dict(data["tokens"])
|
|
3637
3867
|
access_token = str(tokens.get("access_token", "") or "").strip()
|
|
@@ -3670,6 +3900,79 @@ def resolve_codex_runtime_credentials(
|
|
|
3670
3900
|
}
|
|
3671
3901
|
|
|
3672
3902
|
|
|
3903
|
+
def _codex_pool_rate_limit_status() -> Optional[Dict[str, Any]]:
|
|
3904
|
+
"""Return metadata for a pool-only Codex credential in quota cooldown."""
|
|
3905
|
+
def _parse_reset_at(value: Any) -> Optional[float]:
|
|
3906
|
+
if value is None or value == "":
|
|
3907
|
+
return None
|
|
3908
|
+
if isinstance(value, (int, float)):
|
|
3909
|
+
numeric = float(value)
|
|
3910
|
+
if numeric <= 0:
|
|
3911
|
+
return None
|
|
3912
|
+
return numeric / 1000.0 if numeric > 1_000_000_000_000 else numeric
|
|
3913
|
+
if isinstance(value, str):
|
|
3914
|
+
raw = value.strip()
|
|
3915
|
+
if not raw:
|
|
3916
|
+
return None
|
|
3917
|
+
try:
|
|
3918
|
+
numeric = float(raw)
|
|
3919
|
+
except ValueError:
|
|
3920
|
+
numeric = None
|
|
3921
|
+
if numeric is not None:
|
|
3922
|
+
return numeric / 1000.0 if numeric > 1_000_000_000_000 else numeric
|
|
3923
|
+
try:
|
|
3924
|
+
return datetime.fromisoformat(raw.replace("Z", "+00:00")).timestamp()
|
|
3925
|
+
except ValueError:
|
|
3926
|
+
return None
|
|
3927
|
+
return None
|
|
3928
|
+
|
|
3929
|
+
try:
|
|
3930
|
+
with _auth_store_lock():
|
|
3931
|
+
auth_store = _load_auth_store()
|
|
3932
|
+
pool = auth_store.get("credential_pool")
|
|
3933
|
+
if not isinstance(pool, dict):
|
|
3934
|
+
return None
|
|
3935
|
+
entries = pool.get("openai-codex")
|
|
3936
|
+
if not isinstance(entries, list):
|
|
3937
|
+
return None
|
|
3938
|
+
now = time.time()
|
|
3939
|
+
for entry in entries:
|
|
3940
|
+
if not isinstance(entry, dict):
|
|
3941
|
+
continue
|
|
3942
|
+
token = entry.get("access_token")
|
|
3943
|
+
if not isinstance(token, str) or not token.strip():
|
|
3944
|
+
continue
|
|
3945
|
+
if entry.get("last_status") != "exhausted":
|
|
3946
|
+
continue
|
|
3947
|
+
code = entry.get("last_error_code")
|
|
3948
|
+
reason = str(entry.get("last_error_reason") or "").lower()
|
|
3949
|
+
message = str(entry.get("last_error_message") or "").lower()
|
|
3950
|
+
is_rate_limited = (
|
|
3951
|
+
code == 429
|
|
3952
|
+
or "rate_limit" in reason
|
|
3953
|
+
or "usage_limit" in reason
|
|
3954
|
+
or "quota" in reason
|
|
3955
|
+
or "rate limit" in message
|
|
3956
|
+
or "usage limit" in message
|
|
3957
|
+
or "quota" in message
|
|
3958
|
+
)
|
|
3959
|
+
if not is_rate_limited:
|
|
3960
|
+
continue
|
|
3961
|
+
reset_at = _parse_reset_at(entry.get("last_error_reset_at"))
|
|
3962
|
+
if reset_at is not None and reset_at <= now:
|
|
3963
|
+
continue
|
|
3964
|
+
return {
|
|
3965
|
+
"label": entry.get("label"),
|
|
3966
|
+
"last_refresh": entry.get("last_refresh"),
|
|
3967
|
+
"reset_at": reset_at,
|
|
3968
|
+
"reason": entry.get("last_error_reason"),
|
|
3969
|
+
"message": entry.get("last_error_message"),
|
|
3970
|
+
}
|
|
3971
|
+
except Exception:
|
|
3972
|
+
logger.debug("Codex pool rate-limit lookup failed", exc_info=True)
|
|
3973
|
+
return None
|
|
3974
|
+
|
|
3975
|
+
|
|
3673
3976
|
def _pool_codex_access_token() -> str:
|
|
3674
3977
|
"""Return the most-recent usable access_token from the openai-codex pool.
|
|
3675
3978
|
|
|
@@ -3714,13 +4017,64 @@ def _pool_codex_access_token() -> str:
|
|
|
3714
4017
|
# xAI Grok OAuth — tokens stored in ~/.hermes/auth.json
|
|
3715
4018
|
# =============================================================================
|
|
3716
4019
|
|
|
4020
|
+
def _xai_oauth_state_from_store(auth_store: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
|
4021
|
+
"""Return usable xAI OAuth state from provider state or credential pool."""
|
|
4022
|
+
state = _load_provider_state(auth_store, "xai-oauth")
|
|
4023
|
+
tokens = state.get("tokens") if isinstance(state, dict) else None
|
|
4024
|
+
if isinstance(tokens, dict):
|
|
4025
|
+
access_token = str(tokens.get("access_token", "") or "").strip()
|
|
4026
|
+
refresh_token = str(tokens.get("refresh_token", "") or "").strip()
|
|
4027
|
+
if access_token and refresh_token:
|
|
4028
|
+
return state
|
|
4029
|
+
|
|
4030
|
+
credential_pool = auth_store.get("credential_pool")
|
|
4031
|
+
entries = (
|
|
4032
|
+
credential_pool.get("xai-oauth")
|
|
4033
|
+
if isinstance(credential_pool, dict)
|
|
4034
|
+
else None
|
|
4035
|
+
)
|
|
4036
|
+
if isinstance(entries, list):
|
|
4037
|
+
for entry in entries:
|
|
4038
|
+
if not isinstance(entry, dict):
|
|
4039
|
+
continue
|
|
4040
|
+
access_token = str(entry.get("access_token", "") or "").strip()
|
|
4041
|
+
refresh_token = str(entry.get("refresh_token", "") or "").strip()
|
|
4042
|
+
if not access_token or not refresh_token:
|
|
4043
|
+
continue
|
|
4044
|
+
merged = dict(state or {})
|
|
4045
|
+
merged["tokens"] = {
|
|
4046
|
+
"access_token": access_token,
|
|
4047
|
+
"refresh_token": refresh_token,
|
|
4048
|
+
"token_type": str(entry.get("token_type") or "Bearer"),
|
|
4049
|
+
}
|
|
4050
|
+
if entry.get("last_refresh"):
|
|
4051
|
+
merged["last_refresh"] = entry.get("last_refresh")
|
|
4052
|
+
merged.setdefault("auth_mode", "oauth_pkce")
|
|
4053
|
+
return merged
|
|
4054
|
+
|
|
4055
|
+
return state if isinstance(state, dict) else None
|
|
4056
|
+
|
|
4057
|
+
|
|
4058
|
+
def _xai_oauth_state_has_usable_tokens(state: Optional[Dict[str, Any]]) -> bool:
|
|
4059
|
+
tokens = state.get("tokens") if isinstance(state, dict) else None
|
|
4060
|
+
return (
|
|
4061
|
+
isinstance(tokens, dict)
|
|
4062
|
+
and bool(str(tokens.get("access_token", "") or "").strip())
|
|
4063
|
+
and bool(str(tokens.get("refresh_token", "") or "").strip())
|
|
4064
|
+
)
|
|
4065
|
+
|
|
4066
|
+
|
|
3717
4067
|
def _read_xai_oauth_tokens(*, _lock: bool = True) -> Dict[str, Any]:
|
|
3718
4068
|
if _lock:
|
|
3719
4069
|
with _auth_store_lock():
|
|
3720
4070
|
auth_store = _load_auth_store()
|
|
3721
4071
|
else:
|
|
3722
4072
|
auth_store = _load_auth_store()
|
|
3723
|
-
state =
|
|
4073
|
+
state = _xai_oauth_state_from_store(auth_store)
|
|
4074
|
+
if not _xai_oauth_state_has_usable_tokens(state):
|
|
4075
|
+
global_state = _xai_oauth_state_from_store(_load_global_auth_store())
|
|
4076
|
+
if _xai_oauth_state_has_usable_tokens(global_state):
|
|
4077
|
+
state = global_state
|
|
3724
4078
|
if not state:
|
|
3725
4079
|
raise AuthError(
|
|
3726
4080
|
"No xAI OAuth credentials stored. Select xAI Grok OAuth (SuperGrok / Premium+) in `hermes model`.",
|
|
@@ -3760,6 +4114,62 @@ def _read_xai_oauth_tokens(*, _lock: bool = True) -> Dict[str, Any]:
|
|
|
3760
4114
|
}
|
|
3761
4115
|
|
|
3762
4116
|
|
|
4117
|
+
def _profile_has_own_xai_oauth_state(auth_store: Dict[str, Any]) -> bool:
|
|
4118
|
+
"""True when this store has its OWN ``providers.xai-oauth`` block.
|
|
4119
|
+
|
|
4120
|
+
Distinguishes a profile that genuinely shadows the root xAI grant from
|
|
4121
|
+
one that only *reads* root via ``_load_provider_state``'s fallback. Only
|
|
4122
|
+
the latter needs the refresh write-through below.
|
|
4123
|
+
"""
|
|
4124
|
+
providers = auth_store.get("providers")
|
|
4125
|
+
return isinstance(providers, dict) and isinstance(providers.get("xai-oauth"), dict)
|
|
4126
|
+
|
|
4127
|
+
|
|
4128
|
+
def _write_through_xai_oauth_to_global_root(state: Dict[str, Any]) -> None:
|
|
4129
|
+
"""Persist a rotated xAI OAuth ``state`` into the global-root auth.json.
|
|
4130
|
+
|
|
4131
|
+
Best-effort write-through for the multi-profile rotation hazard (#43589):
|
|
4132
|
+
xAI rotates the refresh_token on every refresh, so when a profile session
|
|
4133
|
+
refreshes a grant it resolved from the root fallback, the rotated chain
|
|
4134
|
+
must land back in root. Otherwise root keeps a now-revoked refresh token
|
|
4135
|
+
and every other profile reading the stale root grant dies with
|
|
4136
|
+
``invalid_grant`` once its access token expires.
|
|
4137
|
+
|
|
4138
|
+
Only updates ``providers.xai-oauth`` in the root store; never touches the
|
|
4139
|
+
profile store (the caller already saved that). Swallows all errors — a
|
|
4140
|
+
failed write-through degrades to the pre-existing behavior (root stale),
|
|
4141
|
+
it must never break the profile's own successful save.
|
|
4142
|
+
"""
|
|
4143
|
+
global_path = _global_auth_file_path()
|
|
4144
|
+
if global_path is None:
|
|
4145
|
+
# Classic mode (profile == root); the profile save already hit root.
|
|
4146
|
+
return
|
|
4147
|
+
# Seat belt: under pytest, refuse to write the real user's
|
|
4148
|
+
# ~/.hermes/auth.json even when HERMES_HOME points at a profile path
|
|
4149
|
+
# (mirrors the read-side guard in _load_global_auth_store). Uses the
|
|
4150
|
+
# unmodified HOME env, not Path.home() which fixtures may monkeypatch.
|
|
4151
|
+
if os.environ.get("PYTEST_CURRENT_TEST"):
|
|
4152
|
+
real_home_env = os.environ.get("HOME", "")
|
|
4153
|
+
if real_home_env:
|
|
4154
|
+
real_root = Path(real_home_env) / ".hermes" / "auth.json"
|
|
4155
|
+
try:
|
|
4156
|
+
if global_path.resolve(strict=False) == real_root.resolve(strict=False):
|
|
4157
|
+
return
|
|
4158
|
+
except Exception:
|
|
4159
|
+
return
|
|
4160
|
+
try:
|
|
4161
|
+
if global_path.exists():
|
|
4162
|
+
global_store = _load_auth_store(global_path)
|
|
4163
|
+
else:
|
|
4164
|
+
global_store = {}
|
|
4165
|
+
if not isinstance(global_store, dict):
|
|
4166
|
+
return
|
|
4167
|
+
_store_provider_state(global_store, "xai-oauth", dict(state), set_active=False)
|
|
4168
|
+
_save_auth_store(global_store, global_path)
|
|
4169
|
+
except Exception as exc: # pragma: no cover - best effort
|
|
4170
|
+
logger.debug("xAI OAuth: write-through to global root failed: %s", exc)
|
|
4171
|
+
|
|
4172
|
+
|
|
3763
4173
|
def _save_xai_oauth_tokens(
|
|
3764
4174
|
tokens: Dict[str, Any],
|
|
3765
4175
|
*,
|
|
@@ -3771,6 +4181,11 @@ def _save_xai_oauth_tokens(
|
|
|
3771
4181
|
last_refresh = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
|
|
3772
4182
|
with _auth_store_lock():
|
|
3773
4183
|
auth_store = _load_auth_store()
|
|
4184
|
+
# A profile that lacks its own xai-oauth block is reading the root
|
|
4185
|
+
# grant through _load_provider_state's fallback. When such a profile
|
|
4186
|
+
# refreshes the (rotating) grant, we must write the rotated chain back
|
|
4187
|
+
# to root too, or root is left holding a revoked refresh token (#43589).
|
|
4188
|
+
write_through_to_root = not _profile_has_own_xai_oauth_state(auth_store)
|
|
3774
4189
|
state = _load_provider_state(auth_store, "xai-oauth") or {}
|
|
3775
4190
|
state["tokens"] = tokens
|
|
3776
4191
|
state["last_refresh"] = last_refresh
|
|
@@ -3781,6 +4196,8 @@ def _save_xai_oauth_tokens(
|
|
|
3781
4196
|
state["redirect_uri"] = redirect_uri
|
|
3782
4197
|
_save_provider_state(auth_store, "xai-oauth", state)
|
|
3783
4198
|
_save_auth_store(auth_store)
|
|
4199
|
+
if write_through_to_root:
|
|
4200
|
+
_write_through_xai_oauth_to_global_root(state)
|
|
3784
4201
|
|
|
3785
4202
|
|
|
3786
4203
|
def _xai_access_token_is_expiring(access_token: str, skew_seconds: int = 0) -> bool:
|
|
@@ -5467,18 +5884,24 @@ def _snapshot_nous_pool_status() -> Dict[str, Any]:
|
|
|
5467
5884
|
# subscription-feature checks) call it many times per render — `hermes tools` → "All Platforms"
|
|
5468
5885
|
# was firing the refresh ~31× during one menu paint, racking up >13s of HTTP and burning
|
|
5469
5886
|
# single-use refresh tokens. Cache the snapshot for a few seconds, keyed on the auth.json
|
|
5470
|
-
# mtime so that
|
|
5887
|
+
# path + mtime so that profile switches do not share a process memo and
|
|
5888
|
+
# `hermes auth login/logout/add/remove` invalidate naturally on the next call.
|
|
5471
5889
|
_NOUS_AUTH_STATUS_CACHE_TTL = 15.0 # seconds
|
|
5472
|
-
_nous_auth_status_cache: Optional[Tuple[float, Optional[float], Dict[str, Any]]] = None
|
|
5890
|
+
_nous_auth_status_cache: Optional[Tuple[float, str, Optional[float], Dict[str, Any]]] = None
|
|
5473
5891
|
|
|
5474
5892
|
|
|
5475
|
-
def
|
|
5893
|
+
def _auth_file_cache_key() -> Tuple[str, Optional[float]]:
|
|
5894
|
+
auth_file = _auth_file_path()
|
|
5476
5895
|
try:
|
|
5477
|
-
|
|
5896
|
+
auth_file_key = str(auth_file.resolve(strict=False))
|
|
5897
|
+
except Exception:
|
|
5898
|
+
auth_file_key = str(auth_file)
|
|
5899
|
+
try:
|
|
5900
|
+
return auth_file_key, auth_file.stat().st_mtime
|
|
5478
5901
|
except FileNotFoundError:
|
|
5479
|
-
return None
|
|
5902
|
+
return auth_file_key, None
|
|
5480
5903
|
except Exception:
|
|
5481
|
-
return None
|
|
5904
|
+
return auth_file_key, None
|
|
5482
5905
|
|
|
5483
5906
|
|
|
5484
5907
|
def invalidate_nous_auth_status_cache() -> None:
|
|
@@ -5510,18 +5933,19 @@ def get_nous_auth_status() -> Dict[str, Any]:
|
|
|
5510
5933
|
"""
|
|
5511
5934
|
global _nous_auth_status_cache
|
|
5512
5935
|
now = time.monotonic()
|
|
5513
|
-
mtime =
|
|
5936
|
+
auth_file_key, mtime = _auth_file_cache_key()
|
|
5514
5937
|
cached = _nous_auth_status_cache
|
|
5515
5938
|
if cached is not None:
|
|
5516
|
-
cached_at, cached_mtime, cached_status = cached
|
|
5939
|
+
cached_at, cached_auth_file_key, cached_mtime, cached_status = cached
|
|
5517
5940
|
if (
|
|
5518
|
-
|
|
5941
|
+
cached_auth_file_key == auth_file_key
|
|
5942
|
+
and cached_mtime == mtime
|
|
5519
5943
|
and (now - cached_at) < _NOUS_AUTH_STATUS_CACHE_TTL
|
|
5520
5944
|
):
|
|
5521
5945
|
return dict(cached_status)
|
|
5522
5946
|
|
|
5523
5947
|
status = _compute_nous_auth_status()
|
|
5524
|
-
_nous_auth_status_cache = (now, mtime, dict(status))
|
|
5948
|
+
_nous_auth_status_cache = (now, auth_file_key, mtime, dict(status))
|
|
5525
5949
|
return status
|
|
5526
5950
|
|
|
5527
5951
|
|
|
@@ -5604,6 +6028,22 @@ def get_codex_auth_status() -> Dict[str, Any]:
|
|
|
5604
6028
|
"source": f"pool:{getattr(entry, 'label', 'unknown')}",
|
|
5605
6029
|
"api_key": api_key,
|
|
5606
6030
|
}
|
|
6031
|
+
rate_limit = _codex_pool_rate_limit_status()
|
|
6032
|
+
if rate_limit:
|
|
6033
|
+
return {
|
|
6034
|
+
"logged_in": True,
|
|
6035
|
+
"auth_store": str(_auth_file_path()),
|
|
6036
|
+
"last_refresh": rate_limit.get("last_refresh"),
|
|
6037
|
+
"auth_mode": "chatgpt",
|
|
6038
|
+
"source": f"pool:{rate_limit.get('label') or 'unknown'}",
|
|
6039
|
+
"rate_limited": True,
|
|
6040
|
+
"error_code": CODEX_RATE_LIMITED_CODE,
|
|
6041
|
+
"error": (
|
|
6042
|
+
rate_limit.get("message")
|
|
6043
|
+
or "Codex provider quota exhausted; retry after the usage limit resets."
|
|
6044
|
+
),
|
|
6045
|
+
"reset_at": rate_limit.get("reset_at"),
|
|
6046
|
+
}
|
|
5607
6047
|
except Exception:
|
|
5608
6048
|
pass
|
|
5609
6049
|
|
|
@@ -6069,6 +6509,40 @@ def _reset_config_provider() -> Path:
|
|
|
6069
6509
|
return config_path
|
|
6070
6510
|
|
|
6071
6511
|
|
|
6512
|
+
def _confirm_expensive_model_selection(
|
|
6513
|
+
model_id: str,
|
|
6514
|
+
*,
|
|
6515
|
+
provider: str = "",
|
|
6516
|
+
base_url: str = "",
|
|
6517
|
+
api_key: str = "",
|
|
6518
|
+
) -> bool:
|
|
6519
|
+
"""Prompt before saving a model whose known pricing exceeds guardrails."""
|
|
6520
|
+
try:
|
|
6521
|
+
from hermes_cli.model_cost_guard import expensive_model_warning
|
|
6522
|
+
|
|
6523
|
+
warning = expensive_model_warning(
|
|
6524
|
+
model_id,
|
|
6525
|
+
provider=provider,
|
|
6526
|
+
base_url=base_url,
|
|
6527
|
+
api_key=api_key,
|
|
6528
|
+
)
|
|
6529
|
+
except Exception:
|
|
6530
|
+
warning = None
|
|
6531
|
+
if warning is None:
|
|
6532
|
+
return True
|
|
6533
|
+
|
|
6534
|
+
print()
|
|
6535
|
+
print("=" * 72)
|
|
6536
|
+
print(warning.message)
|
|
6537
|
+
print("=" * 72)
|
|
6538
|
+
try:
|
|
6539
|
+
response = input("Switch anyway? [y/N]: ").strip().lower()
|
|
6540
|
+
except (KeyboardInterrupt, EOFError):
|
|
6541
|
+
print()
|
|
6542
|
+
return False
|
|
6543
|
+
return response in {"y", "yes"}
|
|
6544
|
+
|
|
6545
|
+
|
|
6072
6546
|
def _prompt_model_selection(
|
|
6073
6547
|
model_ids: List[str],
|
|
6074
6548
|
current_model: str = "",
|
|
@@ -6076,6 +6550,9 @@ def _prompt_model_selection(
|
|
|
6076
6550
|
unavailable_models: Optional[List[str]] = None,
|
|
6077
6551
|
portal_url: str = "",
|
|
6078
6552
|
unavailable_message: str = "",
|
|
6553
|
+
confirm_provider: str = "",
|
|
6554
|
+
confirm_base_url: str = "",
|
|
6555
|
+
confirm_api_key: str = "",
|
|
6079
6556
|
) -> Optional[str]:
|
|
6080
6557
|
"""Interactive model selection. Puts current_model first with a marker. Returns chosen model ID or None.
|
|
6081
6558
|
|
|
@@ -6089,6 +6566,18 @@ def _prompt_model_selection(
|
|
|
6089
6566
|
|
|
6090
6567
|
_unavailable = unavailable_models or []
|
|
6091
6568
|
|
|
6569
|
+
def _confirmed_selection(mid: str) -> Optional[str]:
|
|
6570
|
+
if not mid:
|
|
6571
|
+
return None
|
|
6572
|
+
if confirm_provider and not _confirm_expensive_model_selection(
|
|
6573
|
+
mid,
|
|
6574
|
+
provider=confirm_provider,
|
|
6575
|
+
base_url=confirm_base_url,
|
|
6576
|
+
api_key=confirm_api_key,
|
|
6577
|
+
):
|
|
6578
|
+
return None
|
|
6579
|
+
return mid
|
|
6580
|
+
|
|
6092
6581
|
# Reorder: current model first, then the rest (deduplicated)
|
|
6093
6582
|
ordered = []
|
|
6094
6583
|
if current_model and current_model in model_ids:
|
|
@@ -6204,13 +6693,13 @@ def _prompt_model_selection(
|
|
|
6204
6693
|
return None
|
|
6205
6694
|
print()
|
|
6206
6695
|
if idx < len(ordered):
|
|
6207
|
-
return ordered[idx]
|
|
6696
|
+
return _confirmed_selection(ordered[idx])
|
|
6208
6697
|
elif idx == len(ordered):
|
|
6209
6698
|
try:
|
|
6210
6699
|
custom = input("Enter model name: ").strip()
|
|
6211
6700
|
except (EOFError, KeyboardInterrupt):
|
|
6212
6701
|
return None
|
|
6213
|
-
return custom if custom else None
|
|
6702
|
+
return _confirmed_selection(custom) if custom else None
|
|
6214
6703
|
return None
|
|
6215
6704
|
except (ImportError, NotImplementedError, OSError, subprocess.SubprocessError):
|
|
6216
6705
|
pass
|
|
@@ -6242,10 +6731,10 @@ def _prompt_model_selection(
|
|
|
6242
6731
|
return None
|
|
6243
6732
|
idx = int(choice)
|
|
6244
6733
|
if 1 <= idx <= n:
|
|
6245
|
-
return ordered[idx - 1]
|
|
6734
|
+
return _confirmed_selection(ordered[idx - 1])
|
|
6246
6735
|
elif idx == n + 1:
|
|
6247
6736
|
custom = input("Enter model name: ").strip()
|
|
6248
|
-
return custom if custom else None
|
|
6737
|
+
return _confirmed_selection(custom) if custom else None
|
|
6249
6738
|
elif idx == n + 2:
|
|
6250
6739
|
return None
|
|
6251
6740
|
print(f"Please enter 1-{n + 2}")
|
|
@@ -6589,6 +7078,7 @@ def _xai_oauth_loopback_login(
|
|
|
6589
7078
|
authorization_endpoint = discovery["authorization_endpoint"]
|
|
6590
7079
|
token_endpoint = discovery["token_endpoint"]
|
|
6591
7080
|
|
|
7081
|
+
allow_missing_state = False
|
|
6592
7082
|
if manual_paste:
|
|
6593
7083
|
# No HTTP listener — synthesize a redirect_uri matching what
|
|
6594
7084
|
# the server would have bound to so the authorize URL the user
|
|
@@ -6615,6 +7105,7 @@ def _xai_oauth_loopback_login(
|
|
|
6615
7105
|
print("Open this URL to authorize Hermes with xAI:")
|
|
6616
7106
|
print(authorize_url)
|
|
6617
7107
|
callback = _prompt_manual_callback_paste(redirect_uri)
|
|
7108
|
+
allow_missing_state = True
|
|
6618
7109
|
else:
|
|
6619
7110
|
server, thread, callback_result, redirect_uri = _xai_start_callback_server()
|
|
6620
7111
|
try:
|
|
@@ -6654,6 +7145,7 @@ def _xai_oauth_loopback_login(
|
|
|
6654
7145
|
thread,
|
|
6655
7146
|
callback_result,
|
|
6656
7147
|
timeout_seconds=max(30.0, timeout_seconds * 9),
|
|
7148
|
+
manual_paste_redirect_uri=redirect_uri,
|
|
6657
7149
|
)
|
|
6658
7150
|
except AuthError as exc:
|
|
6659
7151
|
if (
|
|
@@ -6670,6 +7162,7 @@ def _xai_oauth_loopback_login(
|
|
|
6670
7162
|
callback = _prompt_manual_callback_paste(redirect_uri)
|
|
6671
7163
|
if callback.get("code") is None and callback.get("error") is None:
|
|
6672
7164
|
raise exc
|
|
7165
|
+
allow_missing_state = True
|
|
6673
7166
|
except Exception:
|
|
6674
7167
|
try:
|
|
6675
7168
|
server.shutdown()
|
|
@@ -6690,7 +7183,7 @@ def _xai_oauth_loopback_login(
|
|
|
6690
7183
|
code="xai_authorization_failed",
|
|
6691
7184
|
)
|
|
6692
7185
|
callback_state = callback.get("state")
|
|
6693
|
-
# Manual
|
|
7186
|
+
# Manual bare-code paths: when a user pastes only the opaque
|
|
6694
7187
|
# authorization code (no ``code=``/``state=`` query parameters),
|
|
6695
7188
|
# ``_parse_pasted_callback`` returns ``state=None``. xAI's consent
|
|
6696
7189
|
# page renders the code in-page rather than redirecting through the
|
|
@@ -6698,10 +7191,12 @@ def _xai_oauth_loopback_login(
|
|
|
6698
7191
|
# VPS, container consoles) the bare code is the only thing the user
|
|
6699
7192
|
# can obtain. PKCE (code_verifier) still binds the exchange to this
|
|
6700
7193
|
# client, so the local state-equality check is redundant on the
|
|
6701
|
-
# bare-code
|
|
7194
|
+
# bare-code paths — we substitute the locally generated state to keep
|
|
6702
7195
|
# the rest of the validation chain (and the token exchange) unchanged.
|
|
6703
7196
|
# See #26923 (AccursedGalaxy comment, 2026-05-20).
|
|
6704
|
-
if
|
|
7197
|
+
if callback.get("_manual_paste"):
|
|
7198
|
+
allow_missing_state = True
|
|
7199
|
+
if callback_state is None and (manual_paste or allow_missing_state):
|
|
6705
7200
|
callback_state = state
|
|
6706
7201
|
if callback_state != state:
|
|
6707
7202
|
raise AuthError(
|
|
@@ -6768,23 +7263,61 @@ def _codex_device_code_login() -> Dict[str, Any]:
|
|
|
6768
7263
|
issuer = "https://auth.openai.com"
|
|
6769
7264
|
client_id = CODEX_OAUTH_CLIENT_ID
|
|
6770
7265
|
|
|
6771
|
-
# Step 1: Request device code
|
|
6772
|
-
|
|
6773
|
-
|
|
6774
|
-
|
|
6775
|
-
|
|
6776
|
-
|
|
6777
|
-
|
|
7266
|
+
# Step 1: Request device code. OpenAI's auth endpoint rate-limits this
|
|
7267
|
+
# request (HTTP 429) when login is attempted too often from the same
|
|
7268
|
+
# IP/account — retry with capped backoff (honoring ``Retry-After``)
|
|
7269
|
+
# before surfacing a clear, actionable message instead of a bare status.
|
|
7270
|
+
resp = None
|
|
7271
|
+
max_attempts = 4
|
|
7272
|
+
for attempt in range(1, max_attempts + 1):
|
|
7273
|
+
try:
|
|
7274
|
+
with httpx.Client(timeout=httpx.Timeout(15.0)) as client:
|
|
7275
|
+
resp = client.post(
|
|
7276
|
+
f"{issuer}/api/accounts/deviceauth/usercode",
|
|
7277
|
+
json={"client_id": client_id},
|
|
7278
|
+
headers={"Content-Type": "application/json"},
|
|
7279
|
+
)
|
|
7280
|
+
except Exception as exc:
|
|
7281
|
+
raise AuthError(
|
|
7282
|
+
f"Failed to request device code: {exc}",
|
|
7283
|
+
provider="openai-codex", code="device_code_request_failed",
|
|
6778
7284
|
)
|
|
6779
|
-
|
|
7285
|
+
|
|
7286
|
+
if resp.status_code != 429:
|
|
7287
|
+
break
|
|
7288
|
+
|
|
7289
|
+
if attempt < max_attempts:
|
|
7290
|
+
retry_after = _parse_retry_after_seconds(
|
|
7291
|
+
getattr(resp, "headers", None)
|
|
7292
|
+
)
|
|
7293
|
+
# Exponential backoff (2s, 4s, 8s) capped, preferring the
|
|
7294
|
+
# server-provided Retry-After when present.
|
|
7295
|
+
delay = retry_after if retry_after is not None else 2 ** attempt
|
|
7296
|
+
delay = max(1, min(int(delay), 60))
|
|
7297
|
+
print(
|
|
7298
|
+
"OpenAI is rate-limiting login requests "
|
|
7299
|
+
f"(429); retrying in {delay}s..."
|
|
7300
|
+
)
|
|
7301
|
+
_time.sleep(delay)
|
|
7302
|
+
|
|
7303
|
+
if resp is not None and resp.status_code == 429:
|
|
7304
|
+
retry_after = _parse_retry_after_seconds(getattr(resp, "headers", None))
|
|
7305
|
+
wait_hint = (
|
|
7306
|
+
f" Try again in about {retry_after}s."
|
|
7307
|
+
if retry_after is not None
|
|
7308
|
+
else " Wait a minute and run the login again."
|
|
7309
|
+
)
|
|
6780
7310
|
raise AuthError(
|
|
6781
|
-
|
|
6782
|
-
|
|
7311
|
+
"OpenAI is rate-limiting Codex login requests (HTTP 429). "
|
|
7312
|
+
"This is a temporary throttle on OpenAI's side, not a credential "
|
|
7313
|
+
f"problem.{wait_hint}",
|
|
7314
|
+
provider="openai-codex", code=CODEX_RATE_LIMITED_CODE,
|
|
6783
7315
|
)
|
|
6784
7316
|
|
|
6785
|
-
if resp.status_code != 200:
|
|
7317
|
+
if resp is None or resp.status_code != 200:
|
|
7318
|
+
status = resp.status_code if resp is not None else "unknown"
|
|
6786
7319
|
raise AuthError(
|
|
6787
|
-
f"Device code request returned status {
|
|
7320
|
+
f"Device code request returned status {status}.",
|
|
6788
7321
|
provider="openai-codex", code="device_code_request_error",
|
|
6789
7322
|
)
|
|
6790
7323
|
|
|
@@ -6872,6 +7405,22 @@ def _codex_device_code_login() -> Dict[str, Any]:
|
|
|
6872
7405
|
provider="openai-codex", code="token_exchange_failed",
|
|
6873
7406
|
)
|
|
6874
7407
|
|
|
7408
|
+
if token_resp.status_code == 429:
|
|
7409
|
+
retry_after = _parse_retry_after_seconds(
|
|
7410
|
+
getattr(token_resp, "headers", None)
|
|
7411
|
+
)
|
|
7412
|
+
wait_hint = (
|
|
7413
|
+
f" Try again in about {retry_after}s."
|
|
7414
|
+
if retry_after is not None
|
|
7415
|
+
else " Wait a minute and run the login again."
|
|
7416
|
+
)
|
|
7417
|
+
raise AuthError(
|
|
7418
|
+
"OpenAI is rate-limiting Codex login requests (HTTP 429) during "
|
|
7419
|
+
"token exchange. This is a temporary throttle on OpenAI's side, "
|
|
7420
|
+
f"not a credential problem.{wait_hint}",
|
|
7421
|
+
provider="openai-codex", code=CODEX_RATE_LIMITED_CODE,
|
|
7422
|
+
)
|
|
7423
|
+
|
|
6875
7424
|
if token_resp.status_code != 200:
|
|
6876
7425
|
raise AuthError(
|
|
6877
7426
|
f"Token exchange returned status {token_resp.status_code}.",
|
|
@@ -7348,6 +7897,7 @@ def _nous_device_code_login(
|
|
|
7348
7897
|
timeout_seconds: float = 15.0,
|
|
7349
7898
|
insecure: bool = False,
|
|
7350
7899
|
ca_bundle: Optional[str] = None,
|
|
7900
|
+
on_verification: Optional[Callable[[str, str], None]] = None,
|
|
7351
7901
|
) -> Dict[str, Any]:
|
|
7352
7902
|
"""Run the Nous device-code flow and return full OAuth state without persisting."""
|
|
7353
7903
|
pconfig = PROVIDER_REGISTRY["nous"]
|
|
@@ -7402,6 +7952,16 @@ def _nous_device_code_login(
|
|
|
7402
7952
|
else:
|
|
7403
7953
|
print(" Could not open browser automatically — use the URL above.")
|
|
7404
7954
|
|
|
7955
|
+
# Surface the verification URL/code to an out-of-band consumer (e.g. the
|
|
7956
|
+
# TUI gateway, whose stdout is a JSON-RPC pipe — a plain print() there is
|
|
7957
|
+
# dropped). Fired AFTER the print/browser block and BEFORE polling blocks,
|
|
7958
|
+
# so the consumer can render the link while we wait. Best-effort.
|
|
7959
|
+
if on_verification is not None:
|
|
7960
|
+
try:
|
|
7961
|
+
on_verification(verification_url, user_code)
|
|
7962
|
+
except Exception:
|
|
7963
|
+
pass
|
|
7964
|
+
|
|
7405
7965
|
effective_interval = max(1, min(interval, DEVICE_AUTH_POLL_INTERVAL_CAP_SECONDS))
|
|
7406
7966
|
print(f"Waiting for approval (polling every {effective_interval}s)...")
|
|
7407
7967
|
|
|
@@ -7467,6 +8027,91 @@ def _nous_device_code_login(
|
|
|
7467
8027
|
raise
|
|
7468
8028
|
|
|
7469
8029
|
|
|
8030
|
+
def nous_token_has_billing_scope() -> bool:
|
|
8031
|
+
"""Return True if the currently-held Nous token carries ``billing:manage``.
|
|
8032
|
+
|
|
8033
|
+
Reads the persisted ``scope`` string saved at login (``_save_provider_state``
|
|
8034
|
+
stores ``token_data.get("scope") or scope``). A space-delimited match. Used by
|
|
8035
|
+
the lazy step-up: if False, the first billing call will 403 ``insufficient_scope``
|
|
8036
|
+
anyway, but checking up front lets a surface skip a doomed round-trip.
|
|
8037
|
+
"""
|
|
8038
|
+
try:
|
|
8039
|
+
state = get_provider_auth_state("nous") or {}
|
|
8040
|
+
except Exception:
|
|
8041
|
+
return False
|
|
8042
|
+
scope = state.get("scope")
|
|
8043
|
+
if not isinstance(scope, str):
|
|
8044
|
+
return False
|
|
8045
|
+
return NOUS_BILLING_MANAGE_SCOPE in scope.split()
|
|
8046
|
+
|
|
8047
|
+
|
|
8048
|
+
def step_up_nous_billing_scope(
|
|
8049
|
+
*,
|
|
8050
|
+
open_browser: bool = True,
|
|
8051
|
+
timeout_seconds: float = 15.0,
|
|
8052
|
+
on_verification: Optional[Callable[[str, str], None]] = None,
|
|
8053
|
+
) -> bool:
|
|
8054
|
+
"""Re-run the device flow requesting ``billing:manage`` and persist the result.
|
|
8055
|
+
|
|
8056
|
+
The lazy step-up (plan D-A): triggered when a billing endpoint returns
|
|
8057
|
+
``403 insufficient_scope``. Runs a fresh device-connect with
|
|
8058
|
+
``inference:invoke tool:invoke billing:manage`` on the scope. The user must be
|
|
8059
|
+
an ADMIN/OWNER and tick "Allow terminal billing" in the portal for the minted
|
|
8060
|
+
token to actually carry the scope; otherwise the server silently downscopes and this
|
|
8061
|
+
returns False.
|
|
8062
|
+
|
|
8063
|
+
Reuses the held credential's portal/inference URLs + client_id so the step-up
|
|
8064
|
+
targets the same deployment (incl. a preview via ``HERMES_PORTAL_BASE_URL`` set
|
|
8065
|
+
at the original login). Persists to the auth store + shared store + pool, exactly
|
|
8066
|
+
like ``_login_nous`` — but WITHOUT the model picker (this is a scope upgrade, not
|
|
8067
|
+
a fresh login).
|
|
8068
|
+
|
|
8069
|
+
Returns True iff the new token carries ``billing:manage``.
|
|
8070
|
+
"""
|
|
8071
|
+
prior = get_provider_auth_state("nous") or {}
|
|
8072
|
+
pconfig = PROVIDER_REGISTRY["nous"]
|
|
8073
|
+
|
|
8074
|
+
# Build the step-up scope: existing scopes (if any) + billing:manage, deduped,
|
|
8075
|
+
# order-stable. Fall back to the standard inference+tool+billing set.
|
|
8076
|
+
_raw_scope = prior.get("scope")
|
|
8077
|
+
prior_scope = _raw_scope if isinstance(_raw_scope, str) else ""
|
|
8078
|
+
requested: list[str] = []
|
|
8079
|
+
for tok in (prior_scope.split() or [NOUS_INFERENCE_INVOKE_SCOPE, "tool:invoke"]):
|
|
8080
|
+
if tok and tok not in requested:
|
|
8081
|
+
requested.append(tok)
|
|
8082
|
+
if NOUS_BILLING_MANAGE_SCOPE not in requested:
|
|
8083
|
+
requested.append(NOUS_BILLING_MANAGE_SCOPE)
|
|
8084
|
+
scope = " ".join(requested)
|
|
8085
|
+
|
|
8086
|
+
auth_state = _nous_device_code_login(
|
|
8087
|
+
portal_base_url=prior.get("portal_base_url") or None,
|
|
8088
|
+
inference_base_url=prior.get("inference_base_url") or None,
|
|
8089
|
+
client_id=prior.get("client_id") or pconfig.client_id,
|
|
8090
|
+
scope=scope,
|
|
8091
|
+
open_browser=open_browser,
|
|
8092
|
+
timeout_seconds=timeout_seconds,
|
|
8093
|
+
on_verification=on_verification,
|
|
8094
|
+
)
|
|
8095
|
+
|
|
8096
|
+
with _auth_store_lock():
|
|
8097
|
+
auth_store = _load_auth_store()
|
|
8098
|
+
_save_provider_state(auth_store, "nous", auth_state)
|
|
8099
|
+
_save_auth_store(auth_store)
|
|
8100
|
+
|
|
8101
|
+
# Mirror to shared store + reseed the pool (best-effort), same as _login_nous.
|
|
8102
|
+
try:
|
|
8103
|
+
_write_shared_nous_state(auth_state)
|
|
8104
|
+
except Exception:
|
|
8105
|
+
pass
|
|
8106
|
+
try:
|
|
8107
|
+
_sync_nous_pool_from_auth_store()
|
|
8108
|
+
except Exception:
|
|
8109
|
+
pass
|
|
8110
|
+
|
|
8111
|
+
granted = auth_state.get("scope")
|
|
8112
|
+
return isinstance(granted, str) and NOUS_BILLING_MANAGE_SCOPE in granted.split()
|
|
8113
|
+
|
|
8114
|
+
|
|
7470
8115
|
def _login_nous(args, pconfig: ProviderConfig) -> None:
|
|
7471
8116
|
"""Nous Portal device authorization flow."""
|
|
7472
8117
|
timeout_seconds = getattr(args, "timeout", None) or 15.0
|
|
@@ -7618,6 +8263,9 @@ def _login_nous(args, pconfig: ProviderConfig) -> None:
|
|
|
7618
8263
|
unavailable_models=unavailable_models,
|
|
7619
8264
|
portal_url=_portal,
|
|
7620
8265
|
unavailable_message=unavailable_message,
|
|
8266
|
+
confirm_provider="nous",
|
|
8267
|
+
confirm_base_url=inference_base_url,
|
|
8268
|
+
confirm_api_key=runtime_key,
|
|
7621
8269
|
)
|
|
7622
8270
|
elif unavailable_models:
|
|
7623
8271
|
_url = (_portal or DEFAULT_NOUS_PORTAL_URL).rstrip("/")
|