code-puppy 0.0.214__py3-none-any.whl → 0.0.366__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (231) hide show
  1. code_puppy/__init__.py +7 -1
  2. code_puppy/agents/__init__.py +2 -0
  3. code_puppy/agents/agent_c_reviewer.py +59 -6
  4. code_puppy/agents/agent_code_puppy.py +7 -1
  5. code_puppy/agents/agent_code_reviewer.py +12 -2
  6. code_puppy/agents/agent_cpp_reviewer.py +73 -6
  7. code_puppy/agents/agent_creator_agent.py +45 -4
  8. code_puppy/agents/agent_golang_reviewer.py +92 -3
  9. code_puppy/agents/agent_javascript_reviewer.py +101 -8
  10. code_puppy/agents/agent_manager.py +81 -4
  11. code_puppy/agents/agent_pack_leader.py +383 -0
  12. code_puppy/agents/agent_planning.py +163 -0
  13. code_puppy/agents/agent_python_programmer.py +165 -0
  14. code_puppy/agents/agent_python_reviewer.py +28 -6
  15. code_puppy/agents/agent_qa_expert.py +98 -6
  16. code_puppy/agents/agent_qa_kitten.py +12 -7
  17. code_puppy/agents/agent_security_auditor.py +113 -3
  18. code_puppy/agents/agent_terminal_qa.py +323 -0
  19. code_puppy/agents/agent_typescript_reviewer.py +106 -7
  20. code_puppy/agents/base_agent.py +802 -176
  21. code_puppy/agents/event_stream_handler.py +350 -0
  22. code_puppy/agents/pack/__init__.py +34 -0
  23. code_puppy/agents/pack/bloodhound.py +304 -0
  24. code_puppy/agents/pack/husky.py +321 -0
  25. code_puppy/agents/pack/retriever.py +393 -0
  26. code_puppy/agents/pack/shepherd.py +348 -0
  27. code_puppy/agents/pack/terrier.py +287 -0
  28. code_puppy/agents/pack/watchdog.py +367 -0
  29. code_puppy/agents/prompt_reviewer.py +145 -0
  30. code_puppy/agents/subagent_stream_handler.py +276 -0
  31. code_puppy/api/__init__.py +13 -0
  32. code_puppy/api/app.py +169 -0
  33. code_puppy/api/main.py +21 -0
  34. code_puppy/api/pty_manager.py +446 -0
  35. code_puppy/api/routers/__init__.py +12 -0
  36. code_puppy/api/routers/agents.py +36 -0
  37. code_puppy/api/routers/commands.py +217 -0
  38. code_puppy/api/routers/config.py +74 -0
  39. code_puppy/api/routers/sessions.py +232 -0
  40. code_puppy/api/templates/terminal.html +361 -0
  41. code_puppy/api/websocket.py +154 -0
  42. code_puppy/callbacks.py +142 -4
  43. code_puppy/chatgpt_codex_client.py +283 -0
  44. code_puppy/claude_cache_client.py +586 -0
  45. code_puppy/cli_runner.py +916 -0
  46. code_puppy/command_line/add_model_menu.py +1079 -0
  47. code_puppy/command_line/agent_menu.py +395 -0
  48. code_puppy/command_line/attachments.py +10 -5
  49. code_puppy/command_line/autosave_menu.py +605 -0
  50. code_puppy/command_line/clipboard.py +527 -0
  51. code_puppy/command_line/colors_menu.py +520 -0
  52. code_puppy/command_line/command_handler.py +176 -738
  53. code_puppy/command_line/command_registry.py +150 -0
  54. code_puppy/command_line/config_commands.py +715 -0
  55. code_puppy/command_line/core_commands.py +792 -0
  56. code_puppy/command_line/diff_menu.py +863 -0
  57. code_puppy/command_line/load_context_completion.py +15 -22
  58. code_puppy/command_line/mcp/base.py +0 -3
  59. code_puppy/command_line/mcp/catalog_server_installer.py +175 -0
  60. code_puppy/command_line/mcp/custom_server_form.py +688 -0
  61. code_puppy/command_line/mcp/custom_server_installer.py +195 -0
  62. code_puppy/command_line/mcp/edit_command.py +148 -0
  63. code_puppy/command_line/mcp/handler.py +9 -4
  64. code_puppy/command_line/mcp/help_command.py +6 -5
  65. code_puppy/command_line/mcp/install_command.py +15 -26
  66. code_puppy/command_line/mcp/install_menu.py +685 -0
  67. code_puppy/command_line/mcp/list_command.py +2 -2
  68. code_puppy/command_line/mcp/logs_command.py +174 -65
  69. code_puppy/command_line/mcp/remove_command.py +2 -2
  70. code_puppy/command_line/mcp/restart_command.py +12 -4
  71. code_puppy/command_line/mcp/search_command.py +16 -10
  72. code_puppy/command_line/mcp/start_all_command.py +18 -6
  73. code_puppy/command_line/mcp/start_command.py +47 -25
  74. code_puppy/command_line/mcp/status_command.py +4 -5
  75. code_puppy/command_line/mcp/stop_all_command.py +7 -1
  76. code_puppy/command_line/mcp/stop_command.py +8 -4
  77. code_puppy/command_line/mcp/test_command.py +2 -2
  78. code_puppy/command_line/mcp/wizard_utils.py +20 -16
  79. code_puppy/command_line/mcp_completion.py +174 -0
  80. code_puppy/command_line/model_picker_completion.py +75 -25
  81. code_puppy/command_line/model_settings_menu.py +884 -0
  82. code_puppy/command_line/motd.py +14 -8
  83. code_puppy/command_line/onboarding_slides.py +179 -0
  84. code_puppy/command_line/onboarding_wizard.py +340 -0
  85. code_puppy/command_line/pin_command_completion.py +329 -0
  86. code_puppy/command_line/prompt_toolkit_completion.py +463 -63
  87. code_puppy/command_line/session_commands.py +296 -0
  88. code_puppy/command_line/utils.py +54 -0
  89. code_puppy/config.py +898 -112
  90. code_puppy/error_logging.py +118 -0
  91. code_puppy/gemini_code_assist.py +385 -0
  92. code_puppy/gemini_model.py +602 -0
  93. code_puppy/http_utils.py +210 -148
  94. code_puppy/keymap.py +128 -0
  95. code_puppy/main.py +5 -698
  96. code_puppy/mcp_/__init__.py +17 -0
  97. code_puppy/mcp_/async_lifecycle.py +35 -4
  98. code_puppy/mcp_/blocking_startup.py +70 -43
  99. code_puppy/mcp_/captured_stdio_server.py +2 -2
  100. code_puppy/mcp_/config_wizard.py +4 -4
  101. code_puppy/mcp_/dashboard.py +15 -6
  102. code_puppy/mcp_/managed_server.py +65 -38
  103. code_puppy/mcp_/manager.py +146 -52
  104. code_puppy/mcp_/mcp_logs.py +224 -0
  105. code_puppy/mcp_/registry.py +6 -6
  106. code_puppy/mcp_/server_registry_catalog.py +24 -5
  107. code_puppy/messaging/__init__.py +199 -2
  108. code_puppy/messaging/bus.py +610 -0
  109. code_puppy/messaging/commands.py +167 -0
  110. code_puppy/messaging/markdown_patches.py +57 -0
  111. code_puppy/messaging/message_queue.py +17 -48
  112. code_puppy/messaging/messages.py +500 -0
  113. code_puppy/messaging/queue_console.py +1 -24
  114. code_puppy/messaging/renderers.py +43 -146
  115. code_puppy/messaging/rich_renderer.py +1027 -0
  116. code_puppy/messaging/spinner/__init__.py +21 -5
  117. code_puppy/messaging/spinner/console_spinner.py +86 -51
  118. code_puppy/messaging/subagent_console.py +461 -0
  119. code_puppy/model_factory.py +634 -83
  120. code_puppy/model_utils.py +167 -0
  121. code_puppy/models.json +66 -68
  122. code_puppy/models_dev_api.json +1 -0
  123. code_puppy/models_dev_parser.py +592 -0
  124. code_puppy/plugins/__init__.py +164 -10
  125. code_puppy/plugins/antigravity_oauth/__init__.py +10 -0
  126. code_puppy/plugins/antigravity_oauth/accounts.py +406 -0
  127. code_puppy/plugins/antigravity_oauth/antigravity_model.py +704 -0
  128. code_puppy/plugins/antigravity_oauth/config.py +42 -0
  129. code_puppy/plugins/antigravity_oauth/constants.py +136 -0
  130. code_puppy/plugins/antigravity_oauth/oauth.py +478 -0
  131. code_puppy/plugins/antigravity_oauth/register_callbacks.py +406 -0
  132. code_puppy/plugins/antigravity_oauth/storage.py +271 -0
  133. code_puppy/plugins/antigravity_oauth/test_plugin.py +319 -0
  134. code_puppy/plugins/antigravity_oauth/token.py +167 -0
  135. code_puppy/plugins/antigravity_oauth/transport.py +767 -0
  136. code_puppy/plugins/antigravity_oauth/utils.py +169 -0
  137. code_puppy/plugins/chatgpt_oauth/__init__.py +8 -0
  138. code_puppy/plugins/chatgpt_oauth/config.py +52 -0
  139. code_puppy/plugins/chatgpt_oauth/oauth_flow.py +328 -0
  140. code_puppy/plugins/chatgpt_oauth/register_callbacks.py +94 -0
  141. code_puppy/plugins/chatgpt_oauth/test_plugin.py +293 -0
  142. code_puppy/plugins/chatgpt_oauth/utils.py +489 -0
  143. code_puppy/plugins/claude_code_oauth/README.md +167 -0
  144. code_puppy/plugins/claude_code_oauth/SETUP.md +93 -0
  145. code_puppy/plugins/claude_code_oauth/__init__.py +6 -0
  146. code_puppy/plugins/claude_code_oauth/config.py +50 -0
  147. code_puppy/plugins/claude_code_oauth/register_callbacks.py +308 -0
  148. code_puppy/plugins/claude_code_oauth/test_plugin.py +283 -0
  149. code_puppy/plugins/claude_code_oauth/utils.py +518 -0
  150. code_puppy/plugins/customizable_commands/__init__.py +0 -0
  151. code_puppy/plugins/customizable_commands/register_callbacks.py +169 -0
  152. code_puppy/plugins/example_custom_command/README.md +280 -0
  153. code_puppy/plugins/example_custom_command/register_callbacks.py +2 -2
  154. code_puppy/plugins/file_permission_handler/__init__.py +4 -0
  155. code_puppy/plugins/file_permission_handler/register_callbacks.py +523 -0
  156. code_puppy/plugins/frontend_emitter/__init__.py +25 -0
  157. code_puppy/plugins/frontend_emitter/emitter.py +121 -0
  158. code_puppy/plugins/frontend_emitter/register_callbacks.py +261 -0
  159. code_puppy/plugins/oauth_puppy_html.py +228 -0
  160. code_puppy/plugins/shell_safety/__init__.py +6 -0
  161. code_puppy/plugins/shell_safety/agent_shell_safety.py +69 -0
  162. code_puppy/plugins/shell_safety/command_cache.py +156 -0
  163. code_puppy/plugins/shell_safety/register_callbacks.py +202 -0
  164. code_puppy/prompts/antigravity_system_prompt.md +1 -0
  165. code_puppy/prompts/codex_system_prompt.md +310 -0
  166. code_puppy/pydantic_patches.py +131 -0
  167. code_puppy/reopenable_async_client.py +8 -8
  168. code_puppy/round_robin_model.py +9 -12
  169. code_puppy/session_storage.py +2 -1
  170. code_puppy/status_display.py +21 -4
  171. code_puppy/summarization_agent.py +41 -13
  172. code_puppy/terminal_utils.py +418 -0
  173. code_puppy/tools/__init__.py +37 -1
  174. code_puppy/tools/agent_tools.py +536 -52
  175. code_puppy/tools/browser/__init__.py +37 -0
  176. code_puppy/tools/browser/browser_control.py +19 -23
  177. code_puppy/tools/browser/browser_interactions.py +41 -48
  178. code_puppy/tools/browser/browser_locators.py +36 -38
  179. code_puppy/tools/browser/browser_manager.py +316 -0
  180. code_puppy/tools/browser/browser_navigation.py +16 -16
  181. code_puppy/tools/browser/browser_screenshot.py +79 -143
  182. code_puppy/tools/browser/browser_scripts.py +32 -42
  183. code_puppy/tools/browser/browser_workflows.py +44 -27
  184. code_puppy/tools/browser/chromium_terminal_manager.py +259 -0
  185. code_puppy/tools/browser/terminal_command_tools.py +521 -0
  186. code_puppy/tools/browser/terminal_screenshot_tools.py +556 -0
  187. code_puppy/tools/browser/terminal_tools.py +525 -0
  188. code_puppy/tools/command_runner.py +930 -147
  189. code_puppy/tools/common.py +1113 -5
  190. code_puppy/tools/display.py +84 -0
  191. code_puppy/tools/file_modifications.py +288 -89
  192. code_puppy/tools/file_operations.py +226 -154
  193. code_puppy/tools/subagent_context.py +158 -0
  194. code_puppy/uvx_detection.py +242 -0
  195. code_puppy/version_checker.py +30 -11
  196. code_puppy-0.0.366.data/data/code_puppy/models.json +110 -0
  197. code_puppy-0.0.366.data/data/code_puppy/models_dev_api.json +1 -0
  198. {code_puppy-0.0.214.dist-info → code_puppy-0.0.366.dist-info}/METADATA +149 -75
  199. code_puppy-0.0.366.dist-info/RECORD +217 -0
  200. {code_puppy-0.0.214.dist-info → code_puppy-0.0.366.dist-info}/WHEEL +1 -1
  201. code_puppy/command_line/mcp/add_command.py +0 -183
  202. code_puppy/messaging/spinner/textual_spinner.py +0 -106
  203. code_puppy/tools/browser/camoufox_manager.py +0 -216
  204. code_puppy/tools/browser/vqa_agent.py +0 -70
  205. code_puppy/tui/__init__.py +0 -10
  206. code_puppy/tui/app.py +0 -1105
  207. code_puppy/tui/components/__init__.py +0 -21
  208. code_puppy/tui/components/chat_view.py +0 -551
  209. code_puppy/tui/components/command_history_modal.py +0 -218
  210. code_puppy/tui/components/copy_button.py +0 -139
  211. code_puppy/tui/components/custom_widgets.py +0 -63
  212. code_puppy/tui/components/human_input_modal.py +0 -175
  213. code_puppy/tui/components/input_area.py +0 -167
  214. code_puppy/tui/components/sidebar.py +0 -309
  215. code_puppy/tui/components/status_bar.py +0 -185
  216. code_puppy/tui/messages.py +0 -27
  217. code_puppy/tui/models/__init__.py +0 -8
  218. code_puppy/tui/models/chat_message.py +0 -25
  219. code_puppy/tui/models/command_history.py +0 -89
  220. code_puppy/tui/models/enums.py +0 -24
  221. code_puppy/tui/screens/__init__.py +0 -17
  222. code_puppy/tui/screens/autosave_picker.py +0 -175
  223. code_puppy/tui/screens/help.py +0 -130
  224. code_puppy/tui/screens/mcp_install_wizard.py +0 -803
  225. code_puppy/tui/screens/settings.py +0 -306
  226. code_puppy/tui/screens/tools.py +0 -74
  227. code_puppy/tui_state.py +0 -55
  228. code_puppy-0.0.214.data/data/code_puppy/models.json +0 -112
  229. code_puppy-0.0.214.dist-info/RECORD +0 -131
  230. {code_puppy-0.0.214.dist-info → code_puppy-0.0.366.dist-info}/entry_points.txt +0 -0
  231. {code_puppy-0.0.214.dist-info → code_puppy-0.0.366.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,93 @@
1
+ # Claude Code OAuth Plugin Setup Guide
2
+
3
+ This guide walks you through using the Claude Code OAuth plugin inside Code Puppy.
4
+
5
+ ## Quick Start
6
+
7
+ 1. Ensure the plugin files live under `code_puppy/plugins/claude_code_oauth/`
8
+ 2. Restart Code Puppy so it loads the plugin
9
+ 3. Run `/claude-code-auth` and follow the prompts
10
+
11
+ ## Why No Client Registration?
12
+
13
+ Anthropic exposes a shared **public client** (`claude-cli`) for command-line tools. That means:
14
+ - No client secret is needed
15
+ - Everyone authenticates through Claude Console
16
+ - Security is enforced with PKCE and per-user tokens
17
+
18
+ ## Authentication Flow
19
+
20
+ 1. Call `/claude-code-auth`
21
+ 2. Your browser opens the Claude OAuth consent flow at `https://claude.ai/oauth/authorize`
22
+ 3. Sign in (or pick an account) and approve the "Claude CLI" access request
23
+ 4. The browser closes automatically after the redirect is captured
24
+ 5. Tokens are stored locally at `~/.code_puppy/claude_code_oauth.json`
25
+ 6. Available Claude Code models are fetched and added to `extra_models.json`
26
+
27
+ ## Commands Recap
28
+
29
+ - `/claude-code-auth` – Authenticate and sync models
30
+ - `/claude-code-status` – Show auth status, expiry, configured models
31
+ - `/claude-code-logout` – Remove tokens and any models added by the plugin
32
+
33
+ ## Configuration Defaults
34
+
35
+ `config.py` ships with values aligned to llxprt-code:
36
+
37
+ ```python
38
+ CLAUDE_CODE_OAUTH_CONFIG = {
39
+ "auth_url": "https://claude.ai/oauth/authorize",
40
+ "token_url": "https://claude.ai/api/oauth/token",
41
+ "api_base_url": "https://api.anthropic.com",
42
+ "client_id": "9d1c250a-e61b-44d9-88ed-5944d1962f5e",
43
+ "scope": "org:create_api_key user:profile user:inference",
44
+ "redirect_host": "http://localhost",
45
+ "redirect_path": "callback",
46
+ "callback_port_range": (8765, 8795),
47
+ "callback_timeout": 180,
48
+ "prefix": "claude-code-",
49
+ "default_context_length": 200000,
50
+ "api_key_env_var": "CLAUDE_CODE_ACCESS_TOKEN",
51
+ }
52
+ ```
53
+
54
+ Change these only if Anthropic updates their endpoints or scopes.
55
+
56
+ ## After Authentication
57
+
58
+ - Models appear in `~/.code_puppy/extra_models.json` with the `claude-code-` prefix
59
+ - The environment variable `CLAUDE_CODE_ACCESS_TOKEN` is used by those models
60
+ - `/claude-code-status` shows token expiry when the API provides it
61
+
62
+ ## Troubleshooting Tips
63
+
64
+ - **Browser did not open** – Copy the displayed URL into your browser manually
65
+ - **Invalid code** – The code expires quickly; generate a new one in Claude Console
66
+ - **State mismatch** – Rare, but rerun `/claude-code-auth` if the browser reports a mismatch
67
+ - **No models added** – Your account might lack Claude Code access; tokens are still stored for later use
68
+
69
+ ## Files Created
70
+
71
+ ```
72
+ ~/.code_puppy/
73
+ ├── claude_code_oauth.json # OAuth tokens (0600 permissions)
74
+ └── extra_models.json # Extended model registry
75
+ ```
76
+
77
+ ## Manual Testing
78
+
79
+ Run the helper script for sanity checks:
80
+
81
+ ```bash
82
+ python code_puppy/plugins/claude_code_oauth/test_plugin.py
83
+ ```
84
+
85
+ It verifies imports, configuration values, and filesystem expectations without hitting the Anthropic API.
86
+
87
+ ## Security Notes
88
+
89
+ - Tokens are stored locally and never transmitted elsewhere
90
+ - PKCE protects the flow even without a client secret
91
+ - HTTPS endpoints are enforced for all requests
92
+
93
+ Enjoy hacking with Claude Code straight from Code Puppy! 🐶💻
@@ -0,0 +1,6 @@
1
+ """
2
+ Claude Code OAuth Plugin for Code Puppy
3
+
4
+ This plugin provides OAuth authentication for Claude Code and automatically
5
+ adds available models to the extra_models.json configuration.
6
+ """
@@ -0,0 +1,50 @@
1
+ from pathlib import Path
2
+ from typing import Any, Dict
3
+
4
+ from code_puppy import config
5
+
6
+ # Claude Code OAuth configuration
7
+ CLAUDE_CODE_OAUTH_CONFIG: Dict[str, Any] = {
8
+ # OAuth endpoints inferred from official Claude Code OAuth flow
9
+ "auth_url": "https://claude.ai/oauth/authorize",
10
+ "token_url": "https://console.anthropic.com/v1/oauth/token",
11
+ "api_base_url": "https://api.anthropic.com",
12
+ # OAuth client configuration observed in Claude Code CLI flow
13
+ "client_id": "9d1c250a-e61b-44d9-88ed-5944d1962f5e",
14
+ "scope": "org:create_api_key user:profile user:inference",
15
+ # Callback handling (we host a localhost callback to capture the redirect)
16
+ "redirect_host": "http://localhost",
17
+ "redirect_path": "callback",
18
+ "callback_port_range": (8765, 8795),
19
+ "callback_timeout": 180,
20
+ # Console redirect fallback (for manual flows, if needed)
21
+ "console_redirect_uri": "https://console.anthropic.com/oauth/code/callback",
22
+ # Local configuration (uses XDG_DATA_HOME)
23
+ "token_storage": None, # Set dynamically in get_token_storage_path()
24
+ # Model configuration
25
+ "prefix": "claude-code-",
26
+ "default_context_length": 200000,
27
+ "api_key_env_var": "CLAUDE_CODE_ACCESS_TOKEN",
28
+ "anthropic_version": "2023-06-01",
29
+ }
30
+
31
+
32
+ def get_token_storage_path() -> Path:
33
+ """Get the path for storing OAuth tokens (uses XDG_DATA_HOME)."""
34
+ data_dir = Path(config.DATA_DIR)
35
+ data_dir.mkdir(parents=True, exist_ok=True, mode=0o700)
36
+ return data_dir / "claude_code_oauth.json"
37
+
38
+
39
+ def get_config_dir() -> Path:
40
+ """Get the Code Puppy configuration directory (uses XDG_CONFIG_HOME)."""
41
+ config_dir = Path(config.CONFIG_DIR)
42
+ config_dir.mkdir(parents=True, exist_ok=True, mode=0o700)
43
+ return config_dir
44
+
45
+
46
+ def get_claude_models_path() -> Path:
47
+ """Get the path to the dedicated claude_models.json file (uses XDG_DATA_HOME)."""
48
+ data_dir = Path(config.DATA_DIR)
49
+ data_dir.mkdir(parents=True, exist_ok=True, mode=0o700)
50
+ return data_dir / "claude_models.json"
@@ -0,0 +1,308 @@
1
+ """
2
+ Claude Code OAuth Plugin for Code Puppy.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ import logging
8
+ import threading
9
+ import time
10
+ from http.server import BaseHTTPRequestHandler, HTTPServer
11
+ from typing import Any, Dict, List, Optional, Tuple
12
+ from urllib.parse import parse_qs, urlparse
13
+
14
+ from code_puppy.callbacks import register_callback
15
+ from code_puppy.config import set_model_name
16
+ from code_puppy.messaging import emit_error, emit_info, emit_success, emit_warning
17
+
18
+ from ..oauth_puppy_html import oauth_failure_html, oauth_success_html
19
+ from .config import CLAUDE_CODE_OAUTH_CONFIG, get_token_storage_path
20
+ from .utils import (
21
+ OAuthContext,
22
+ add_models_to_extra_config,
23
+ assign_redirect_uri,
24
+ build_authorization_url,
25
+ exchange_code_for_tokens,
26
+ fetch_claude_code_models,
27
+ load_claude_models_filtered,
28
+ load_stored_tokens,
29
+ prepare_oauth_context,
30
+ remove_claude_code_models,
31
+ save_tokens,
32
+ )
33
+
34
+ logger = logging.getLogger(__name__)
35
+
36
+
37
+ class _OAuthResult:
38
+ def __init__(self) -> None:
39
+ self.code: Optional[str] = None
40
+ self.state: Optional[str] = None
41
+ self.error: Optional[str] = None
42
+
43
+
44
+ class _CallbackHandler(BaseHTTPRequestHandler):
45
+ result: _OAuthResult
46
+ received_event: threading.Event
47
+
48
+ def do_GET(self) -> None: # noqa: N802
49
+ logger.info("Callback received: path=%s", self.path)
50
+ parsed = urlparse(self.path)
51
+ params: Dict[str, List[str]] = parse_qs(parsed.query)
52
+
53
+ code = params.get("code", [None])[0]
54
+ state = params.get("state", [None])[0]
55
+
56
+ if code and state:
57
+ self.result.code = code
58
+ self.result.state = state
59
+ success_html = oauth_success_html(
60
+ "Claude Code",
61
+ "You're totally synced with Claude Code now!",
62
+ )
63
+ self._write_response(200, success_html)
64
+ else:
65
+ self.result.error = "Missing code or state"
66
+ failure_html = oauth_failure_html(
67
+ "Claude Code",
68
+ "Missing code or state parameter 🥺",
69
+ )
70
+ self._write_response(400, failure_html)
71
+
72
+ self.received_event.set()
73
+
74
+ def log_message(self, format: str, *args: Any) -> None: # noqa: A003
75
+ return
76
+
77
+ def _write_response(self, status: int, body: str) -> None:
78
+ self.send_response(status)
79
+ self.send_header("Content-Type", "text/html; charset=utf-8")
80
+ self.end_headers()
81
+ self.wfile.write(body.encode("utf-8"))
82
+
83
+
84
+ def _start_callback_server(
85
+ context: OAuthContext,
86
+ ) -> Optional[Tuple[HTTPServer, _OAuthResult, threading.Event]]:
87
+ port_range = CLAUDE_CODE_OAUTH_CONFIG["callback_port_range"]
88
+
89
+ for port in range(port_range[0], port_range[1] + 1):
90
+ try:
91
+ server = HTTPServer(("localhost", port), _CallbackHandler)
92
+ assign_redirect_uri(context, port)
93
+ result = _OAuthResult()
94
+ event = threading.Event()
95
+ _CallbackHandler.result = result
96
+ _CallbackHandler.received_event = event
97
+
98
+ def run_server() -> None:
99
+ with server:
100
+ server.serve_forever()
101
+
102
+ threading.Thread(target=run_server, daemon=True).start()
103
+ return server, result, event
104
+ except OSError:
105
+ continue
106
+
107
+ emit_error("Could not start OAuth callback server; all candidate ports are in use")
108
+ return None
109
+
110
+
111
+ def _await_callback(context: OAuthContext) -> Optional[str]:
112
+ timeout = CLAUDE_CODE_OAUTH_CONFIG["callback_timeout"]
113
+
114
+ started = _start_callback_server(context)
115
+ if not started:
116
+ return None
117
+
118
+ server, result, event = started
119
+ redirect_uri = context.redirect_uri
120
+ if not redirect_uri:
121
+ emit_error("Failed to assign redirect URI for OAuth flow")
122
+ server.shutdown()
123
+ return None
124
+
125
+ auth_url = build_authorization_url(context)
126
+
127
+ try:
128
+ import webbrowser
129
+
130
+ from code_puppy.tools.common import should_suppress_browser
131
+
132
+ if should_suppress_browser():
133
+ emit_info(
134
+ "[HEADLESS MODE] Would normally open browser for Claude Code OAuth…"
135
+ )
136
+ emit_info(f"In normal mode, would visit: {auth_url}")
137
+ else:
138
+ emit_info("Opening browser for Claude Code OAuth…")
139
+ webbrowser.open(auth_url)
140
+ emit_info(f"If it doesn't open automatically, visit: {auth_url}")
141
+ except Exception as exc: # pragma: no cover
142
+ if not should_suppress_browser():
143
+ emit_warning(f"Failed to open browser automatically: {exc}")
144
+ emit_info(f"Please open the URL manually: {auth_url}")
145
+
146
+ emit_info(f"Listening for callback on {redirect_uri}")
147
+ emit_info(
148
+ "If Claude redirects you to the console callback page, copy the full URL "
149
+ "and paste it back into Code Puppy."
150
+ )
151
+
152
+ if not event.wait(timeout=timeout):
153
+ emit_error("OAuth callback timed out. Please try again.")
154
+ server.shutdown()
155
+ return None
156
+
157
+ server.shutdown()
158
+
159
+ if result.error:
160
+ emit_error(f"OAuth callback error: {result.error}")
161
+ return None
162
+
163
+ if result.state != context.state:
164
+ emit_error("State mismatch detected; aborting authentication.")
165
+ return None
166
+
167
+ return result.code
168
+
169
+
170
+ def _custom_help() -> List[Tuple[str, str]]:
171
+ return [
172
+ (
173
+ "claude-code-auth",
174
+ "Authenticate with Claude Code via OAuth and import available models",
175
+ ),
176
+ (
177
+ "claude-code-status",
178
+ "Check Claude Code OAuth authentication status and configured models",
179
+ ),
180
+ ("claude-code-logout", "Remove Claude Code OAuth tokens and imported models"),
181
+ ]
182
+
183
+
184
+ def _reload_current_agent() -> None:
185
+ """Reload the current agent so new auth tokens are picked up immediately."""
186
+ try:
187
+ from code_puppy.agents import get_current_agent
188
+
189
+ current_agent = get_current_agent()
190
+ if current_agent is None:
191
+ logger.debug("No current agent to reload")
192
+ return
193
+
194
+ # JSON agents may need to refresh their config before reload
195
+ if hasattr(current_agent, "refresh_config"):
196
+ try:
197
+ current_agent.refresh_config()
198
+ except Exception:
199
+ # Non-fatal, continue to reload
200
+ pass
201
+
202
+ current_agent.reload_code_generation_agent()
203
+ emit_info("Active agent reloaded with new authentication")
204
+ except Exception as e:
205
+ emit_warning(f"Authentication succeeded but agent reload failed: {e}")
206
+ logger.exception("Failed to reload agent after authentication")
207
+
208
+
209
+ def _perform_authentication() -> None:
210
+ context = prepare_oauth_context()
211
+ code = _await_callback(context)
212
+ if not code:
213
+ return
214
+
215
+ emit_info("Exchanging authorization code for tokens…")
216
+ tokens = exchange_code_for_tokens(code, context)
217
+ if not tokens:
218
+ emit_error("Token exchange failed. Please retry the authentication flow.")
219
+ return
220
+
221
+ if not save_tokens(tokens):
222
+ emit_error(
223
+ "Tokens retrieved but failed to save locally. Check file permissions."
224
+ )
225
+ return
226
+
227
+ emit_success("Claude Code OAuth authentication successful!")
228
+
229
+ access_token = tokens.get("access_token")
230
+ if not access_token:
231
+ emit_warning("No access token returned; skipping model discovery.")
232
+ return
233
+
234
+ emit_info("Fetching available Claude Code models…")
235
+ models = fetch_claude_code_models(access_token)
236
+ if not models:
237
+ emit_warning(
238
+ "Claude Code authentication succeeded but no models were returned."
239
+ )
240
+ return
241
+
242
+ emit_info(f"Discovered {len(models)} models: {', '.join(models)}")
243
+ if add_models_to_extra_config(models):
244
+ emit_success(
245
+ "Claude Code models added to your configuration. Use the `claude-code-` prefix!"
246
+ )
247
+
248
+ # Reload the current agent so the new auth token is picked up immediately
249
+ _reload_current_agent()
250
+
251
+
252
+ def _handle_custom_command(command: str, name: str) -> Optional[bool]:
253
+ if not name:
254
+ return None
255
+
256
+ if name == "claude-code-auth":
257
+ emit_info("Starting Claude Code OAuth authentication…")
258
+ tokens = load_stored_tokens()
259
+ if tokens and tokens.get("access_token"):
260
+ emit_warning(
261
+ "Existing Claude Code tokens found. Continuing will overwrite them."
262
+ )
263
+ _perform_authentication()
264
+ set_model_name("claude-code-claude-opus-4-5-20251101")
265
+ return True
266
+
267
+ if name == "claude-code-status":
268
+ tokens = load_stored_tokens()
269
+ if tokens and tokens.get("access_token"):
270
+ emit_success("Claude Code OAuth: Authenticated")
271
+ expires_at = tokens.get("expires_at")
272
+ if expires_at:
273
+ remaining = max(0, int(expires_at - time.time()))
274
+ hours, minutes = divmod(remaining // 60, 60)
275
+ emit_info(f"Token expires in ~{hours}h {minutes}m")
276
+
277
+ claude_models = [
278
+ name
279
+ for name, cfg in load_claude_models_filtered().items()
280
+ if cfg.get("oauth_source") == "claude-code-plugin"
281
+ ]
282
+ if claude_models:
283
+ emit_info(f"Configured Claude Code models: {', '.join(claude_models)}")
284
+ else:
285
+ emit_warning("No Claude Code models configured yet.")
286
+ else:
287
+ emit_warning("Claude Code OAuth: Not authenticated")
288
+ emit_info("Run /claude-code-auth to begin the browser sign-in flow.")
289
+ return True
290
+
291
+ if name == "claude-code-logout":
292
+ token_path = get_token_storage_path()
293
+ if token_path.exists():
294
+ token_path.unlink()
295
+ emit_info("Removed Claude Code OAuth tokens")
296
+
297
+ removed = remove_claude_code_models()
298
+ if removed:
299
+ emit_info(f"Removed {removed} Claude Code models from configuration")
300
+
301
+ emit_success("Claude Code logout complete")
302
+ return True
303
+
304
+ return None
305
+
306
+
307
+ register_callback("custom_command_help", _custom_help)
308
+ register_callback("custom_command", _handle_custom_command)