codepp 0.0.437__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 (288) hide show
  1. code_puppy/__init__.py +10 -0
  2. code_puppy/__main__.py +10 -0
  3. code_puppy/agents/__init__.py +31 -0
  4. code_puppy/agents/agent_c_reviewer.py +155 -0
  5. code_puppy/agents/agent_code_puppy.py +117 -0
  6. code_puppy/agents/agent_code_reviewer.py +90 -0
  7. code_puppy/agents/agent_cpp_reviewer.py +132 -0
  8. code_puppy/agents/agent_creator_agent.py +638 -0
  9. code_puppy/agents/agent_golang_reviewer.py +151 -0
  10. code_puppy/agents/agent_helios.py +124 -0
  11. code_puppy/agents/agent_javascript_reviewer.py +160 -0
  12. code_puppy/agents/agent_manager.py +742 -0
  13. code_puppy/agents/agent_pack_leader.py +385 -0
  14. code_puppy/agents/agent_planning.py +165 -0
  15. code_puppy/agents/agent_python_programmer.py +169 -0
  16. code_puppy/agents/agent_python_reviewer.py +90 -0
  17. code_puppy/agents/agent_qa_expert.py +163 -0
  18. code_puppy/agents/agent_qa_kitten.py +208 -0
  19. code_puppy/agents/agent_scheduler.py +121 -0
  20. code_puppy/agents/agent_security_auditor.py +181 -0
  21. code_puppy/agents/agent_terminal_qa.py +323 -0
  22. code_puppy/agents/agent_typescript_reviewer.py +166 -0
  23. code_puppy/agents/base_agent.py +2156 -0
  24. code_puppy/agents/event_stream_handler.py +348 -0
  25. code_puppy/agents/json_agent.py +202 -0
  26. code_puppy/agents/pack/__init__.py +34 -0
  27. code_puppy/agents/pack/bloodhound.py +304 -0
  28. code_puppy/agents/pack/husky.py +327 -0
  29. code_puppy/agents/pack/retriever.py +393 -0
  30. code_puppy/agents/pack/shepherd.py +348 -0
  31. code_puppy/agents/pack/terrier.py +287 -0
  32. code_puppy/agents/pack/watchdog.py +367 -0
  33. code_puppy/agents/prompt_reviewer.py +145 -0
  34. code_puppy/agents/subagent_stream_handler.py +276 -0
  35. code_puppy/api/__init__.py +13 -0
  36. code_puppy/api/app.py +169 -0
  37. code_puppy/api/main.py +21 -0
  38. code_puppy/api/pty_manager.py +453 -0
  39. code_puppy/api/routers/__init__.py +12 -0
  40. code_puppy/api/routers/agents.py +36 -0
  41. code_puppy/api/routers/commands.py +217 -0
  42. code_puppy/api/routers/config.py +75 -0
  43. code_puppy/api/routers/sessions.py +234 -0
  44. code_puppy/api/templates/terminal.html +361 -0
  45. code_puppy/api/websocket.py +154 -0
  46. code_puppy/callbacks.py +692 -0
  47. code_puppy/chatgpt_codex_client.py +338 -0
  48. code_puppy/claude_cache_client.py +672 -0
  49. code_puppy/cli_runner.py +1073 -0
  50. code_puppy/command_line/__init__.py +1 -0
  51. code_puppy/command_line/add_model_menu.py +1092 -0
  52. code_puppy/command_line/agent_menu.py +662 -0
  53. code_puppy/command_line/attachments.py +395 -0
  54. code_puppy/command_line/autosave_menu.py +704 -0
  55. code_puppy/command_line/clipboard.py +527 -0
  56. code_puppy/command_line/colors_menu.py +532 -0
  57. code_puppy/command_line/command_handler.py +293 -0
  58. code_puppy/command_line/command_registry.py +150 -0
  59. code_puppy/command_line/config_commands.py +719 -0
  60. code_puppy/command_line/core_commands.py +867 -0
  61. code_puppy/command_line/diff_menu.py +865 -0
  62. code_puppy/command_line/file_path_completion.py +73 -0
  63. code_puppy/command_line/load_context_completion.py +52 -0
  64. code_puppy/command_line/mcp/__init__.py +10 -0
  65. code_puppy/command_line/mcp/base.py +32 -0
  66. code_puppy/command_line/mcp/catalog_server_installer.py +175 -0
  67. code_puppy/command_line/mcp/custom_server_form.py +688 -0
  68. code_puppy/command_line/mcp/custom_server_installer.py +195 -0
  69. code_puppy/command_line/mcp/edit_command.py +148 -0
  70. code_puppy/command_line/mcp/handler.py +138 -0
  71. code_puppy/command_line/mcp/help_command.py +147 -0
  72. code_puppy/command_line/mcp/install_command.py +214 -0
  73. code_puppy/command_line/mcp/install_menu.py +705 -0
  74. code_puppy/command_line/mcp/list_command.py +94 -0
  75. code_puppy/command_line/mcp/logs_command.py +235 -0
  76. code_puppy/command_line/mcp/remove_command.py +82 -0
  77. code_puppy/command_line/mcp/restart_command.py +100 -0
  78. code_puppy/command_line/mcp/search_command.py +123 -0
  79. code_puppy/command_line/mcp/start_all_command.py +135 -0
  80. code_puppy/command_line/mcp/start_command.py +117 -0
  81. code_puppy/command_line/mcp/status_command.py +184 -0
  82. code_puppy/command_line/mcp/stop_all_command.py +112 -0
  83. code_puppy/command_line/mcp/stop_command.py +80 -0
  84. code_puppy/command_line/mcp/test_command.py +107 -0
  85. code_puppy/command_line/mcp/utils.py +129 -0
  86. code_puppy/command_line/mcp/wizard_utils.py +334 -0
  87. code_puppy/command_line/mcp_completion.py +174 -0
  88. code_puppy/command_line/model_picker_completion.py +197 -0
  89. code_puppy/command_line/model_settings_menu.py +932 -0
  90. code_puppy/command_line/motd.py +96 -0
  91. code_puppy/command_line/onboarding_slides.py +179 -0
  92. code_puppy/command_line/onboarding_wizard.py +342 -0
  93. code_puppy/command_line/pin_command_completion.py +329 -0
  94. code_puppy/command_line/prompt_toolkit_completion.py +846 -0
  95. code_puppy/command_line/session_commands.py +302 -0
  96. code_puppy/command_line/shell_passthrough.py +145 -0
  97. code_puppy/command_line/skills_completion.py +160 -0
  98. code_puppy/command_line/uc_menu.py +893 -0
  99. code_puppy/command_line/utils.py +93 -0
  100. code_puppy/command_line/wiggum_state.py +78 -0
  101. code_puppy/config.py +1770 -0
  102. code_puppy/error_logging.py +134 -0
  103. code_puppy/gemini_code_assist.py +385 -0
  104. code_puppy/gemini_model.py +754 -0
  105. code_puppy/hook_engine/README.md +105 -0
  106. code_puppy/hook_engine/__init__.py +21 -0
  107. code_puppy/hook_engine/aliases.py +155 -0
  108. code_puppy/hook_engine/engine.py +221 -0
  109. code_puppy/hook_engine/executor.py +296 -0
  110. code_puppy/hook_engine/matcher.py +156 -0
  111. code_puppy/hook_engine/models.py +240 -0
  112. code_puppy/hook_engine/registry.py +106 -0
  113. code_puppy/hook_engine/validator.py +144 -0
  114. code_puppy/http_utils.py +361 -0
  115. code_puppy/keymap.py +128 -0
  116. code_puppy/main.py +10 -0
  117. code_puppy/mcp_/__init__.py +66 -0
  118. code_puppy/mcp_/async_lifecycle.py +286 -0
  119. code_puppy/mcp_/blocking_startup.py +469 -0
  120. code_puppy/mcp_/captured_stdio_server.py +275 -0
  121. code_puppy/mcp_/circuit_breaker.py +290 -0
  122. code_puppy/mcp_/config_wizard.py +507 -0
  123. code_puppy/mcp_/dashboard.py +308 -0
  124. code_puppy/mcp_/error_isolation.py +407 -0
  125. code_puppy/mcp_/examples/retry_example.py +226 -0
  126. code_puppy/mcp_/health_monitor.py +589 -0
  127. code_puppy/mcp_/managed_server.py +428 -0
  128. code_puppy/mcp_/manager.py +807 -0
  129. code_puppy/mcp_/mcp_logs.py +224 -0
  130. code_puppy/mcp_/registry.py +451 -0
  131. code_puppy/mcp_/retry_manager.py +337 -0
  132. code_puppy/mcp_/server_registry_catalog.py +1126 -0
  133. code_puppy/mcp_/status_tracker.py +355 -0
  134. code_puppy/mcp_/system_tools.py +209 -0
  135. code_puppy/mcp_prompts/__init__.py +1 -0
  136. code_puppy/mcp_prompts/hook_creator.py +103 -0
  137. code_puppy/messaging/__init__.py +255 -0
  138. code_puppy/messaging/bus.py +613 -0
  139. code_puppy/messaging/commands.py +167 -0
  140. code_puppy/messaging/markdown_patches.py +57 -0
  141. code_puppy/messaging/message_queue.py +361 -0
  142. code_puppy/messaging/messages.py +569 -0
  143. code_puppy/messaging/queue_console.py +271 -0
  144. code_puppy/messaging/renderers.py +311 -0
  145. code_puppy/messaging/rich_renderer.py +1158 -0
  146. code_puppy/messaging/spinner/__init__.py +83 -0
  147. code_puppy/messaging/spinner/console_spinner.py +240 -0
  148. code_puppy/messaging/spinner/spinner_base.py +95 -0
  149. code_puppy/messaging/subagent_console.py +460 -0
  150. code_puppy/model_factory.py +848 -0
  151. code_puppy/model_switching.py +63 -0
  152. code_puppy/model_utils.py +168 -0
  153. code_puppy/models.json +174 -0
  154. code_puppy/models_dev_api.json +1 -0
  155. code_puppy/models_dev_parser.py +592 -0
  156. code_puppy/plugins/__init__.py +186 -0
  157. code_puppy/plugins/agent_skills/__init__.py +22 -0
  158. code_puppy/plugins/agent_skills/config.py +175 -0
  159. code_puppy/plugins/agent_skills/discovery.py +136 -0
  160. code_puppy/plugins/agent_skills/downloader.py +392 -0
  161. code_puppy/plugins/agent_skills/installer.py +22 -0
  162. code_puppy/plugins/agent_skills/metadata.py +219 -0
  163. code_puppy/plugins/agent_skills/prompt_builder.py +60 -0
  164. code_puppy/plugins/agent_skills/register_callbacks.py +241 -0
  165. code_puppy/plugins/agent_skills/remote_catalog.py +322 -0
  166. code_puppy/plugins/agent_skills/skill_catalog.py +257 -0
  167. code_puppy/plugins/agent_skills/skills_install_menu.py +664 -0
  168. code_puppy/plugins/agent_skills/skills_menu.py +781 -0
  169. code_puppy/plugins/antigravity_oauth/__init__.py +10 -0
  170. code_puppy/plugins/antigravity_oauth/accounts.py +406 -0
  171. code_puppy/plugins/antigravity_oauth/antigravity_model.py +706 -0
  172. code_puppy/plugins/antigravity_oauth/config.py +42 -0
  173. code_puppy/plugins/antigravity_oauth/constants.py +133 -0
  174. code_puppy/plugins/antigravity_oauth/oauth.py +478 -0
  175. code_puppy/plugins/antigravity_oauth/register_callbacks.py +518 -0
  176. code_puppy/plugins/antigravity_oauth/storage.py +288 -0
  177. code_puppy/plugins/antigravity_oauth/test_plugin.py +319 -0
  178. code_puppy/plugins/antigravity_oauth/token.py +167 -0
  179. code_puppy/plugins/antigravity_oauth/transport.py +863 -0
  180. code_puppy/plugins/antigravity_oauth/utils.py +168 -0
  181. code_puppy/plugins/chatgpt_oauth/__init__.py +8 -0
  182. code_puppy/plugins/chatgpt_oauth/config.py +52 -0
  183. code_puppy/plugins/chatgpt_oauth/oauth_flow.py +329 -0
  184. code_puppy/plugins/chatgpt_oauth/register_callbacks.py +176 -0
  185. code_puppy/plugins/chatgpt_oauth/test_plugin.py +301 -0
  186. code_puppy/plugins/chatgpt_oauth/utils.py +523 -0
  187. code_puppy/plugins/claude_code_hooks/__init__.py +1 -0
  188. code_puppy/plugins/claude_code_hooks/config.py +137 -0
  189. code_puppy/plugins/claude_code_hooks/register_callbacks.py +175 -0
  190. code_puppy/plugins/claude_code_oauth/README.md +167 -0
  191. code_puppy/plugins/claude_code_oauth/SETUP.md +93 -0
  192. code_puppy/plugins/claude_code_oauth/__init__.py +25 -0
  193. code_puppy/plugins/claude_code_oauth/config.py +52 -0
  194. code_puppy/plugins/claude_code_oauth/register_callbacks.py +453 -0
  195. code_puppy/plugins/claude_code_oauth/test_plugin.py +283 -0
  196. code_puppy/plugins/claude_code_oauth/token_refresh_heartbeat.py +241 -0
  197. code_puppy/plugins/claude_code_oauth/utils.py +640 -0
  198. code_puppy/plugins/customizable_commands/__init__.py +0 -0
  199. code_puppy/plugins/customizable_commands/register_callbacks.py +152 -0
  200. code_puppy/plugins/example_custom_command/README.md +280 -0
  201. code_puppy/plugins/example_custom_command/register_callbacks.py +51 -0
  202. code_puppy/plugins/file_permission_handler/__init__.py +4 -0
  203. code_puppy/plugins/file_permission_handler/register_callbacks.py +470 -0
  204. code_puppy/plugins/frontend_emitter/__init__.py +25 -0
  205. code_puppy/plugins/frontend_emitter/emitter.py +121 -0
  206. code_puppy/plugins/frontend_emitter/register_callbacks.py +261 -0
  207. code_puppy/plugins/hook_creator/__init__.py +1 -0
  208. code_puppy/plugins/hook_creator/register_callbacks.py +33 -0
  209. code_puppy/plugins/hook_manager/__init__.py +1 -0
  210. code_puppy/plugins/hook_manager/config.py +290 -0
  211. code_puppy/plugins/hook_manager/hooks_menu.py +564 -0
  212. code_puppy/plugins/hook_manager/register_callbacks.py +227 -0
  213. code_puppy/plugins/oauth_puppy_html.py +228 -0
  214. code_puppy/plugins/scheduler/__init__.py +1 -0
  215. code_puppy/plugins/scheduler/register_callbacks.py +88 -0
  216. code_puppy/plugins/scheduler/scheduler_menu.py +522 -0
  217. code_puppy/plugins/scheduler/scheduler_wizard.py +341 -0
  218. code_puppy/plugins/shell_safety/__init__.py +6 -0
  219. code_puppy/plugins/shell_safety/agent_shell_safety.py +69 -0
  220. code_puppy/plugins/shell_safety/command_cache.py +156 -0
  221. code_puppy/plugins/shell_safety/register_callbacks.py +202 -0
  222. code_puppy/plugins/synthetic_status/__init__.py +1 -0
  223. code_puppy/plugins/synthetic_status/register_callbacks.py +132 -0
  224. code_puppy/plugins/synthetic_status/status_api.py +147 -0
  225. code_puppy/plugins/universal_constructor/__init__.py +13 -0
  226. code_puppy/plugins/universal_constructor/models.py +138 -0
  227. code_puppy/plugins/universal_constructor/register_callbacks.py +47 -0
  228. code_puppy/plugins/universal_constructor/registry.py +302 -0
  229. code_puppy/plugins/universal_constructor/sandbox.py +584 -0
  230. code_puppy/prompts/antigravity_system_prompt.md +1 -0
  231. code_puppy/pydantic_patches.py +356 -0
  232. code_puppy/reopenable_async_client.py +232 -0
  233. code_puppy/round_robin_model.py +150 -0
  234. code_puppy/scheduler/__init__.py +41 -0
  235. code_puppy/scheduler/__main__.py +9 -0
  236. code_puppy/scheduler/cli.py +118 -0
  237. code_puppy/scheduler/config.py +126 -0
  238. code_puppy/scheduler/daemon.py +280 -0
  239. code_puppy/scheduler/executor.py +155 -0
  240. code_puppy/scheduler/platform.py +19 -0
  241. code_puppy/scheduler/platform_unix.py +22 -0
  242. code_puppy/scheduler/platform_win.py +32 -0
  243. code_puppy/session_storage.py +338 -0
  244. code_puppy/status_display.py +257 -0
  245. code_puppy/summarization_agent.py +176 -0
  246. code_puppy/terminal_utils.py +418 -0
  247. code_puppy/tools/__init__.py +501 -0
  248. code_puppy/tools/agent_tools.py +603 -0
  249. code_puppy/tools/ask_user_question/__init__.py +26 -0
  250. code_puppy/tools/ask_user_question/constants.py +73 -0
  251. code_puppy/tools/ask_user_question/demo_tui.py +55 -0
  252. code_puppy/tools/ask_user_question/handler.py +232 -0
  253. code_puppy/tools/ask_user_question/models.py +304 -0
  254. code_puppy/tools/ask_user_question/registration.py +26 -0
  255. code_puppy/tools/ask_user_question/renderers.py +309 -0
  256. code_puppy/tools/ask_user_question/terminal_ui.py +329 -0
  257. code_puppy/tools/ask_user_question/theme.py +155 -0
  258. code_puppy/tools/ask_user_question/tui_loop.py +423 -0
  259. code_puppy/tools/browser/__init__.py +37 -0
  260. code_puppy/tools/browser/browser_control.py +289 -0
  261. code_puppy/tools/browser/browser_interactions.py +545 -0
  262. code_puppy/tools/browser/browser_locators.py +640 -0
  263. code_puppy/tools/browser/browser_manager.py +378 -0
  264. code_puppy/tools/browser/browser_navigation.py +251 -0
  265. code_puppy/tools/browser/browser_screenshot.py +179 -0
  266. code_puppy/tools/browser/browser_scripts.py +462 -0
  267. code_puppy/tools/browser/browser_workflows.py +221 -0
  268. code_puppy/tools/browser/chromium_terminal_manager.py +259 -0
  269. code_puppy/tools/browser/terminal_command_tools.py +534 -0
  270. code_puppy/tools/browser/terminal_screenshot_tools.py +552 -0
  271. code_puppy/tools/browser/terminal_tools.py +525 -0
  272. code_puppy/tools/command_runner.py +1346 -0
  273. code_puppy/tools/common.py +1409 -0
  274. code_puppy/tools/display.py +84 -0
  275. code_puppy/tools/file_modifications.py +886 -0
  276. code_puppy/tools/file_operations.py +802 -0
  277. code_puppy/tools/scheduler_tools.py +412 -0
  278. code_puppy/tools/skills_tools.py +244 -0
  279. code_puppy/tools/subagent_context.py +158 -0
  280. code_puppy/tools/tools_content.py +51 -0
  281. code_puppy/tools/universal_constructor.py +889 -0
  282. code_puppy/uvx_detection.py +242 -0
  283. code_puppy/version_checker.py +82 -0
  284. codepp-0.0.437.dist-info/METADATA +766 -0
  285. codepp-0.0.437.dist-info/RECORD +288 -0
  286. codepp-0.0.437.dist-info/WHEEL +4 -0
  287. codepp-0.0.437.dist-info/entry_points.txt +3 -0
  288. codepp-0.0.437.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,453 @@
1
+ """
2
+ Claude Code OAuth Plugin for Code Puppy.
3
+
4
+ Provides OAuth authentication for Claude Code models and registers
5
+ the 'claude_code' model type handler.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import logging
11
+ import threading
12
+ import time
13
+ from http.server import BaseHTTPRequestHandler, HTTPServer
14
+ from typing import Any, Dict, List, Optional, Tuple
15
+ from urllib.parse import parse_qs, urlparse
16
+
17
+ from code_puppy.callbacks import register_callback
18
+ from code_puppy.messaging import emit_error, emit_info, emit_success, emit_warning
19
+ from code_puppy.model_switching import set_model_and_reload_agent
20
+
21
+ from ..oauth_puppy_html import oauth_failure_html, oauth_success_html
22
+ from .config import CLAUDE_CODE_OAUTH_CONFIG, get_token_storage_path
23
+ from .utils import (
24
+ OAuthContext,
25
+ add_models_to_extra_config,
26
+ assign_redirect_uri,
27
+ build_authorization_url,
28
+ exchange_code_for_tokens,
29
+ fetch_claude_code_models,
30
+ get_valid_access_token,
31
+ load_claude_models_filtered,
32
+ load_stored_tokens,
33
+ prepare_oauth_context,
34
+ remove_claude_code_models,
35
+ save_tokens,
36
+ )
37
+
38
+ logger = logging.getLogger(__name__)
39
+
40
+
41
+ class _OAuthResult:
42
+ def __init__(self) -> None:
43
+ self.code: Optional[str] = None
44
+ self.state: Optional[str] = None
45
+ self.error: Optional[str] = None
46
+
47
+
48
+ class _CallbackHandler(BaseHTTPRequestHandler):
49
+ result: _OAuthResult
50
+ received_event: threading.Event
51
+
52
+ def do_GET(self) -> None: # noqa: N802
53
+ logger.info("Callback received: path=%s", self.path)
54
+ parsed = urlparse(self.path)
55
+ params: Dict[str, List[str]] = parse_qs(parsed.query)
56
+
57
+ code = params.get("code", [None])[0]
58
+ state = params.get("state", [None])[0]
59
+
60
+ if code and state:
61
+ self.result.code = code
62
+ self.result.state = state
63
+ success_html = oauth_success_html(
64
+ "Claude Code",
65
+ "You're totally synced with Claude Code now!",
66
+ )
67
+ self._write_response(200, success_html)
68
+ else:
69
+ self.result.error = "Missing code or state"
70
+ failure_html = oauth_failure_html(
71
+ "Claude Code",
72
+ "Missing code or state parameter 🥺",
73
+ )
74
+ self._write_response(400, failure_html)
75
+
76
+ self.received_event.set()
77
+
78
+ def log_message(self, log_format: str, *args: Any) -> None:
79
+ return
80
+
81
+ def _write_response(self, status: int, body: str) -> None:
82
+ self.send_response(status)
83
+ self.send_header("Content-Type", "text/html; charset=utf-8")
84
+ self.end_headers()
85
+ self.wfile.write(body.encode("utf-8"))
86
+
87
+
88
+ def _start_callback_server(
89
+ context: OAuthContext,
90
+ ) -> Optional[Tuple[HTTPServer, _OAuthResult, threading.Event]]:
91
+ port_range = CLAUDE_CODE_OAUTH_CONFIG["callback_port_range"]
92
+
93
+ for port in range(port_range[0], port_range[1] + 1):
94
+ try:
95
+ server = HTTPServer(("localhost", port), _CallbackHandler)
96
+ assign_redirect_uri(context, port)
97
+ result = _OAuthResult()
98
+ event = threading.Event()
99
+ _CallbackHandler.result = result
100
+ _CallbackHandler.received_event = event
101
+
102
+ def run_server(server=server) -> None:
103
+ with server:
104
+ server.serve_forever()
105
+
106
+ threading.Thread(target=run_server, daemon=True).start()
107
+ return server, result, event
108
+ except OSError:
109
+ continue
110
+
111
+ emit_error("Could not start OAuth callback server; all candidate ports are in use")
112
+ return None
113
+
114
+
115
+ def _await_callback(context: OAuthContext) -> Optional[str]:
116
+ timeout = CLAUDE_CODE_OAUTH_CONFIG["callback_timeout"]
117
+
118
+ started = _start_callback_server(context)
119
+ if not started:
120
+ return None
121
+
122
+ server, result, event = started
123
+ redirect_uri = context.redirect_uri
124
+ if not redirect_uri:
125
+ emit_error("Failed to assign redirect URI for OAuth flow")
126
+ server.shutdown()
127
+ return None
128
+
129
+ auth_url = build_authorization_url(context)
130
+
131
+ try:
132
+ import webbrowser
133
+
134
+ from code_puppy.tools.common import should_suppress_browser
135
+
136
+ if should_suppress_browser():
137
+ emit_info(
138
+ "[HEADLESS MODE] Would normally open browser for Claude Code OAuth…"
139
+ )
140
+ emit_info(f"In normal mode, would visit: {auth_url}")
141
+ else:
142
+ emit_info("Opening browser for Claude Code OAuth…")
143
+ webbrowser.open(auth_url)
144
+ emit_info(f"If it doesn't open automatically, visit: {auth_url}")
145
+ except Exception as exc: # pragma: no cover
146
+ if not should_suppress_browser():
147
+ emit_warning(f"Failed to open browser automatically: {exc}")
148
+ emit_info(f"Please open the URL manually: {auth_url}")
149
+
150
+ emit_info(f"Listening for callback on {redirect_uri}")
151
+ emit_info(
152
+ "If Claude redirects you to the console callback page, copy the full URL "
153
+ "and paste it back into Code Puppy."
154
+ )
155
+
156
+ if not event.wait(timeout=timeout):
157
+ emit_error("OAuth callback timed out. Please try again.")
158
+ server.shutdown()
159
+ return None
160
+
161
+ server.shutdown()
162
+
163
+ if result.error:
164
+ emit_error(f"OAuth callback error: {result.error}")
165
+ return None
166
+
167
+ if result.state != context.state:
168
+ emit_error("State mismatch detected; aborting authentication.")
169
+ return None
170
+
171
+ return result.code
172
+
173
+
174
+ def _custom_help() -> List[Tuple[str, str]]:
175
+ return [
176
+ (
177
+ "claude-code-auth",
178
+ "Authenticate with Claude Code via OAuth and import available models",
179
+ ),
180
+ (
181
+ "claude-code-status",
182
+ "Check Claude Code OAuth authentication status and configured models",
183
+ ),
184
+ ("claude-code-logout", "Remove Claude Code OAuth tokens and imported models"),
185
+ ]
186
+
187
+
188
+ def _perform_authentication() -> None:
189
+ context = prepare_oauth_context()
190
+ code = _await_callback(context)
191
+ if not code:
192
+ return
193
+
194
+ emit_info("Exchanging authorization code for tokens…")
195
+ tokens = exchange_code_for_tokens(code, context)
196
+ if not tokens:
197
+ emit_error("Token exchange failed. Please retry the authentication flow.")
198
+ return
199
+
200
+ if not save_tokens(tokens):
201
+ emit_error(
202
+ "Tokens retrieved but failed to save locally. Check file permissions."
203
+ )
204
+ return
205
+
206
+ emit_success("Claude Code OAuth authentication successful!")
207
+
208
+ access_token = tokens.get("access_token")
209
+ if not access_token:
210
+ emit_warning("No access token returned; skipping model discovery.")
211
+ return
212
+
213
+ emit_info("Fetching available Claude Code models…")
214
+ models = fetch_claude_code_models(access_token)
215
+ if not models:
216
+ emit_warning(
217
+ "Claude Code authentication succeeded but no models were returned."
218
+ )
219
+ return
220
+
221
+ emit_info(f"Discovered {len(models)} models: {', '.join(models)}")
222
+ if add_models_to_extra_config(models):
223
+ emit_success(
224
+ "Claude Code models added to your configuration. Use the `claude-code-` prefix!"
225
+ )
226
+
227
+
228
+ def _handle_custom_command(command: str, name: str) -> Optional[bool]:
229
+ if not name:
230
+ return None
231
+
232
+ if name == "claude-code-auth":
233
+ emit_info("Starting Claude Code OAuth authentication…")
234
+ tokens = load_stored_tokens()
235
+ if tokens and tokens.get("access_token"):
236
+ emit_warning(
237
+ "Existing Claude Code tokens found. Continuing will overwrite them."
238
+ )
239
+ _perform_authentication()
240
+ set_model_and_reload_agent("claude-code-claude-opus-4-6")
241
+ return True
242
+
243
+ if name == "claude-code-status":
244
+ tokens = load_stored_tokens()
245
+ if tokens and tokens.get("access_token"):
246
+ emit_success("Claude Code OAuth: Authenticated")
247
+ expires_at = tokens.get("expires_at")
248
+ if expires_at:
249
+ remaining = max(0, int(expires_at - time.time()))
250
+ hours, minutes = divmod(remaining // 60, 60)
251
+ emit_info(f"Token expires in ~{hours}h {minutes}m")
252
+
253
+ claude_models = [
254
+ name
255
+ for name, cfg in load_claude_models_filtered().items()
256
+ if cfg.get("oauth_source") == "claude-code-plugin"
257
+ ]
258
+ if claude_models:
259
+ emit_info(f"Configured Claude Code models: {', '.join(claude_models)}")
260
+ else:
261
+ emit_warning("No Claude Code models configured yet.")
262
+ else:
263
+ emit_warning("Claude Code OAuth: Not authenticated")
264
+ emit_info("Run /claude-code-auth to begin the browser sign-in flow.")
265
+ return True
266
+
267
+ if name == "claude-code-logout":
268
+ token_path = get_token_storage_path()
269
+ if token_path.exists():
270
+ token_path.unlink()
271
+ emit_info("Removed Claude Code OAuth tokens")
272
+
273
+ removed = remove_claude_code_models()
274
+ if removed:
275
+ emit_info(f"Removed {removed} Claude Code models from configuration")
276
+
277
+ emit_success("Claude Code logout complete")
278
+ return True
279
+
280
+ return None
281
+
282
+
283
+ def _create_claude_code_model(model_name: str, model_config: Dict, config: Dict) -> Any:
284
+ """Create a Claude Code model instance.
285
+
286
+ This handler is registered via the 'register_model_type' callback to handle
287
+ models with type='claude_code'.
288
+ """
289
+ from anthropic import AsyncAnthropic
290
+ from pydantic_ai.models.anthropic import AnthropicModel
291
+ from pydantic_ai.providers.anthropic import AnthropicProvider
292
+
293
+ from code_puppy.claude_cache_client import (
294
+ ClaudeCacheAsyncClient,
295
+ patch_anthropic_client_messages,
296
+ )
297
+ from code_puppy.config import get_effective_model_settings
298
+ from code_puppy.http_utils import get_cert_bundle_path
299
+ from code_puppy.model_factory import get_custom_config
300
+
301
+ url, headers, verify, api_key = get_custom_config(model_config)
302
+
303
+ # Refresh token if this is from the plugin
304
+ if model_config.get("oauth_source") == "claude-code-plugin":
305
+ refreshed_token = get_valid_access_token()
306
+ if refreshed_token:
307
+ api_key = refreshed_token
308
+ custom_endpoint = model_config.get("custom_endpoint")
309
+ if isinstance(custom_endpoint, dict):
310
+ custom_endpoint["api_key"] = refreshed_token
311
+
312
+ if not api_key:
313
+ emit_warning(
314
+ f"API key is not set for Claude Code endpoint; skipping model '{model_config.get('name')}'."
315
+ )
316
+ return None
317
+
318
+ # Check if interleaved thinking is enabled (defaults to True for OAuth models)
319
+ effective_settings = get_effective_model_settings(model_name)
320
+ interleaved_thinking = effective_settings.get("interleaved_thinking", True)
321
+
322
+ # Handle anthropic-beta header based on interleaved_thinking setting
323
+ if "anthropic-beta" in headers:
324
+ beta_parts = [p.strip() for p in headers["anthropic-beta"].split(",")]
325
+ if interleaved_thinking:
326
+ if "interleaved-thinking-2025-05-14" not in beta_parts:
327
+ beta_parts.append("interleaved-thinking-2025-05-14")
328
+ else:
329
+ beta_parts = [p for p in beta_parts if "interleaved-thinking" not in p]
330
+ headers["anthropic-beta"] = ",".join(beta_parts) if beta_parts else None
331
+ if headers.get("anthropic-beta") is None:
332
+ del headers["anthropic-beta"]
333
+ elif interleaved_thinking:
334
+ headers["anthropic-beta"] = "interleaved-thinking-2025-05-14"
335
+
336
+ # Add 1M context beta header for long-context models
337
+ from code_puppy.model_factory import CONTEXT_1M_BETA
338
+
339
+ if model_config.get("context_length", 0) >= 1_000_000:
340
+ if "anthropic-beta" in headers:
341
+ beta_parts = [p.strip() for p in headers["anthropic-beta"].split(",")]
342
+ if CONTEXT_1M_BETA not in beta_parts:
343
+ beta_parts.append(CONTEXT_1M_BETA)
344
+ headers["anthropic-beta"] = ",".join(beta_parts)
345
+ else:
346
+ headers["anthropic-beta"] = CONTEXT_1M_BETA
347
+
348
+ # Use a dedicated client wrapper that injects cache_control on /v1/messages
349
+ if verify is None:
350
+ verify = get_cert_bundle_path()
351
+
352
+ # Disable HTTP/2 for Claude Code OAuth - the UnprefixingStream wrapper
353
+ # that transforms tool names in streaming responses doesn't play well
354
+ # with HTTP/2's compression handling, causing zlib decompression errors.
355
+ client = ClaudeCacheAsyncClient(
356
+ headers=headers,
357
+ verify=verify,
358
+ timeout=180,
359
+ http2=False,
360
+ )
361
+
362
+ anthropic_client = AsyncAnthropic(
363
+ base_url=url,
364
+ http_client=client,
365
+ auth_token=api_key,
366
+ )
367
+ patch_anthropic_client_messages(anthropic_client)
368
+ anthropic_client.api_key = None
369
+ anthropic_client.auth_token = api_key
370
+ provider = AnthropicProvider(anthropic_client=anthropic_client)
371
+ return AnthropicModel(model_name=model_config["name"], provider=provider)
372
+
373
+
374
+ def _register_model_types() -> List[Dict[str, Any]]:
375
+ """Register the claude_code model type handler."""
376
+ return [{"type": "claude_code", "handler": _create_claude_code_model}]
377
+
378
+
379
+ # Global storage for the token refresh heartbeat
380
+ # Using a dict to allow multiple concurrent agent runs (keyed by session_id)
381
+ _active_heartbeats: Dict[str, Any] = {}
382
+
383
+
384
+ async def _on_agent_run_start(
385
+ agent_name: str,
386
+ model_name: str,
387
+ session_id: Optional[str] = None,
388
+ ) -> None:
389
+ """Start token refresh heartbeat for Claude Code OAuth models.
390
+
391
+ This callback is triggered when an agent run starts. If the model is a
392
+ Claude Code OAuth model, we start a background heartbeat to keep the
393
+ token fresh during long-running operations.
394
+ """
395
+ # Only start heartbeat for Claude Code models
396
+ if not model_name.startswith("claude-code"):
397
+ return
398
+
399
+ try:
400
+ from .token_refresh_heartbeat import TokenRefreshHeartbeat
401
+
402
+ heartbeat = TokenRefreshHeartbeat()
403
+ await heartbeat.start()
404
+
405
+ # Store heartbeat for cleanup, keyed by session_id
406
+ key = session_id or "default"
407
+ _active_heartbeats[key] = heartbeat
408
+ logger.debug(
409
+ "Started token refresh heartbeat for session %s (model: %s)",
410
+ key,
411
+ model_name,
412
+ )
413
+ except ImportError:
414
+ logger.debug("Token refresh heartbeat module not available")
415
+ except Exception as exc:
416
+ logger.debug("Failed to start token refresh heartbeat: %s", exc)
417
+
418
+
419
+ async def _on_agent_run_end(
420
+ agent_name: str,
421
+ model_name: str,
422
+ session_id: Optional[str] = None,
423
+ success: bool = True,
424
+ error: Optional[Exception] = None,
425
+ response_text: Optional[str] = None,
426
+ metadata: Optional[Dict[str, Any]] = None,
427
+ ) -> None:
428
+ """Stop token refresh heartbeat when agent run ends.
429
+
430
+ This callback is triggered when an agent run completes (success or failure).
431
+ We stop any heartbeat that was started for this session.
432
+ """
433
+ # We don't use response_text or metadata, just cleanup the heartbeat
434
+ key = session_id or "default"
435
+ heartbeat = _active_heartbeats.pop(key, None)
436
+
437
+ if heartbeat is not None:
438
+ try:
439
+ await heartbeat.stop()
440
+ logger.debug(
441
+ "Stopped token refresh heartbeat for session %s (refreshed %d times)",
442
+ key,
443
+ heartbeat.refresh_count,
444
+ )
445
+ except Exception as exc:
446
+ logger.debug("Error stopping token refresh heartbeat: %s", exc)
447
+
448
+
449
+ register_callback("custom_command_help", _custom_help)
450
+ register_callback("custom_command", _handle_custom_command)
451
+ register_callback("register_model_type", _register_model_types)
452
+ register_callback("agent_run_start", _on_agent_run_start)
453
+ register_callback("agent_run_end", _on_agent_run_end)